Merge remote-tracking branch 'upstream/staging' into improve-bulk-edit-and-fixes

This commit is contained in:
Wolfsblvt 2024-03-29 02:42:27 +01:00
commit 9005d3f790
16 changed files with 255 additions and 166 deletions

View File

@ -1,3 +1,4 @@
@echo off
pushd %~dp0
set NODE_ENV=production
call npm install --no-audit --no-fund --quiet --omit=dev

58
package-lock.json generated
View File

@ -21,7 +21,7 @@
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csrf-csrf": "^2.2.3",
"express": "^4.18.2",
"express": "^4.19.2",
"form-data": "^4.0.0",
"google-translate-api-browser": "^3.0.1",
"gpt3-tokenizer": "^1.1.5",
@ -1752,15 +1752,16 @@
"version": "0.1.12"
},
"node_modules/express": {
"version": "4.18.2",
"license": "MIT",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@ -1791,55 +1792,14 @@
"node": ">= 0.10.0"
}
},
"node_modules/express/node_modules/body-parser": {
"version": "1.20.1",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/express/node_modules/bytes": {
"version": "3.1.2",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/cookie": {
"version": "0.5.0",
"license": "MIT",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/raw-body": {
"version": "2.5.1",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/safe-buffer": {
"version": "5.2.1",
"funding": [

View File

@ -11,7 +11,7 @@
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csrf-csrf": "^2.2.3",
"express": "^4.18.2",
"express": "^4.19.2",
"form-data": "^4.0.0",
"google-translate-api-browser": "^3.0.1",
"gpt3-tokenizer": "^1.1.5",

View File

@ -2604,11 +2604,20 @@
<div>
<h4 data-i18n="Google Model">Google Model</h4>
<select id="model_google_select">
<option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
<option value="gemini-pro">Gemini Pro</option>
<option value="gemini-pro-vision">Gemini Pro Vision</option>
<option value="text-bison-001">Bison Text</option>
<option value="chat-bison-001">Bison Chat</option>
<optgroup label="Latest">
<!-- Points to 1.0, no default 1.5 endpoint -->
<option value="gemini-pro">Gemini Pro</option>
<option value="gemini-pro-vision">Gemini Pro Vision</option>
<option value="gemini-ultra">Gemini Ultra</option>
<option value="text-bison-001">Bison Text</option>
<option value="chat-bison-001">Bison Chat</option>
</optgroup>
<optgroup label="Sub-versions">
<option value="gemini-1.5-pro-latest">Gemini 1.5 Pro</option>
<option value="gemini-1.0-pro-latest">Gemini 1.0 Pro</option>
<option value="gemini-1.0-pro-vision-latest">Gemini 1.0 Pro Vision</option>
<option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
</optgroup>
</select>
</div>
</form>
@ -4101,7 +4110,7 @@
</div>
<div id="tags_div">
<div class="tag_controls">
<input id="tagInput" class="text_pole tag_input wide100p margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="50" />
<input id="tagInput" class="text_pole textarea_compact tag_input wide100p margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="50" />
<div class="tags_view menu_button fa-solid fa-tags" title="View all tags" data-i18n="[title]View all tags"></div>
</div>
<div id="tagList" class="tags"></div>
@ -4121,7 +4130,7 @@
</div>
<div id="descriptionWrapper" class="flex-container flexFlowColumn flex1">
<hr>
<div id="description_div" class="marginBot5 flex-container alignitemscenter">
<div id="description_div" class="flex-container alignitemscenter">
<span data-i18n="Character Description">Description</span>
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="description_textarea" title="Expand the editor"></i>
<a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-description" class="notes-link" target="_blank">
@ -4134,7 +4143,7 @@
</div>
</div>
<div id="firstMessageWrapper" class="flex-container flexFlowColumn flex1">
<div id="first_message_div" class="marginBot5 title_restorable">
<div id="first_message_div" class="title_restorable">
<div class="flex-container alignitemscenter flex1">
<span data-i18n="First message">First message</span>
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="firstmessage_textarea" title="Expand the editor"></i>
@ -4184,7 +4193,7 @@
</div>
<div id="group_tags_div" class="wide100p">
<div class="tag_controls">
<input id="groupTagInput" class="text_pole tag_input flex1 margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="50" />
<input id="groupTagInput" class="text_pole textarea_compact tag_input flex1 margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="50" />
<div class="tags_view menu_button fa-solid fa-tags margin0" title="View all tags" data-i18n="[title]View all tags"></div>
</div>
<div id="groupTagList" class="tags paddingTopBot5"></div>

View File

@ -172,6 +172,7 @@ import {
importTags,
tag_filter_types,
compareTagsForSort,
initTags,
} from './scripts/tags.js';
import {
SECRET_KEYS,
@ -413,6 +414,7 @@ export const event_types = {
CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected',
// TODO: Naming convention is inconsistent with other events
CHARACTER_DELETED: 'characterDeleted',
CHARACTER_DUPLICATED: 'character_duplicated',
};
export const eventSource = new EventEmitter();
@ -865,6 +867,7 @@ async function firstLoadInit() {
getSystemMessages();
sendSystemMessage(system_message_types.WELCOME);
initLocales();
initTags();
await getUserAvatars(true, user_avatar);
await getCharacters();
await getBackgrounds();
@ -1238,7 +1241,7 @@ function getCharacterBlock(item, id) {
const template = $('#character_template .character_select').clone();
template.attr({ 'chid': id, 'id': `CharID${id}` });
template.find('img').attr('src', this_avatar).attr('alt', item.name);
template.find('.avatar').attr('title', `[Character] ${item.name}`);
template.find('.avatar').attr('title', `[Character] ${item.name}\nFile: ${item.avatar}`);
template.find('.ch_name').text(item.name).attr('title', `[Character] ${item.name}`);
if (power_user.show_card_avatar_urls) {
template.find('.ch_avatar_url').text(item.avatar);
@ -1856,6 +1859,7 @@ function insertSVGIcon(mes, extra) {
function getMessageFromTemplate({
mesId,
swipeId,
characterName,
isUser,
avatarImg,
@ -1873,6 +1877,7 @@ function getMessageFromTemplate({
const mes = messageTemplate.clone();
mes.attr({
'mesid': mesId,
'swipeid': swipeId,
'ch_name': characterName,
'is_user': isUser,
'is_system': !!isSystem,
@ -2019,6 +2024,7 @@ function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true
let params = {
mesId: forceId ?? chat.length - 1,
swipeId: mes.swipe_id ?? 0,
characterName: mes.name,
isUser: mes.is_user,
avatarImg: avatarImg,
@ -2728,9 +2734,7 @@ class StreamingProcessor {
const continueMsg = this.type === 'continue' ? this.messageAlreadyGenerated : undefined;
saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), continueMsg);
await saveChatConditional();
activateSendButtons();
showSwipeButtons();
setGenerationProgress(0);
unblockGeneration();
generatedPromptCache = '';
//console.log("Generated text size:", text.length, text)
@ -2773,11 +2777,8 @@ class StreamingProcessor {
this.isStopped = true;
this.hideMessageButtons(this.messageId);
$('#send_textarea').removeAttr('disabled');
is_send_press = false;
activateSendButtons();
setGenerationProgress(0);
showSwipeButtons();
generatedPromptCache = '';
unblockGeneration();
}
setFirstSwipe(messageId) {
@ -3968,6 +3969,8 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 });
}
generatedPromptCache = '';
unblockGeneration();
console.log(exception);
streamingProcessor = null;
@ -4179,9 +4182,10 @@ export function removeMacros(str) {
* @param {string} messageText Message text.
* @param {string} messageBias Message bias.
* @param {number} [insertAt] Optional index to insert the message at.
* @params {boolean} [compact] Send as a compact display message.
* @returns {Promise<void>} A promise that resolves when the message is inserted.
*/
export async function sendMessageAsUser(messageText, messageBias, insertAt = null) {
export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false) {
messageText = getRegexedString(messageText, regex_placement.USER_INPUT);
const message = {
@ -4190,7 +4194,9 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
is_system: false,
send_date: getMessageTimeStamp(),
mes: substituteParams(messageText),
extra: {},
extra: {
isSmallSys: compact,
},
};
if (power_user.message_token_count_enabled) {
@ -4318,6 +4324,8 @@ async function DupeChar() {
});
if (response.ok) {
toastr.success('Character Duplicated');
const data = await response.json();
await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
getCharacters();
}
}
@ -5736,6 +5744,12 @@ export function setUserAvatar(imgfile) {
$('.zoomed_avatar[forchar]').remove();
}
export function retriggerFirstMessageOnEmptyChat() {
if (this_chid >= 0 && !selected_group && chat.length === 1) {
$('#firstmessage_textarea').trigger('input');
}
}
async function uploadUserAvatar(e) {
const file = e.target.files[0];
@ -8470,9 +8484,7 @@ jQuery(async function () {
setUserAvatar(imgfile);
// force firstMes {{user}} update on persona switch
if (this_chid >= 0 && !selected_group && chat.length === 1) {
$('#firstmessage_textarea').trigger('input');
}
retriggerFirstMessageOnEmptyChat();
});
$(document).on('click', '#user_avatar_block .avatar_upload', function () {
$('#avatar_upload_overwrite').val('');
@ -8597,11 +8609,15 @@ jQuery(async function () {
await clearChat();
chat.length = 0;
chat_file_for_del = getCurrentChatDetails().sessionName;
const isDelChatCheckbox = document.getElementById('del_chat_checkbox').checked;
chat_file_for_del = getCurrentChatDetails()?.sessionName;
const isDelChatCheckbox = document.getElementById('del_chat_checkbox')?.checked;
// Make it easier to find in backups
if (isDelChatCheckbox) {
await saveChatConditional();
}
if (selected_group) {
//Fix it; When you're creating a new group chat (but not when initially converting from the existing regular chat), the first greeting message doesn't automatically get translated.
await createNewGroupChat(selected_group);
if (isDelChatCheckbox) await deleteGroupChat(selected_group, chat_file_for_del);
}
@ -8664,14 +8680,13 @@ jQuery(async function () {
$('#form_create').submit(createOrEditCharacter);
$('#delete_button').on('click', function () {
popup_type = 'del_ch';
callPopup(`
<h3>Delete the character?</h3>
<b>THIS IS PERMANENT!<br><br>
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<span>Also delete the chat files</span>
</label><br></b>`,
<small>Also delete the chat files</small>
</label><br></b>`, 'del_ch', '',
);
});
@ -8979,7 +8994,7 @@ jQuery(async function () {
<label for="del_chat_checkbox" class="checkbox_label justifyCenter"
title="If necessary, you can later restore this chat file from the /backups folder">
<input type="checkbox" id="del_chat_checkbox" />
<span>Also delete the current chat file</span>
<small>Also delete the current chat file</small>
</label><br>
`, 'new_chat', '');
}
@ -9576,6 +9591,7 @@ jQuery(async function () {
const userName = String($('#your_name').val()).trim();
setUserName(userName);
await updatePersonaNameIfExists(user_avatar, userName);
retriggerFirstMessageOnEmptyChat();
});
$('#sync_name_button').on('click', async function () {

View File

@ -50,16 +50,24 @@ class CharacterContextMenu {
* Duplicate one or more characters
*
* @param characterId
* @returns {Promise<Response>}
* @returns {Promise<any>}
*/
static duplicate = async (characterId) => {
const character = CharacterContextMenu.#getCharacter(characterId);
const body = { avatar_url: character.avatar };
return fetch('/api/characters/duplicate', {
const result = await fetch('/api/characters/duplicate', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ avatar_url: character.avatar }),
body: JSON.stringify(body),
});
if (!result.ok) {
throw new Error('Character not duplicated');
}
const data = await result.json();
await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
};
/**

View File

@ -403,6 +403,7 @@ function saveUserInput() {
const userInput = String($('#send_textarea').val());
SaveLocal('userInput', userInput);
}
const saveUserInputDebounced = debounce(saveUserInput);
// Make the DIV element draggable:
@ -662,6 +663,30 @@ export async function initMovingUI() {
}
}
/**@type {HTMLTextAreaElement} */
const sendTextArea = document.querySelector('#send_textarea');
const chatBlock = document.getElementById('chat');
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
/**
* this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
*/
function autoFitSendTextArea() {
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
if (sendTextArea.scrollHeight == sendTextArea.offsetHeight) {
// Needs to be pulled dynamically because it is affected by font size changes
const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height');
sendTextArea.style.height = sendTextAreaMinHeight;
}
sendTextArea.style.height = sendTextArea.scrollHeight + 0.3 + 'px';
if (!isFirefox) {
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));
chatBlock.scrollTop = newScrollTop;
}
}
export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea);
// ---------------------------------------------------
export function initRossMods() {
@ -824,19 +849,13 @@ export function initRossMods() {
saveSettingsDebounced();
});
//this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
$('#send_textarea').on('input', function () {
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const chatBlock = $('#chat');
const originalScrollBottom = chatBlock[0].scrollHeight - (chatBlock.scrollTop() + chatBlock.outerHeight());
this.style.height = window.getComputedStyle(this).getPropertyValue('min-height');
this.style.height = this.scrollHeight + 0.3 + 'px';
if (!isFirefox) {
const newScrollTop = Math.round(chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom));
chatBlock.scrollTop(newScrollTop);
$(sendTextArea).on('input', () => {
if (sendTextArea.scrollHeight > sendTextArea.offsetHeight || sendTextArea.value === '') {
autoFitSendTextArea();
} else {
autoFitSendTextAreaDebounced();
}
saveUserInput();
saveUserInputDebounced();
});
restoreUserInput();
@ -891,23 +910,30 @@ export function initRossMods() {
processHotkeys(event.originalEvent);
});
const hotkeyTargets = {
'send_textarea': sendTextArea,
'dialogue_popup_input': document.querySelector('#dialogue_popup_input'),
};
//Additional hotkeys CTRL+ENTER and CTRL+UPARROW
/**
* @param {KeyboardEvent} event
*/
function processHotkeys(event) {
//Enter to send when send_textarea in focus
if ($(':focus').attr('id') === 'send_textarea') {
if (document.activeElement == hotkeyTargets['send_textarea']) {
const sendOnEnter = shouldSendOnEnter();
if (!event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {
event.preventDefault();
sendTextareaMessage();
return;
}
}
if ($(':focus').attr('id') === 'dialogue_popup_input' && !isMobile()) {
if (document.activeElement == hotkeyTargets['dialogue_popup_input'] && !isMobile()) {
if (!event.shiftKey && !event.ctrlKey && event.key == 'Enter') {
event.preventDefault();
$('#dialogue_popup_ok').trigger('click');
return;
}
}
//ctrl+shift+up to scroll to context line
@ -919,6 +945,7 @@ export function initRossMods() {
scrollTop: contextLine.offset().top - $('#chat').offset().top + $('#chat').scrollTop(),
}, 300);
} else { toastr.warning('Context line not found, send a message first!'); }
return;
}
//ctrl+shift+down to scroll to bottom of chat
if (event.shiftKey && event.ctrlKey && event.key == 'ArrowDown') {
@ -926,6 +953,7 @@ export function initRossMods() {
$('#chat').animate({
scrollTop: $('#chat').prop('scrollHeight'),
}, 300);
return;
}
// Alt+Enter or AltGr+Enter to Continue
@ -933,6 +961,7 @@ export function initRossMods() {
if (is_send_press == false) {
console.debug('Continuing with Alt+Enter');
$('#option_continue').trigger('click');
return;
}
}
@ -942,6 +971,7 @@ export function initRossMods() {
if (editMesDone.length > 0) {
console.debug('Accepting edits with Ctrl+Enter');
editMesDone.trigger('click');
return;
} else if (is_send_press == false) {
const skipConfirmKey = 'RegenerateWithCtrlEnter';
const skipConfirm = LoadLocalBool(skipConfirmKey);
@ -968,6 +998,7 @@ export function initRossMods() {
doRegenerate();
});
}
return;
} else {
console.debug('Ctrl+Enter ignored');
}
@ -976,7 +1007,7 @@ export function initRossMods() {
// Helper function to check if nanogallery2's lightbox is active
function isNanogallery2LightboxActive() {
// Check if the body has the 'nGY2On' class, adjust this based on actual behavior
return $('body').hasClass('nGY2_body_scrollbar');
return document.body.classList.contains('nGY2_body_scrollbar');
}
if (event.key == 'ArrowLeft') { //swipes left
@ -989,6 +1020,7 @@ export function initRossMods() {
!isInputElementInFocus()
) {
$('.swipe_left:last').click();
return;
}
}
if (event.key == 'ArrowRight') { //swipes right
@ -1001,13 +1033,14 @@ export function initRossMods() {
!isInputElementInFocus()
) {
$('.swipe_right:last').click();
return;
}
}
if (event.ctrlKey && event.key == 'ArrowUp') { //edits last USER message if chatbar is empty and focused
if (
$('#send_textarea').val() === '' &&
hotkeyTargets['send_textarea'].value === '' &&
chatbarInFocus === true &&
($('.swipe_right:last').css('display') === 'flex' || $('.last_mes').attr('is_system') === 'true') &&
$('#character_popup').css('display') === 'none' &&
@ -1018,6 +1051,7 @@ export function initRossMods() {
const editMes = lastIsUserMes.querySelector('.mes_block .mes_edit');
if (editMes !== null) {
$(editMes).trigger('click');
return;
}
}
}
@ -1025,7 +1059,7 @@ export function initRossMods() {
if (event.key == 'ArrowUp') { //edits last message if chatbar is empty and focused
console.log('got uparrow input');
if (
$('#send_textarea').val() === '' &&
hotkeyTargets['send_textarea'].value === '' &&
chatbarInFocus === true &&
//$('.swipe_right:last').css('display') === 'flex' &&
$('.last_mes .mes_buttons').is(':visible') &&
@ -1036,6 +1070,7 @@ export function initRossMods() {
const editMes = lastMes.querySelector('.mes_block .mes_edit');
if (editMes !== null) {
$(editMes).click();
return;
}
}
}

View File

@ -189,6 +189,8 @@ export async function getGroupChat(groupId) {
await printMessages();
} else {
sendSystemMessage(system_message_types.GROUP, '', { isSmallSys: true });
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1));
if (group && Array.isArray(group.members)) {
for (let member of group.members) {
const character = characters.find(x => x.avatar === member || x.name === member);
@ -199,7 +201,9 @@ export async function getGroupChat(groupId) {
const mes = await getFirstCharacterMessage(character);
chat.push(mes);
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
addOneMessage(mes);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1));
}
}
await saveGroupChat(groupId, false);

View File

@ -3507,11 +3507,11 @@ async function onModelChange() {
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (value === 'gemini-1.5-pro') {
} else if (value === 'gemini-1.5-pro-latest') {
$('#openai_max_context').attr('max', max_1mil);
} else if (value === 'gemini-pro') {
} else if (value === 'gemini-ultra' || value === 'gemini-1.0-pro-latest' || value === 'gemini-pro' || value === 'gemini-1.0-ultra-latest') {
$('#openai_max_context').attr('max', max_32k);
} else if (value === 'gemini-pro-vision') {
} else if (value === 'gemini-1.0-pro-vision-latest' || value === 'gemini-pro-vision') {
$('#openai_max_context').attr('max', max_16k);
} else {
$('#openai_max_context').attr('max', max_8k);
@ -3939,21 +3939,26 @@ export function isImageInliningSupported() {
return false;
}
const gpt4v = 'gpt-4-vision';
const geminiProV = 'gemini-pro-vision';
const claude = 'claude-3';
if (!oai_settings.image_inlining) {
return false;
}
// gultra just isn't being offered as multimodal, thanks google.
const visionSupportedModels = [
'gpt-4-vision',
'gemini-1.0-pro-vision-latest',
'gemini-1.5-pro-latest',
'gemini-pro-vision',
'claude-3'
];
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.OPENAI:
return oai_settings.openai_model.includes(gpt4v);
return visionSupportedModels.some(model => oai_settings.openai_model.includes(model));
case chat_completion_sources.MAKERSUITE:
return oai_settings.google_model.includes(geminiProV);
return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
case chat_completion_sources.CLAUDE:
return oai_settings.claude_model.includes(claude);
return visionSupportedModels.some(model => oai_settings.claude_model.includes(model));
case chat_completion_sources.OPENROUTER:
return !oai_settings.openrouter_force_instruct;
case chat_completion_sources.CUSTOM:

View File

@ -22,6 +22,7 @@ import {
name1,
reloadCurrentChat,
removeMacros,
retriggerFirstMessageOnEmptyChat,
saveChatConditional,
sendMessageAsUser,
sendSystemMessage,
@ -203,10 +204,10 @@ parser.addCommand('name', setNameCallback, ['persona'], '<span class="monospace"
parser.addCommand('sync', syncCallback, [], ' syncs the user persona in user-attributed messages in the current chat', true, true);
parser.addCommand('lock', bindCallback, ['bind'], ' locks/unlocks a persona (name and avatar) to the current chat', true, true);
parser.addCommand('bg', setBackgroundCallback, ['background'], '<span class="monospace">(filename)</span> sets a background according to filename, partial names allowed', false, true);
parser.addCommand('sendas', sendMessageAs, [], ' sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe": <tt>/sendas name="Chloe" Hello, guys!</tt>', true, true);
parser.addCommand('sys', sendNarratorMessage, ['nar'], '<span class="monospace">(text)</span> sends message as a system narrator', false, true);
parser.addCommand('sendas', sendMessageAs, [], '<span class="monospace">[name=CharName compact=true/false (text)] sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe": <tt>/sendas name="Chloe" Hello, guys!</tt>. If "compact" is set to true, the message is sent using a compact layout.', true, true);
parser.addCommand('sys', sendNarratorMessage, ['nar'], '<span class="monospace">[compact=true/false (text)]</span> sends message as a system narrator. If "compact" is set to true, the message is sent using a compact layout.', false, true);
parser.addCommand('sysname', setNarratorName, [], '<span class="monospace">(name)</span> sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.', true, true);
parser.addCommand('comment', sendCommentMessage, [], '<span class="monospace">(text)</span> adds a note/comment message not part of the chat', false, true);
parser.addCommand('comment', sendCommentMessage, [], '<span class="monospace">[compact=true/false (text)]</span> adds a note/comment message not part of the chat. If "compact" is set to true, the message is sent using a compact layout.', false, true);
parser.addCommand('single', setStoryModeCallback, ['story'], ' sets the message style to single document mode without names or avatars visible', true, true);
parser.addCommand('bubble', setBubbleModeCallback, ['bubbles'], ' sets the message style to bubble chat mode', true, true);
parser.addCommand('flat', setFlatModeCallback, ['default'], ' sets the message style to flat chat mode', true, true);
@ -215,7 +216,7 @@ parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace
parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">(prompt)</span> generates a system message using a specified prompt', true, true);
parser.addCommand('ask', askCharacter, [], '<span class="monospace">(prompt)</span> asks a specified character card a prompt', true, true);
parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '<span class="monospace">(name)</span> deletes all messages attributed to a specified name', true, true);
parser.addCommand('send', sendUserMessageCallback, [], '<span class="monospace">(text)</span> adds a user message to the chat log without triggering a generation', true, true);
parser.addCommand('send', sendUserMessageCallback, [], '<span class="monospace">[compact=true/false (text)]</span> adds a user message to the chat log without triggering a generation. If "compact" is set to true, the message is sent using a compact layout.', true, true);
parser.addCommand('trigger', triggerGenerationCallback, [], ' <span class="monospace">await=true/false</span> triggers a message generation. If in group, can trigger a message for the specified group member index or name. If <code>await=true</code> named argument passed, the command will await for the triggered generation before continuing.', true, true);
parser.addCommand('hide', hideMessageCallback, [], '<span class="monospace">(message index or range)</span> hides a chat message from the prompt', true, true);
parser.addCommand('unhide', unhideMessageCallback, [], '<span class="monospace">(message index or range)</span> unhides a message from the prompt', true, true);
@ -1175,9 +1176,10 @@ async function sendUserMessageCallback(args, text) {
}
text = text.trim();
const compact = isTrueBoolean(args?.compact);
const bias = extractMessageBias(text);
const insertAt = Number(resolveVariable(args?.at));
await sendMessageAsUser(text, bias, insertAt);
await sendMessageAsUser(text, bias, insertAt, compact);
return '';
}
@ -1340,12 +1342,14 @@ function setNameCallback(_, name) {
for (let persona of Object.values(power_user.personas)) {
if (persona.toLowerCase() === name.toLowerCase()) {
autoSelectPersona(name);
retriggerFirstMessageOnEmptyChat();
return;
}
}
// Otherwise, set just the name
setUserName(name); //this prevented quickReply usage
retriggerFirstMessageOnEmptyChat();
}
async function setNarratorName(_, text) {
@ -1388,6 +1392,7 @@ export async function sendMessageAs(args, text) {
// Messages that do nothing but set bias will be hidden from the context
const bias = extractMessageBias(mesText);
const isSystem = bias && !removeMacros(mesText).length;
const compact = isTrueBoolean(args?.compact);
const character = characters.find(x => x.name === name);
let force_avatar, original_avatar;
@ -1412,6 +1417,7 @@ export async function sendMessageAs(args, text) {
extra: {
bias: bias.trim().length ? bias : null,
gen_id: Date.now(),
isSmallSys: compact,
},
};
@ -1441,6 +1447,7 @@ export async function sendNarratorMessage(args, text) {
// Messages that do nothing but set bias will be hidden from the context
const bias = extractMessageBias(text);
const isSystem = bias && !removeMacros(text).length;
const compact = isTrueBoolean(args?.compact);
const message = {
name: name,
@ -1453,6 +1460,7 @@ export async function sendNarratorMessage(args, text) {
type: system_message_types.NARRATOR,
bias: bias.trim().length ? bias : null,
gen_id: Date.now(),
isSmallSys: compact,
},
};
@ -1517,6 +1525,7 @@ async function sendCommentMessage(args, text) {
return;
}
const compact = isTrueBoolean(args?.compact);
const message = {
name: COMMENT_NAME_DEFAULT,
is_user: false,
@ -1527,6 +1536,7 @@ async function sendCommentMessage(args, text) {
extra: {
type: system_message_types.COMMENT,
gen_id: Date.now(),
isSmallSys: compact,
},
};

View File

@ -8,6 +8,8 @@ import {
entitiesFilter,
printCharacters,
buildAvatarList,
eventSource,
event_types,
} from '../script.js';
// eslint-disable-next-line no-unused-vars
import { FILTER_TYPES, FILTER_STATES, isFilterState, FilterHelper } from './filters.js';
@ -133,7 +135,7 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity
}
if (subForEntity !== undefined && subForEntity.type === 'tag') {
entities = filterTagSubEntities(subForEntity.item, entities, { filterHidden : filterHidden });
entities = filterTagSubEntities(subForEntity.item, entities, { filterHidden: filterHidden });
}
return entities;
@ -1177,7 +1179,7 @@ function onClearAllFiltersClick() {
// We have to manually go through the elements and unfilter by clicking...
// Thankfully nearly all filter controls are three-state-toggles
const filterTags = $('.rm_tag_controls .rm_tag_filter').find('.tag');
for(const tag of filterTags) {
for (const tag of filterTags) {
const toggleState = $(tag).attr('data-toggle-state');
if (toggleState !== undefined && !isFilterState(toggleState ?? FILTER_STATES.UNDEFINED, FILTER_STATES.UNDEFINED)) {
toggleTagThreeState($(tag), { stateOverride: FILTER_STATES.UNDEFINED, simulateClick: true });
@ -1188,7 +1190,17 @@ function onClearAllFiltersClick() {
$('#character_search_bar').val('').trigger('input');
}
jQuery(() => {
/**
* Copy tags from one character to another.
* @param {{oldAvatar: string, newAvatar: string}} data Event data
*/
function copyTags(data) {
const prevTagMap = tag_map[data.oldAvatar] || [];
const newTagMap = tag_map[data.newAvatar] || [];
tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));
}
export function initTags() {
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
@ -1205,5 +1217,5 @@ jQuery(() => {
$(document).on('click', '.tag_view_create', onTagCreateClick);
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
});
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
}

View File

@ -33,7 +33,7 @@
<li><tt>&lcub;&lcub;time_UTC±#&rcub;&rcub;</tt> the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2</li>
<li><tt>&lcub;&lcub;idle_duration&rcub;&rcub;</tt> the time since the last user message was sent</li>
<li><tt>&lcub;&lcub;bias "text here"&rcub;&rcub;</tt> sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li>
<li><tt>&lcub;&lcub;roll:(formula)&rcub;&rcub;</tt> rolls a dice. (ex: <tt>>&lcub;&lcub;roll:1d6&rcub;&rcub</tt> will roll a 6-sided dice and return a number between 1 and 6)</li>
<li><tt>&lcub;&lcub;roll:(formula)&rcub;&rcub;</tt> rolls a dice. (ex: <tt>>&lcub;&lcub;roll:1d6&rcub;&rcub;</tt> will roll a 6-sided dice and return a number between 1 and 6)</li>
<li><tt>&lcub;&lcub;random:(args)&rcub;&rcub;</tt> returns a random item from the list. (ex: <tt>&lcub;&lcub;random:1,2,3,4&rcub;&rcub;</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li>
<li><tt>&lcub;&lcub;random::(arg1)::(arg2)&rcub;&rcub;</tt> alternative syntax for random that allows to use commas in the list items.</li>
<li><tt>&lcub;&lcub;banned "text here"&rcub;&rcub;</tt> dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li>

View File

@ -972,7 +972,6 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'typical_p': settings.typical_p,
'typical': settings.typical_p,
'sampler_seed': settings.seed,
'seed': settings.seed,
'min_p': settings.min_p,
'repetition_penalty': settings.rep_pen,
'frequency_penalty': settings.freq_pen,
@ -1024,6 +1023,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'penalty_alpha': settings.type === OOBA ? settings.penalty_alpha : undefined,
'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,
'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
'seed': settings.seed,
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
'grammar_string': settings.grammar_string,

View File

@ -383,6 +383,15 @@ small {
text-align: center;
}
.mes.smallSysMes .mes_text p:last-child {
margin: 0;
}
.mes.smallSysMes .swipe_right,
.mes.smallSysMes .swipe_left {
display: none !important;
}
.mes.smallSysMes .mes_text {
padding: 0 !important;
text-align: center;
@ -1408,7 +1417,7 @@ input[type="file"] {
}
.extension_token_counter {
font-size: calc(var(--mainFontSize) * 0.9);
font-size: calc(var(--mainFontSize) * 0.875);
width: 100%;
text-align: right;
margin-bottom: 5px;

View File

@ -1008,7 +1008,7 @@ router.post('/duplicate', jsonParser, async function (request, response) {
fs.copyFileSync(filename, newFilename);
console.log(`${filename} was copied to ${newFilename}`);
response.sendStatus(200);
response.send({ path: path.parse(newFilename).base });
}
catch (error) {
console.error(error);

View File

@ -192,53 +192,73 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
function convertGooglePrompt(messages, model) {
// This is a 1x1 transparent PNG
const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const visionSupportedModels = [
'gemini-1.0-pro-vision-latest',
'gemini-1.5-pro-latest',
'gemini-pro-vision',
];
const isMultimodal = visionSupportedModels.includes(model);
let hasImage = false;
const contents = [];
let lastRole = '';
let currentText = '';
messages.forEach((message, index) => {
// fix the roles
if (message.role === 'system') {
message.role = 'user';
} else if (message.role === 'assistant') {
message.role = 'model';
}
const isMultimodal = model === 'gemini-pro-vision';
if (isMultimodal) {
const combinedText = messages.map((message) => {
const role = message.role === 'assistant' ? 'MODEL: ' : 'USER: ';
return role + message.content;
}).join('\n\n').trim();
const imageEntry = messages.find((message) => message.content?.[1]?.image_url);
const imageData = imageEntry?.content?.[1]?.image_url?.data ?? PNG_PIXEL;
contents.push({
parts: [
{ text: combinedText },
{
inlineData: {
mimeType: 'image/png',
data: imageData,
},
},
],
role: 'user',
});
} else {
messages.forEach((message, index) => {
const role = message.role === 'assistant' ? 'model' : 'user';
if (lastRole === role) {
currentText += '\n\n' + message.content;
// similar story as claude
if (message.name) {
if (Array.isArray(message.content)) {
message.content[0].text = `${message.name}: ${message.content[0].text}`;
} else {
if (currentText !== '') {
contents.push({
parts: [{ text: currentText.trim() }],
role: lastRole,
message.content = `${message.name}: ${message.content}`;
}
delete message.name;
}
//create the prompt parts
const parts = [];
if (typeof message.content === 'string') {
parts.push({ text: message.content });
} else if (Array.isArray(message.content)) {
message.content.forEach((part) => {
if (part.type === 'text') {
parts.push({ text: part.text });
} else if (part.type === 'image_url' && isMultimodal) {
parts.push({
inlineData: {
mimeType: 'image/png',
data: part.image_url.url,
},
});
hasImage = true;
}
currentText = message.content;
lastRole = role;
}
if (index === messages.length - 1) {
contents.push({
parts: [{ text: currentText.trim() }],
role: lastRole,
});
}
});
}
// merge consecutive messages with the same role
if (index > 0 && message.role === contents[contents.length - 1].role) {
contents[contents.length - 1].parts[0].text += '\n\n' + parts[0].text;
} else {
contents.push({
role: message.role,
parts: parts,
});
}
});
// pro 1.5 doesn't require a dummy image to be attached, other vision models do
if (isMultimodal && model !== 'gemini-1.5-pro-latest' && !hasImage) {
contents[0].parts.push({
inlineData: {
mimeType: 'image/png',
data: PNG_PIXEL,
},
});
}