From d1a654f41f3021b117321bcdbef046daa7f85084 Mon Sep 17 00:00:00 2001 From: AlpinDale Date: Sun, 1 Dec 2024 02:37:11 +0000 Subject: [PATCH 001/222] add to index.html --- public/index.html | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/public/index.html b/public/index.html index c00ab84f7..e28179009 100644 --- a/public/index.html +++ b/public/index.html @@ -1718,6 +1718,73 @@ Load default order +
+
+
+ Sampler Order +
+
+ Aphrodite only. Samplers will be applied in a top-down order. Use with caution. +
+
+
+ Top P & Top K + 0 +
+
+ Top A + 1 +
+
+ Min P + 2 +
+
+ Tail Free Sampling + 3 +
+
+ Typical P + 4 +
+
+ Dynatemp & Temperature + 5 +
+
+ Penalties + 6 +
+
+ DRY + 7 +
+
+ No Repeat Ngram + 8 +
+
+ Top Nsigma + 9 +
+
+ Eta Cutoff + 10 +
+
+ Epsilon Cutoff + 11 +
+
+ Cubic & Quadratic Sampling + 12 +
+
+ XTC + 13 +
+
+

From af582f43a65a021f53b908f0a507d55dce8c1e34 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 2 Dec 2024 02:52:00 +0200 Subject: [PATCH 002/222] [wip] Persona lorebook --- public/index.html | 22 +------ public/scripts/personas.js | 61 ++++++++++++++++++- public/scripts/power-user.js | 3 +- public/scripts/templates/chatLorebook.html | 18 ++++++ public/scripts/templates/personaLorebook.html | 18 ++++++ public/scripts/world-info.js | 55 ++++++++++++++--- 6 files changed, 146 insertions(+), 31 deletions(-) create mode 100644 public/scripts/templates/chatLorebook.html create mode 100644 public/scripts/templates/personaLorebook.html diff --git a/public/index.html b/public/index.html index 8e5b8b5fd..561b15d55 100644 --- a/public/index.html +++ b/public/index.html @@ -4813,6 +4813,8 @@

+

Persona Description

@@ -5539,26 +5541,6 @@
-
-
-
-

- Chat Lorebook for -

-
-
- - A selected World Info will be bound to this chat. When generating an AI reply, - it will be combined with the entries from global and character lorebooks. - -
-
- -
-
-
diff --git a/public/scripts/personas.js b/public/scripts/personas.js index 01747a528..57f6026c7 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -21,8 +21,10 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor import { debounce_timeout } from './constants.js'; import { FILTER_TYPES, FilterHelper } from './filters.js'; import { selected_group } from './group-chats.js'; -import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js'; +import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { t } from './i18n.js'; +import { world_names } from './world-info.js'; +import { renderTemplateAsync } from './templates.js'; let savePersonasPage = 0; const GRID_STORAGE_KEY = 'Personas_GridView'; @@ -375,6 +377,7 @@ export function initPersona(avatarId, personaName, personaDescription) { position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE, + lorebook: '', }; saveSettingsDebounced(); @@ -418,6 +421,7 @@ export async function convertCharacterToPersona(characterId = null) { position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE, + lorebook: '', }; // If the user is currently using this persona, update the description @@ -461,6 +465,7 @@ export function setPersonaDescription() { .val(power_user.persona_description_role) .find(`option[value="${power_user.persona_description_role}"]`) .prop('selected', String(true)); + $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); countPersonaDescriptionTokens(); } @@ -490,6 +495,7 @@ async function updatePersonaNameIfExists(avatarId, newName) { position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE, + lorebook: '', }; console.log(`Created persona name for ${avatarId} as ${newName}`); } @@ -535,6 +541,7 @@ async function bindUserNameToPersona(e) { position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT, depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH, role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE, + lorebook: isCurrentPersona ? power_user.persona_description_lorebook : '', }; } @@ -579,12 +586,20 @@ function selectCurrentPersona() { power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT; power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH; power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE; + power_user.persona_description_lorebook = descriptor.lorebook ?? ''; } else { power_user.persona_description = ''; power_user.persona_description_position = persona_description_positions.IN_PROMPT; power_user.persona_description_depth = DEFAULT_DEPTH; power_user.persona_description_role = DEFAULT_ROLE; - power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE }; + power_user.persona_description_lorebook = ''; + power_user.persona_descriptions[user_avatar] = { + description: '', + position: persona_description_positions.IN_PROMPT, + depth: DEFAULT_DEPTH, + role: DEFAULT_ROLE, + lorebook: '', + }; } setPersonaDescription(); @@ -652,6 +667,7 @@ async function lockPersona() { position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE, + lorebook: '', }; } @@ -731,6 +747,7 @@ function onPersonaDescriptionInput() { position: Number($('#persona_description_position').find(':selected').val()), depth: Number($('#persona_depth_value').val()), role: Number($('#persona_depth_role').find(':selected').val()), + lorebook: '', }; power_user.persona_descriptions[user_avatar] = object; } @@ -766,6 +783,43 @@ function onPersonaDescriptionDepthRoleInput() { saveSettingsDebounced(); } +async function onPersonaLoreButtonClick() { + const personaName = power_user.personas[user_avatar]; + const selectedLorebook = power_user.persona_description_lorebook; + + if (!personaName) { + toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona name not set`); + return; + } + + const template = $(await renderTemplateAsync('personaLorebook')); + + const worldSelect = template.find('select'); + template.find('.persona_name').text(personaName); + + for (const worldName of world_names) { + const option = document.createElement('option'); + option.value = worldName; + option.innerText = worldName; + option.selected = selectedLorebook === worldName; + worldSelect.append(option); + } + + worldSelect.on('change', function () { + power_user.persona_description_lorebook = String($(this).val()); + + if (power_user.personas[user_avatar]) { + const object = getOrCreatePersonaDescriptor(); + object.lorebook = power_user.persona_description_lorebook; + } + + $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); + saveSettingsDebounced(); + }); + + await callGenericPopup(template, POPUP_TYPE.TEXT); +} + function onPersonaDescriptionPositionInput() { power_user.persona_description_position = Number( $('#persona_description_position').find(':selected').val(), @@ -789,6 +843,7 @@ function getOrCreatePersonaDescriptor() { position: power_user.persona_description_position, depth: power_user.persona_description_depth, role: power_user.persona_description_role, + lorebook: power_user.persona_description_lorebook, }; power_user.persona_descriptions[user_avatar] = object; } @@ -1038,6 +1093,7 @@ async function duplicatePersona(avatarId) { position: descriptor?.position ?? persona_description_positions.IN_PROMPT, depth: descriptor?.depth ?? DEFAULT_DEPTH, role: descriptor?.role ?? DEFAULT_ROLE, + lorebook: descriptor?.lorebook ?? '', }; await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId); @@ -1055,6 +1111,7 @@ export function initPersonas() { $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); $('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput); $('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput); + $('#persona_lore_button').on('click', onPersonaLoreButtonClick); $('#personas_backup').on('click', onBackupPersonas); $('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click')); $('#personas_restore_input').on('change', onPersonasRestoreInput); diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 504d53b13..31e486110 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -262,6 +262,7 @@ let power_user = { persona_description_position: persona_description_positions.IN_PROMPT, persona_description_role: 0, persona_description_depth: 2, + persona_description_lorebook: '', persona_show_notifications: true, persona_sort_order: 'asc', @@ -1925,7 +1926,7 @@ export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null) const mappedData = data.map(x => ({ key: x, name: power_user.personas[x] ?? '', - description: power_user.persona_descriptions[x]?.description ?? '' + description: power_user.persona_descriptions[x]?.description ?? '', })); const keys = [ diff --git a/public/scripts/templates/chatLorebook.html b/public/scripts/templates/chatLorebook.html new file mode 100644 index 000000000..c23c466e4 --- /dev/null +++ b/public/scripts/templates/chatLorebook.html @@ -0,0 +1,18 @@ +
+
+

+ Chat Lorebook for +

+
+
+ + A selected World Info will be bound to this chat. When generating an AI reply, + it will be combined with the entries from global and character lorebooks. + +
+
+ +
+
diff --git a/public/scripts/templates/personaLorebook.html b/public/scripts/templates/personaLorebook.html new file mode 100644 index 000000000..5a8a2b928 --- /dev/null +++ b/public/scripts/templates/personaLorebook.html @@ -0,0 +1,18 @@ +
+
+

+ Persona Lorebook for +

+
+
+ + A selected World Info will be bound to this persona. When generating an AI reply, + it will be combined with the entries from global, character and chat lorebooks. + +
+
+ +
+
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 0f4351933..77ab6e249 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -3548,6 +3548,11 @@ async function getCharacterLore() { continue; } + if (power_user.persona_description_lorebook === worldName) { + console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in persona lore! Skipping...`); + continue; + } + const data = await loadWorldInfo(worldName); const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : []; entries = entries.concat(newEntries); @@ -3598,11 +3603,45 @@ async function getChatLore() { return entries; } +async function getPersonaLore() { + const chatWorld = chat_metadata[METADATA_KEY]; + const personaWorld = power_user.persona_description_lorebook; + + if (!personaWorld) { + return []; + } + + if (chatWorld === personaWorld) { + console.debug(`[WI] Persona world ${personaWorld} is already activated in chat world! Skipping...`); + return []; + } + + if (selected_world_info.includes(personaWorld)) { + console.debug(`[WI] Persona world ${personaWorld} is already activated in global world info! Skipping...`); + return []; + } + + const data = await loadWorldInfo(personaWorld); + const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: personaWorld, ...rest })) : []; + + console.debug(`[WI] Persona lore has ${entries.length} entries`, [personaWorld]); + + return entries; +} + export async function getSortedEntries() { try { - const globalLore = await getGlobalLore(); - const characterLore = await getCharacterLore(); - const chatLore = await getChatLore(); + const [ + globalLore, + characterLore, + chatLore, + personaLore, + ] = await Promise.all([ + getGlobalLore(), + getCharacterLore(), + getChatLore(), + getPersonaLore(), + ]); let entries; @@ -3622,8 +3661,8 @@ export async function getSortedEntries() { break; } - // Chat lore always goes first - entries = [...chatLore.sort(sortFn), ...entries]; + // Chat lore always goes first, then persona lore, then the rest + entries = [...chatLore.sort(sortFn), ...personaLore.sort(sortFn), ...entries]; // Calculate hash and parse decorators. Split maps to preserve old hashes. entries = entries.map((entry) => { @@ -4816,9 +4855,9 @@ export async function importWorldInfo(file) { }); } -export function assignLorebookToChat() { +export async function assignLorebookToChat() { const selectedName = chat_metadata[METADATA_KEY]; - const template = $('#chat_world_template .chat_world').clone(); + const template = $(await renderTemplateAsync('chatLorebook')); const worldSelect = template.find('select'); const chatName = template.find('.chat_name'); @@ -4846,7 +4885,7 @@ export function assignLorebookToChat() { saveMetadata(); }); - callPopup(template, 'text'); + callGenericPopup(template, POPUP_TYPE.TEXT); } jQuery(() => { From 80c8e83f09e9d2a743cb5e5e10d3285f1a93a1ef Mon Sep 17 00:00:00 2001 From: AlpinDale Date: Tue, 3 Dec 2024 01:46:51 +0000 Subject: [PATCH 003/222] use strings instead of IDs --- public/index.html | 96 +++++++++--------------------- public/scripts/samplerSelect.js | 13 ++++ public/scripts/textgen-settings.js | 34 +++++++++++ 3 files changed, 76 insertions(+), 67 deletions(-) diff --git a/public/index.html b/public/index.html index 5d1e54aec..c85ffc237 100644 --- a/public/index.html +++ b/public/index.html @@ -1718,73 +1718,6 @@ Load default order
-
-
-
- Sampler Order -
-
- Aphrodite only. Samplers will be applied in a top-down order. Use with caution. -
-
-
- Top P & Top K - 0 -
-
- Top A - 1 -
-
- Min P - 2 -
-
- Tail Free Sampling - 3 -
-
- Typical P - 4 -
-
- Dynatemp & Temperature - 5 -
-
- Penalties - 6 -
-
- DRY - 7 -
-
- No Repeat Ngram - 8 -
-
- Top Nsigma - 9 -
-
- Eta Cutoff - 10 -
-
- Epsilon Cutoff - 11 -
-
- Cubic & Quadratic Sampling - 12 -
-
- XTC - 13 -
-
-

@@ -1841,6 +1774,35 @@ Load default order

+
+
+

+ Sampler Order +
+

+
+ Aphrodite only. Determines the order of samplers. +
+
+
DRY
+
Penalties
+
No Repeat Ngram
+
Dynatemp & Temperature
+
Top Nsigma
+
Top P & Top K
+
Top A
+
Min P
+
Tail-Free Sampling
+
Eta Cutoff
+
Epsilon Cutoff
+
Typical P
+
Cubic and Quadratic Sampling
+
XTC
+
+ +
diff --git a/public/scripts/samplerSelect.js b/public/scripts/samplerSelect.js index c949b8e91..35f5629ba 100644 --- a/public/scripts/samplerSelect.js +++ b/public/scripts/samplerSelect.js @@ -129,6 +129,10 @@ function setSamplerListListeners() { relatedDOMElement = $('#sampler_priority_block_ooba'); } + if (samplerName === 'samplers_priorities') { //this is for aphrodite's sampler priority + relatedDOMElement = $('#sampler_priority_block_aphrodite'); + } + if (samplerName === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block? relatedDOMElement = $('#contrastiveSearchBlock'); } @@ -237,6 +241,11 @@ async function listSamplers(main_api, arrayOnly = false) { displayname = 'Ooba Sampler Priority Block'; } + if (sampler === 'samplers_priorities') { //this is for aphrodite's sampler priority + targetDOMelement = $('#sampler_priority_block_aphrodite'); + displayname = 'Aphrodite Sampler Priority Block'; + } + if (sampler === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block? targetDOMelement = $('#contrastiveSearchBlock'); displayname = 'Contrast Search Block'; @@ -373,6 +382,10 @@ export async function validateDisabledSamplers(redraw = false) { relatedDOMElement = $('#sampler_priority_block_ooba'); } + if (sampler === 'samplers_priorities') { //this is for aphrodite's sampler priority + relatedDOMElement = $('#sampler_priority_block_aphrodite'); + } + if (sampler === 'dry_multiplier') { relatedDOMElement = $('#dryBlock'); targetDisplayType = 'block'; diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 0dfae364f..077bf6911 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -89,6 +89,22 @@ const OOBA_DEFAULT_ORDER = [ 'encoder_repetition_penalty', 'no_repeat_ngram', ]; +const APHRODITE_DEFAULT_ORDER = [ + 'dry', + 'penalties', + 'no_repeat_ngram', + 'temperature', + 'top_nsigma', + 'top_p_top_k', + 'top_a', + 'min_p', + 'tfs', + 'eta_cutoff', + 'epsilon_cutoff', + 'typical_p', + 'quadratic', + 'xtc' +]; const BIAS_KEY = '#textgenerationwebui_api-settings'; // Maybe let it be configurable in the future? @@ -170,6 +186,7 @@ const settings = { banned_tokens: '', sampler_priority: OOBA_DEFAULT_ORDER, samplers: LLAMACPP_DEFAULT_ORDER, + samplers_priorties: APHRODITE_DEFAULT_ORDER, ignore_eos_token: false, spaces_between_special_tokens: true, speculative_ngram: false, @@ -259,6 +276,7 @@ export const setting_names = [ 'sampler_order', 'sampler_priority', 'samplers', + 'samplers_priorities', 'n', 'logit_bias', 'custom_model', @@ -627,6 +645,13 @@ jQuery(function () { saveSettingsDebounced(); }); + $('#aphrodite_default_order').on('click', function () { + sortOobaItemsByOrder(APHRODITE_DEFAULT_ORDER); + settings.samplers_priorties = APHRODITE_DEFAULT_ORDER; + console.log('Default samplers order loaded:', settings.samplers_priorties); + saveSettingsDebounced(); + }); + $('#textgen_type').on('change', function () { const type = String($(this).val()); settings.type = type; @@ -835,6 +860,14 @@ function setSettingByName(setting, value, trigger) { return; } + if ('samplers_priority' === setting) { + value = Array.isArray(value) ? value : APHRODITE_DEFAULT_ORDER; + insertMissingArrayItems(APHRODITE_DEFAULT_ORDER, value); + sortOobaItemsByOrder(value); + settings.samplers_priorties = value; + return; + } + if ('samplers' === setting) { value = Array.isArray(value) ? value : LLAMACPP_DEFAULT_ORDER; insertMissingArrayItems(LLAMACPP_DEFAULT_ORDER, value); @@ -1259,6 +1292,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'nsigma': settings.nsigma, 'custom_token_bans': toIntArray(banned_tokens), 'no_repeat_ngram_size': settings.no_repeat_ngram_size, + 'sampler_priority': settings.samplers_priorties, }; if (settings.type === OPENROUTER) { From 9960db0ae2ef5cae6d75486294b61ef52d85a700 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:48:10 +0200 Subject: [PATCH 004/222] Redesign extension manager --- public/css/extensions-panel.css | 38 +++++++- public/scripts/extensions.js | 153 ++++++++++++++++++++------------ 2 files changed, 131 insertions(+), 60 deletions(-) diff --git a/public/css/extensions-panel.css b/public/css/extensions-panel.css index da005bd70..6e0da7b3f 100644 --- a/public/css/extensions-panel.css +++ b/public/css/extensions-panel.css @@ -65,7 +65,7 @@ label[for="extensions_autoconnect"] { } .extensions_info .extension_enabled { - color: green; + font-weight: bold; } .extensions_info .extension_disabled { @@ -76,6 +76,42 @@ label[for="extensions_autoconnect"] { color: gray; } +.extensions_info .extension_modules { + font-size: 0.8em; + font-weight: normal; +} + +.extensions_info .extension_block { + display: flex; + flex-wrap: wrap; + padding: 10px; + margin-bottom: 5px; + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 10px; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.extensions_info .extension_name { + font-size: 1.05em; +} + +.extensions_info .extension_version { + opacity: 0.8; + font-size: 0.8em; + font-weight: normal; + margin-left: 5px; +} + +.extensions_info .extension_block a { + color: var(--SmartThemeBodyColor); +} + +.extensions_info .extension_name.update_available { + color: limegreen; +} + input.extension_missing[type="checkbox"] { opacity: 0.5; } diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index e9a45f540..325f69451 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -515,64 +515,64 @@ function addExtensionScript(name, manifest) { * @param {boolean} isDisabled - Whether the extension is disabled or not. * @param {boolean} isExternal - Whether the extension is external or not. * @param {string} checkboxClass - The class for the checkbox HTML element. - * @return {Promise} - The HTML string that represents the extension. + * @return {string} - The HTML string that represents the extension. */ -async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { +function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { const displayName = manifest.display_name; let displayVersion = manifest.version ? ` v${manifest.version}` : ''; - let isUpToDate = true; - let updateButton = ''; + const externalId = name.replace('third-party', ''); let originHtml = ''; if (isExternal) { - let data = await getExtensionVersion(name.replace('third-party', '')); - let branch = data.currentBranchName; - let commitHash = data.currentCommitHash; - let origin = data.remoteUrl; - isUpToDate = data.isUpToDate; - displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; - updateButton = isUpToDate ? - `` : - ``; - originHtml = ``; + originHtml = ''; } let toggleElement = isActive || isDisabled ? `` : ``; - let deleteButton = isExternal ? `` : ''; - - // if external, wrap the name in a link to the repo - - let extensionHtml = `
-

- ${updateButton} - ${deleteButton} - ${originHtml} - - ${DOMPurify.sanitize(displayName)}${displayVersion} - - ${isExternal ? '' : ''} - - ${toggleElement} -

`; + let deleteButton = isExternal ? `` : ''; + let updateButton = isExternal ? `` : ''; + let modulesInfo = ''; if (isActive && Array.isArray(manifest.optional)) { const optional = new Set(manifest.optional); modules.forEach(x => optional.delete(x)); if (optional.size > 0) { const optionalString = DOMPurify.sanitize([...optional].join(', ')); - extensionHtml += `

Optional modules: ${optionalString}

`; + modulesInfo = `
Optional modules: ${optionalString}
`; } } else if (!isDisabled) { // Neither active nor disabled const requirements = new Set(manifest.requires); modules.forEach(x => requirements.delete(x)); if (requirements.size > 0) { const requirementsString = DOMPurify.sanitize([...requirements].join(', ')); - extensionHtml += `

Missing modules: ${requirementsString}

`; + modulesInfo = `
Missing modules: ${requirementsString}
`; } } + // if external, wrap the name in a link to the repo + + let extensionHtml = ` +
+
+ ${toggleElement} +
+
+ ${originHtml} + + ${DOMPurify.sanitize(displayName)} + ${displayVersion} + ${modulesInfo} + + ${isExternal ? '' : ''} +
+ +
+ ${updateButton} + ${deleteButton} +
+
`; + return extensionHtml; } @@ -580,9 +580,9 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt * Gets extension data and generates the corresponding HTML for displaying the extension. * * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest. - * @return {Promise} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. + * @return {object} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. */ -async function getExtensionData(extension) { +function getExtensionData(extension) { const name = extension[0]; const manifest = extension[1]; const isActive = activeExtensions.has(name); @@ -591,7 +591,7 @@ async function getExtensionData(extension) { const checkboxClass = isDisabled ? 'checkbox_disabled' : ''; - const extensionHtml = await generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass); + const extensionHtml = generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass); return { isExternal, extensionHtml }; } @@ -616,40 +616,28 @@ function getModuleInformation() { async function showExtensionsDetails() { let popupPromise; try { - const htmlDefault = $('

Built-in Extensions:

'); - const htmlExternal = $('

Installed Extensions:

').addClass('opacity50p'); - const htmlLoading = $(`

+ const htmlDefault = $('

Built-in Extensions:

'); + const htmlExternal = $('

Installed Extensions:

'); + const htmlLoading = $(`
Loading third-party extensions... Please wait... -

`); + `); - /** @type {Promise[]} */ - const promises = []; - const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); + htmlExternal.append(htmlLoading); - for (const extension of extensions) { - promises.push(getExtensionData(extension)); - } + const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order).map(getExtensionData); - promises.forEach(promise => { - promise.then(value => { - const { isExternal, extensionHtml } = value; - const container = isExternal ? htmlExternal : htmlDefault; - container.append(extensionHtml); - }); - }); - - Promise.allSettled(promises).then(() => { - htmlLoading.remove(); - htmlExternal.removeClass('opacity50p'); + extensions.forEach(value => { + const { isExternal, extensionHtml } = value; + const container = isExternal ? htmlExternal : htmlDefault; + container.append(extensionHtml); }); const html = $('
') .addClass('extensions_info') - .append(getModuleInformation()) .append(htmlDefault) - .append(htmlLoading) - .append(htmlExternal); + .append(htmlExternal) + .append(getModuleInformation()); /** @type {import('./popup.js').CustomPopupButton} */ const updateAllButton = { @@ -692,6 +680,7 @@ async function showExtensionsDetails() { }, }); popupPromise = popup.show(); + checkForUpdatesManual().finally(() => htmlLoading.remove()); } catch (error) { toastr.error('Error loading extensions. See browser console for details.'); console.error(error); @@ -873,6 +862,52 @@ export function doDailyExtensionUpdatesCheck() { }, 1); } +async function checkForUpdatesManual() { + const promises = []; + for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party'))) { + const externalId = id.replace('third-party', ''); + const promise = new Promise(async (resolve, reject) => { + try { + const data = await getExtensionVersion(externalId); + const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`); + if (extensionBlock) { + if (data.isUpToDate === false) { + const buttonElement = extensionBlock.querySelector('.btn_update'); + if (buttonElement) { + buttonElement.classList.remove('displayNone'); + } + const nameElement = extensionBlock.querySelector('.extension_name'); + if (nameElement) { + nameElement.classList.add('update_available'); + } + } + let branch = data.currentBranchName; + let commitHash = data.currentCommitHash; + let origin = data.remoteUrl; + + const originLink = extensionBlock.querySelector('a'); + if (originLink) { + originLink.href = origin; + originLink.target = '_blank'; + originLink.rel = 'noopener noreferrer'; + } + + const versionElement = extensionBlock.querySelector('.extension_version'); + if (versionElement) { + versionElement.textContent += ` (${branch}-${commitHash.substring(0, 7)})`; + } + } + resolve(); + } catch (error) { + console.error('Error checking for extension updates', error); + reject(); + } + }); + promises.push(promise); + } + return Promise.allSettled(promises); +} + /** * Checks if there are updates available for 3rd-party extensions. * @param {boolean} force Skip nag check From cb21162558c5299f113c75eb124d50155c966331 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:56:19 +0200 Subject: [PATCH 005/222] No dangling promise --- public/scripts/world-info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 77ab6e249..d1265ba7c 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -4885,7 +4885,7 @@ export async function assignLorebookToChat() { saveMetadata(); }); - callGenericPopup(template, POPUP_TYPE.TEXT); + return callGenericPopup(template, POPUP_TYPE.TEXT); } jQuery(() => { From a702dab68b4a23ad7acfa42394732cac1bb51764 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:53:10 +0000 Subject: [PATCH 006/222] Alt+Click to open lorebook --- public/index.html | 4 ++-- public/scripts/personas.js | 13 +++++++++++-- public/scripts/world-info.js | 32 ++++++++++++++++++++++++++------ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/public/index.html b/public/index.html index 986c95638..648286958 100644 --- a/public/index.html +++ b/public/index.html @@ -4813,7 +4813,7 @@ -
@@ -4931,7 +4931,7 @@ - + diff --git a/public/scripts/personas.js b/public/scripts/personas.js index 57f6026c7..930e87a4e 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -23,7 +23,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js'; import { selected_group } from './group-chats.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { t } from './i18n.js'; -import { world_names } from './world-info.js'; +import { openWorldInfoEditor, world_names } from './world-info.js'; import { renderTemplateAsync } from './templates.js'; let savePersonasPage = 0; @@ -783,7 +783,11 @@ function onPersonaDescriptionDepthRoleInput() { saveSettingsDebounced(); } -async function onPersonaLoreButtonClick() { +/** + * Opens a popup to set the lorebook for the current persona. + * @param {PointerEvent} event Click event + */ +async function onPersonaLoreButtonClick(event) { const personaName = power_user.personas[user_avatar]; const selectedLorebook = power_user.persona_description_lorebook; @@ -792,6 +796,11 @@ async function onPersonaLoreButtonClick() { return; } + if (event.altKey && selectedLorebook) { + openWorldInfoEditor(selectedLorebook); + return; + } + const template = $(await renderTemplateAsync('personaLorebook')); const worldSelect = template.find('select'); diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index d1265ba7c..51426a260 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -4855,8 +4855,32 @@ export async function importWorldInfo(file) { }); } -export async function assignLorebookToChat() { +/** + * Forces the world info editor to open on a specific world. + * @param {string} worldName The name of the world to open + */ +export function openWorldInfoEditor(worldName) { + console.log(`Opening lorebook for ${worldName}`); + if (!$('#WorldInfo').is(':visible')) { + $('#WIDrawerIcon').trigger('click'); + } + const index = world_names.indexOf(worldName); + $('#world_editor_select').val(index).trigger('change'); +} + +/** + * Assigns a lorebook to the current chat. + * @param {PointerEvent} event Pointer event + * @returns {Promise} + */ +export async function assignLorebookToChat(event) { const selectedName = chat_metadata[METADATA_KEY]; + + if (selectedName && event.altKey) { + openWorldInfoEditor(selectedName); + return; + } + const template = $(await renderTemplateAsync('chatLorebook')); const worldSelect = template.find('select'); @@ -5036,11 +5060,7 @@ jQuery(() => { const worldName = characters[chid]?.data?.extensions?.world; const hasEmbed = checkEmbeddedWorld(chid); if (worldName && world_names.includes(worldName) && !event.shiftKey) { - if (!$('#WorldInfo').is(':visible')) { - $('#WIDrawerIcon').trigger('click'); - } - const index = world_names.indexOf(worldName); - $('#world_editor_select').val(index).trigger('change'); + openWorldInfoEditor(worldName); } else if (hasEmbed && !event.shiftKey) { await importEmbeddedWorldInfo(); saveCharacterDebounced(); From d6f34f7b2c812462030be6af198b4b982808d895 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:53:02 +0200 Subject: [PATCH 007/222] Add prompt injection filters --- public/script.js | 108 ++++++++++++++++++++++++----------- public/scripts/openai.js | 16 ++++-- public/scripts/world-info.js | 2 +- 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/public/script.js b/public/script.js index 5ebc7592e..5648d90b6 100644 --- a/public/script.js +++ b/public/script.js @@ -2875,23 +2875,54 @@ function addPersonaDescriptionExtensionPrompt() { } } -function getAllExtensionPrompts() { - const value = Object - .values(extension_prompts) - .filter(x => x.value) - .map(x => x.value.trim()) - .join('\n'); +/** + * Returns all extension prompts combined. + * @returns {Promise} Combined extension prompts + */ +async function getAllExtensionPrompts() { + const values = []; - return value.length ? substituteParams(value) : ''; + for (const prompt of Object.values(extension_prompts)) { + const value = prompt?.value?.trim(); + + if (!value) { + continue; + } + + const hasFilter = typeof prompt.filter === 'function'; + if (hasFilter && !await prompt.filter()) { + continue; + } + + values.push(value); + } + + return substituteParams(values.join('\n')); } -// Wrapper to fetch extension prompts by module name -export function getExtensionPromptByName(moduleName) { - if (moduleName) { - return substituteParams(extension_prompts[moduleName]?.value); - } else { - return; +/** + * Wrapper to fetch extension prompts by module name + * @param {string} moduleName Module name + * @returns {Promise} Extension prompt + */ +export async function getExtensionPromptByName(moduleName) { + if (!moduleName) { + return ''; } + + const prompt = extension_prompts[moduleName]; + + if (!prompt) { + return ''; + } + + const hasFilter = typeof prompt.filter === 'function'; + + if (hasFilter && !await prompt.filter()) { + return ''; + } + + return substituteParams(prompt.value); } /** @@ -2902,27 +2933,36 @@ export function getExtensionPromptByName(moduleName) { * @param {string} [separator] Separator for joining multiple prompts * @param {number} [role] Role of the prompt * @param {boolean} [wrap] Wrap start and end with a separator - * @returns {string} Extension prompt + * @returns {Promise} Extension prompt */ -export function getExtensionPrompt(position = extension_prompt_types.IN_PROMPT, depth = undefined, separator = '\n', role = undefined, wrap = true) { - let extension_prompt = Object.keys(extension_prompts) +export async function getExtensionPrompt(position = extension_prompt_types.IN_PROMPT, depth = undefined, separator = '\n', role = undefined, wrap = true) { + const filterByFunction = async (prompt) => { + const hasFilter = typeof prompt.filter === 'function'; + if (hasFilter && !await prompt.filter()) { + return false; + } + return true; + }; + const promptPromises = Object.keys(extension_prompts) .sort() .map((x) => extension_prompts[x]) .filter(x => x.position == position && x.value) .filter(x => depth === undefined || x.depth === undefined || x.depth === depth) .filter(x => role === undefined || x.role === undefined || x.role === role) - .map(x => x.value.trim()) - .join(separator); - if (wrap && extension_prompt.length && !extension_prompt.startsWith(separator)) { - extension_prompt = separator + extension_prompt; + .filter(filterByFunction); + const prompts = await Promise.all(promptPromises); + + let values = prompts.map(x => x.value.trim()).join(separator); + if (wrap && values.length && !values.startsWith(separator)) { + values = separator + values; } - if (wrap && extension_prompt.length && !extension_prompt.endsWith(separator)) { - extension_prompt = extension_prompt + separator; + if (wrap && values.length && !values.endsWith(separator)) { + values = values + separator; } - if (extension_prompt.length) { - extension_prompt = substituteParams(extension_prompt); + if (values.length) { + values = substituteParams(values); } - return extension_prompt; + return values; } export function baseChatReplace(value, name1, name2) { @@ -3836,7 +3876,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro // Inject all Depth prompts. Chat Completion does it separately let injectedIndices = []; if (main_api !== 'openai') { - injectedIndices = doChatInject(coreChat, isContinue); + injectedIndices = await doChatInject(coreChat, isContinue); } // Insert character jailbreak as the last user message (if exists, allowed, preferred, and not using Chat Completion) @@ -3909,8 +3949,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro } // Call combined AN into Generate - const beforeScenarioAnchor = getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT).trimStart(); - const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.IN_PROMPT); + const beforeScenarioAnchor = (await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT)).trimStart(); + const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT); const storyStringParams = { description: description, @@ -4473,7 +4513,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro ...thisPromptBits[currentArrayEntry], rawPrompt: generate_data.prompt || generate_data.input, mesId: getNextMessageId(type), - allAnchors: getAllExtensionPrompts(), + allAnchors: await getAllExtensionPrompts(), chatInjects: injectedIndices?.map(index => arrMes[arrMes.length - index - 1])?.join('') || '', summarizeString: (extension_prompts['1_memory']?.value || ''), authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''), @@ -4742,9 +4782,9 @@ export function stopGeneration() { * Injects extension prompts into chat messages. * @param {object[]} messages Array of chat messages * @param {boolean} isContinue Whether the generation is a continuation. If true, the extension prompts of depth 0 are injected at position 1. - * @returns {number[]} Array of indices where the extension prompts were injected + * @returns {Promise} Array of indices where the extension prompts were injected */ -function doChatInject(messages, isContinue) { +async function doChatInject(messages, isContinue) { const injectedIndices = []; let totalInsertedMessages = 0; messages.reverse(); @@ -4762,7 +4802,7 @@ function doChatInject(messages, isContinue) { const wrap = false; for (const role of roles) { - const extensionPrompt = String(getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, role, wrap)).trimStart(); + const extensionPrompt = String(await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, role, wrap)).trimStart(); const isNarrator = role === extension_prompt_roles.SYSTEM; const isUser = role === extension_prompt_roles.USER; const name = names[role]; @@ -7455,14 +7495,16 @@ function select_rm_characters() { * @param {number} depth Insertion depth. 0 represets the last message in context. Expected values up to MAX_INJECTION_DEPTH. * @param {number} role Extension prompt role. Defaults to SYSTEM. * @param {boolean} scan Should the prompt be included in the world info scan. + * @param {(function(): Promise|boolean)} filter Filter function to determine if the prompt should be injected. */ -export function setExtensionPrompt(key, value, position, depth, scan = false, role = extension_prompt_roles.SYSTEM) { +export function setExtensionPrompt(key, value, position, depth, scan = false, role = extension_prompt_roles.SYSTEM, filter = null) { extension_prompts[key] = { value: String(value), position: Number(position), depth: Number(depth), scan: !!scan, role: Number(role ?? extension_prompt_roles.SYSTEM), + filter: filter, }; } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 1f5b4d8f9..1162db674 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -611,8 +611,9 @@ function formatWorldInfo(value) { * * @param {Prompt[]} prompts - Array containing injection prompts. * @param {Object[]} messages - Array containing all messages. + * @returns {Promise} - Array containing all messages with injections. */ -function populationInjectionPrompts(prompts, messages) { +async function populationInjectionPrompts(prompts, messages) { let totalInsertedMessages = 0; const roleTypes = { @@ -635,7 +636,7 @@ function populationInjectionPrompts(prompts, messages) { // Get prompts for current role const rolePrompts = depthPrompts.filter(prompt => prompt.role === role).map(x => x.content).join(separator); // Get extension prompt - const extensionPrompt = getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap); + const extensionPrompt = await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap); const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join(separator); @@ -1020,7 +1021,7 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm } // Add in-chat injections - messages = populationInjectionPrompts(absolutePrompts, messages); + messages = await populationInjectionPrompts(absolutePrompts, messages); // Decide whether dialogue examples should always be added if (power_user.pin_examples) { @@ -1051,9 +1052,9 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm * @param {string} options.systemPromptOverride * @param {string} options.jailbreakPromptOverride * @param {string} options.personaDescription - * @returns {Object} prompts - The prepared and merged system and user-defined prompts. + * @returns {Promise} prompts - The prepared and merged system and user-defined prompts. */ -function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription }) { +async function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription }) { const scenarioText = Scenario && oai_settings.scenario_format ? substituteParams(oai_settings.scenario_format) : ''; const charPersonalityText = charPersonality && oai_settings.personality_format ? substituteParams(oai_settings.personality_format) : ''; const groupNudge = substituteParams(oai_settings.group_nudge_prompt); @@ -1142,6 +1143,9 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor if (!extensionPrompts[key].value) continue; if (![extension_prompt_types.BEFORE_PROMPT, extension_prompt_types.IN_PROMPT].includes(prompt.position)) continue; + const hasFilter = typeof prompt.filter === 'function'; + if (hasFilter && !await prompt.filter()) continue; + systemPrompts.push({ identifier: key.replace(/\W/g, '_'), position: getPromptPosition(prompt.position), @@ -1252,7 +1256,7 @@ export async function prepareOpenAIMessages({ try { // Merge markers and ordered user prompts with system prompts - const prompts = preparePromptsForChatCompletion({ + const prompts = await preparePromptsForChatCompletion({ Scenario, charPersonality, name2, diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 0f4351933..c92845d4a 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -3721,7 +3721,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) { // Put this code here since otherwise, the chat reference is modified for (const key of Object.keys(context.extensionPrompts)) { if (context.extensionPrompts[key]?.scan) { - const prompt = getExtensionPromptByName(key); + const prompt = await getExtensionPromptByName(key); if (prompt) { buffer.addInject(prompt); } From add108b8216820a38a49e89a04c055c17d77d088 Mon Sep 17 00:00:00 2001 From: AlpinDale Date: Sat, 7 Dec 2024 12:36:10 +0000 Subject: [PATCH 008/222] fix the JS issue where both ooba and aphro were using the same container ID --- public/index.html | 2 +- public/scripts/textgen-settings.js | 34 ++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/public/index.html b/public/index.html index 5d54b7d48..d8a679675 100644 --- a/public/index.html +++ b/public/index.html @@ -1783,7 +1783,7 @@
Aphrodite only. Determines the order of samplers.
-
+
DRY
Penalties
No Repeat Ngram
diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 21c4202a8..e5d018b74 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -571,6 +571,15 @@ function sortOobaItemsByOrder(orderArray) { }); } +function sortAphroditeItemsByOrder(orderArray) { + const $container = $('#sampler_priority_container_aphrodite'); + + orderArray.forEach((name) => { + const $item = $container.find(`[data-name="${name}"]`).detach(); + $container.append($item); + }); +} + jQuery(function () { $('#koboldcpp_order').sortable({ delay: getSortableDelay(), @@ -624,6 +633,19 @@ jQuery(function () { }, }); + $('#sampler_priority_container_aphrodite').sortable({ + delay: getSortableDelay(), + stop: function () { + const order = []; + $('#sampler_priority_container_aphrodite').children().each(function () { + order.push($(this).data('name')); + }); + settings.samplers_priorities = order; + console.log('Samplers reordered:', settings.samplers_priorities); + saveSettingsDebounced(); + }, + }); + $('#tabby_json_schema').on('input', function () { const json_schema_string = String($(this).val()); @@ -643,9 +665,9 @@ jQuery(function () { }); $('#aphrodite_default_order').on('click', function () { - sortOobaItemsByOrder(APHRODITE_DEFAULT_ORDER); - settings.samplers_priorties = APHRODITE_DEFAULT_ORDER; - console.log('Default samplers order loaded:', settings.samplers_priorties); + sortAphroditeItemsByOrder(APHRODITE_DEFAULT_ORDER); + settings.samplers_priorities = APHRODITE_DEFAULT_ORDER; + console.log('Default samplers order loaded:', settings.samplers_priorities); saveSettingsDebounced(); }); @@ -860,8 +882,8 @@ function setSettingByName(setting, value, trigger) { if ('samplers_priority' === setting) { value = Array.isArray(value) ? value : APHRODITE_DEFAULT_ORDER; insertMissingArrayItems(APHRODITE_DEFAULT_ORDER, value); - sortOobaItemsByOrder(value); - settings.samplers_priorties = value; + sortAphroditeItemsByOrder(value); + settings.samplers_priorities = value; return; } @@ -1289,7 +1311,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'nsigma': settings.nsigma, 'custom_token_bans': toIntArray(banned_tokens), 'no_repeat_ngram_size': settings.no_repeat_ngram_size, - 'sampler_priority': settings.samplers_priorties, + 'sampler_priority': settings.type === APHRODITE ? settings.samplers_priorities : undefined, }; if (settings.type === OPENROUTER) { From 3849908fe1322436d6fa437eb0c48957d8772cf2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:37:57 +0200 Subject: [PATCH 009/222] Add maxTotalChatBackups config.yaml value --- default/config.yaml | 2 ++ src/endpoints/chats.js | 7 +++++++ src/util.js | 14 +++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index 730ffa967..22276d567 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -98,6 +98,8 @@ skipContentCheck: false disableChatBackup: false # Number of backups to keep for each chat and settings file numberOfBackups: 50 +# Maximum number of chat backups to keep per user (starting from the most recent). Set to -1 to keep all backups. +maxTotalChatBackups: 500 # Interval in milliseconds to throttle chat backups per user chatBackupThrottleInterval: 10000 # Allowed hosts for card downloads diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index 2eb8c379e..dbd20687b 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -32,6 +32,13 @@ function backupChat(directory, name, chat) { writeFileAtomicSync(backupFile, chat, 'utf-8'); removeOldBackups(directory, `chat_${name}_`); + + const maxTotalChatBackups = Number(getConfigValue('maxTotalChatBackups', 500)); + if (isNaN(maxTotalChatBackups) || maxTotalChatBackups < 0) { + return; + } + + removeOldBackups(directory, 'chat_', maxTotalChatBackups); } catch (err) { console.log(`Could not backup chat for ${name}`, err); } diff --git a/src/util.js b/src/util.js index 7b8445453..b07142e6b 100644 --- a/src/util.js +++ b/src/util.js @@ -376,16 +376,24 @@ export function generateTimestamp() { * Remove old backups with the given prefix from a specified directory. * @param {string} directory The root directory to remove backups from. * @param {string} prefix File prefix to filter backups by. + * @param {number?} limit Maximum number of backups to keep. If null, the limit is determined by the `numberOfBackups` config value. */ -export function removeOldBackups(directory, prefix) { - const MAX_BACKUPS = Number(getConfigValue('numberOfBackups', 50)); +export function removeOldBackups(directory, prefix, limit = null) { + const MAX_BACKUPS = limit ?? Number(getConfigValue('numberOfBackups', 50)); let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix)); if (files.length > MAX_BACKUPS) { files = files.map(f => path.join(directory, f)); files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs); - fs.rmSync(files[0]); + while (files.length > MAX_BACKUPS) { + const oldest = files.shift(); + if (!oldest) { + break; + } + + fs.rmSync(oldest); + } } } From abe51682c8010443ea9e92b31b25c29c8cee52cb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Dec 2024 17:10:26 +0200 Subject: [PATCH 010/222] [wip] Add global extensions --- public/css/extensions-panel.css | 2 +- public/scripts/extensions.js | 83 +++++++++++++++--- .../scripts/extensions/third-party/.gitkeep | 0 public/scripts/user.js | 2 +- src/constants.js | 1 + src/endpoints/extensions.js | 86 ++++++++++++------- src/users.js | 30 ++++++- 7 files changed, 162 insertions(+), 42 deletions(-) create mode 100644 public/scripts/extensions/third-party/.gitkeep diff --git a/public/css/extensions-panel.css b/public/css/extensions-panel.css index 6e0da7b3f..40185b490 100644 --- a/public/css/extensions-panel.css +++ b/public/css/extensions-panel.css @@ -84,7 +84,7 @@ label[for="extensions_autoconnect"] { .extensions_info .extension_block { display: flex; flex-wrap: wrap; - padding: 10px; + padding: 5px 10px; margin-bottom: 5px; border: 1px solid var(--SmartThemeBorderColor); border-radius: 10px; diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 242a1bb33..c8b27333d 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -6,6 +6,8 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { renderTemplate, renderTemplateAsync } from './templates.js'; import { isSubsetOf, setValueByPath } from './utils.js'; import { getContext } from './st-context.js'; +import { isAdmin } from './user.js'; +import { t } from './i18n.js'; export { getContext, getApiUrl, @@ -19,6 +21,8 @@ export { /** @type {string[]} */ export let extensionNames = []; +/** @type {Record} */ +export let extensionTypes = {}; let manifests = {}; const defaultUrl = 'http://localhost:5100'; @@ -217,6 +221,10 @@ async function doExtrasFetch(endpoint, args) { return response; } +/** + * Discovers extensions from the API. + * @returns {Promise<{name: string, type: string}[]>} + */ async function discoverExtensions() { try { const response = await fetch('/api/extensions/discover'); @@ -702,7 +710,14 @@ async function showExtensionsDetails() { * If the extension is not up to date, it updates the extension and displays a success message with the new commit hash. */ async function onUpdateClick() { + const isCurrentUserAdmin = isAdmin(); const extensionName = $(this).data('name'); + const isGlobal = extensionTypes[extensionName] === 'global'; + if (isGlobal && !isCurrentUserAdmin) { + toastr.error(t`You don't have permission to update global extensions.`); + return; + } + $(this).find('i').addClass('fa-spin'); await updateExtension(extensionName, false); } @@ -717,7 +732,10 @@ async function updateExtension(extensionName, quiet) { const response = await fetch('/api/extensions/update', { method: 'POST', headers: getRequestHeaders(), - body: JSON.stringify({ extensionName }), + body: JSON.stringify({ + extensionName, + global: extensionTypes[extensionName] === 'global', + }), }); const data = await response.json(); @@ -746,6 +764,13 @@ async function updateExtension(extensionName, quiet) { */ async function onDeleteClick() { const extensionName = $(this).data('name'); + const isCurrentUserAdmin = isAdmin(); + const isGlobal = extensionTypes[extensionName] === 'global'; + if (isGlobal && !isCurrentUserAdmin) { + toastr.error(t`You don't have permission to delete global extensions.`); + return; + } + // use callPopup to create a popup for the user to confirm before delete const confirmation = await callGenericPopup(`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {}); if (confirmation === POPUP_RESULT.AFFIRMATIVE) { @@ -753,12 +778,19 @@ async function onDeleteClick() { } } +/** + * Deletes an extension via the API. + * @param {string} extensionName Extension name to delete + */ export async function deleteExtension(extensionName) { try { await fetch('/api/extensions/delete', { method: 'POST', headers: getRequestHeaders(), - body: JSON.stringify({ extensionName }), + body: JSON.stringify({ + extensionName, + global: extensionTypes[extensionName] === 'global', + }), }); } catch (error) { console.error('Error:', error); @@ -796,9 +828,10 @@ async function getExtensionVersion(extensionName) { /** * Installs a third-party extension via the API. * @param {string} url Extension repository URL + * @param {boolean} global Is the extension global? * @returns {Promise} */ -export async function installExtension(url) { +export async function installExtension(url, global) { console.debug('Extension installation started', url); toastr.info('Please wait...', 'Installing extension'); @@ -806,7 +839,10 @@ export async function installExtension(url) { const request = await fetch('/api/extensions/install', { method: 'POST', headers: getRequestHeaders(), - body: JSON.stringify({ url }), + body: JSON.stringify({ + url, + global, + }), }); if (!request.ok) { @@ -841,7 +877,9 @@ async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) // Activate offline extensions await eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD); - extensionNames = await discoverExtensions(); + const extensions = await discoverExtensions(); + extensionNames = extensions.map(x => x.name); + extensionTypes = Object.fromEntries(extensions.map(x => [x.name, x.type])); manifests = await getManifests(extensionNames); if (versionChanged && enableAutoUpdate) { @@ -926,10 +964,16 @@ async function checkForExtensionUpdates(force) { localStorage.setItem(STORAGE_NAG_KEY, currentDate); } + const isCurrentUserAdmin = isAdmin(); const updatesAvailable = []; const promises = []; for (const [id, manifest] of Object.entries(manifests)) { + const isGlobal = extensionTypes[id] === 'global'; + if (isGlobal && !isCurrentUserAdmin) { + console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); + continue; + } if (manifest.auto_update && id.startsWith('third-party')) { const promise = new Promise(async (resolve, reject) => { try { @@ -965,8 +1009,14 @@ async function autoUpdateExtensions(forceAll) { } const banner = toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 10000 }); + const isCurrentUserAdmin = isAdmin(); const promises = []; for (const [id, manifest] of Object.entries(manifests)) { + const isGlobal = extensionTypes[id] === 'global'; + if (isGlobal && !isCurrentUserAdmin) { + console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); + continue; + } if ((forceAll || manifest.auto_update) && id.startsWith('third-party')) { console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`); promises.push(updateExtension(id.replace('third-party', ''), true)); @@ -1068,8 +1118,23 @@ export async function writeExtensionField(characterId, key, value) { * @returns {Promise} */ export async function openThirdPartyExtensionMenu(suggestUrl = '') { - const html = await renderTemplateAsync('installExtension'); - const input = await callGenericPopup(html, POPUP_TYPE.INPUT, suggestUrl ?? ''); + const isCurrentUserAdmin = isAdmin(); + const html = await renderTemplateAsync('installExtension', { isCurrentUserAdmin }); + const okButton = isCurrentUserAdmin ? t`Install just for me` : t`Install`; + + let global = false; + const installForAllButton = { + text: t`Install for all`, + appendAtEnd: false, + action: async () => { + global = true; + await popup.complete(POPUP_RESULT.AFFIRMATIVE); + }, + }; + + const customButtons = isCurrentUserAdmin ? [installForAllButton] : []; + const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons }); + const input = await popup.show(); if (!input) { console.debug('Extension install cancelled'); @@ -1077,11 +1142,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { } const url = String(input).trim(); - await installExtension(url); + await installExtension(url, global); } - - export async function initExtensions() { await addExtensionsButtonAndMenu(); $('#extensionsMenuButton').css('display', 'flex'); diff --git a/public/scripts/extensions/third-party/.gitkeep b/public/scripts/extensions/third-party/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/public/scripts/user.js b/public/scripts/user.js index c5d984e1b..51f7e1011 100644 --- a/public/scripts/user.js +++ b/public/scripts/user.js @@ -31,7 +31,7 @@ export async function setUserControls(isEnabled) { * Check if the current user is an admin. * @returns {boolean} True if the current user is an admin */ -function isAdmin() { +export function isAdmin() { if (!currentUser) { return false; } diff --git a/src/constants.js b/src/constants.js index 35ba17f85..673bd95f0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,6 +3,7 @@ export const PUBLIC_DIRECTORIES = { backups: 'backups/', sounds: 'public/sounds', extensions: 'public/scripts/extensions', + globalExtensions: 'public/scripts/extensions/third-party', }; export const SETTINGS_FILE = 'settings.json'; diff --git a/src/endpoints/extensions.js b/src/endpoints/extensions.js index ecc2f1d23..ebad7da06 100644 --- a/src/endpoints/extensions.js +++ b/src/endpoints/extensions.js @@ -73,8 +73,18 @@ router.post('/install', jsonParser, async (request, response) => { fs.mkdirSync(path.join(request.user.directories.extensions)); } - const url = request.body.url; - const extensionPath = path.join(request.user.directories.extensions, path.basename(url, '.git')); + if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) { + fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); + } + + const { url, global } = request.body; + + if (global && !request.user.profile.admin) { + return response.status(403).send('Forbidden: No permission to install global extensions.'); + } + + const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; + const extensionPath = path.join(basePath, sanitize(path.basename(url, '.git'))); if (fs.existsSync(extensionPath)) { return response.status(409).send(`Directory already exists at ${extensionPath}`); @@ -83,10 +93,8 @@ router.post('/install', jsonParser, async (request, response) => { await git.clone(url, extensionPath, { '--depth': 1 }); console.log(`Extension has been cloned at ${extensionPath}`); - const { version, author, display_name } = await getManifest(extensionPath); - return response.send({ version, author, display_name, extensionPath }); } catch (error) { console.log('Importing custom content failed', error); @@ -112,8 +120,14 @@ router.post('/update', jsonParser, async (request, response) => { } try { - const extensionName = request.body.extensionName; - const extensionPath = path.join(request.user.directories.extensions, extensionName); + const { extensionName, global } = request.body; + + if (global && !request.user.profile.admin) { + return response.status(403).send('Forbidden: No permission to update global extensions.'); + } + + const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; + const extensionPath = path.join(basePath, extensionName); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); @@ -122,7 +136,6 @@ router.post('/update', jsonParser, async (request, response) => { const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath); const currentBranch = await git.cwd(extensionPath).branch(); if (!isUpToDate) { - await git.cwd(extensionPath).pull('origin', currentBranch.current); console.log(`Extension has been updated at ${extensionPath}`); } else { @@ -157,8 +170,9 @@ router.post('/version', jsonParser, async (request, response) => { } try { - const extensionName = request.body.extensionName; - const extensionPath = path.join(request.user.directories.extensions, extensionName); + const { extensionName, global } = request.body; + const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; + const extensionPath = path.join(basePath, sanitize(extensionName)); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); @@ -193,11 +207,15 @@ router.post('/delete', jsonParser, async (request, response) => { return response.status(400).send('Bad Request: extensionName is required in the request body.'); } - // Sanitize the extension name to prevent directory traversal - const extensionName = sanitize(request.body.extensionName); - try { - const extensionPath = path.join(request.user.directories.extensions, extensionName); + const { extensionName, global } = request.body; + + if (global && !request.user.profile.admin) { + return response.status(403).send('Forbidden: No permission to delete global extensions.'); + } + + const basePath = global ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; + const extensionPath = path.join(basePath, sanitize(extensionName)); if (!fs.existsSync(extensionPath)) { return response.status(404).send(`Directory does not exist at ${extensionPath}`); @@ -219,26 +237,36 @@ router.post('/delete', jsonParser, async (request, response) => { * If the folder is called third-party, search for subfolders instead */ router.get('/discover', jsonParser, function (request, response) { - // get all folders in the extensions folder, except third-party - const extensions = fs - .readdirSync(PUBLIC_DIRECTORIES.extensions) - .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) - .filter(f => f !== 'third-party'); - - // get all folders in the third-party folder, if it exists - if (!fs.existsSync(path.join(request.user.directories.extensions))) { - return response.send(extensions); + fs.mkdirSync(path.join(request.user.directories.extensions)); } - const thirdPartyExtensions = fs + if (!fs.existsSync(PUBLIC_DIRECTORIES.globalExtensions)) { + fs.mkdirSync(PUBLIC_DIRECTORIES.globalExtensions); + } + + // Get all folders in system extensions folder, excluding third-party + const buildInExtensions = fs + .readdirSync(PUBLIC_DIRECTORIES.extensions) + .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) + .filter(f => f !== 'third-party') + .map(f => ({ type: 'system', name: f })); + + // Get all folders in global extensions folder + const globalExtensions = fs + .readdirSync(PUBLIC_DIRECTORIES.globalExtensions) + .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory()) + .map(f => ({ type: 'global', name: `third-party/${f}` })); + + // Get all folders in local extensions folder + const userExtensions = fs .readdirSync(path.join(request.user.directories.extensions)) - .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()); + .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()) + .map(f => ({ type: 'local', name: `third-party/${f}` })); - // add the third-party extensions to the extensions array - extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`)); - console.log(extensions); + // Combine all extensions + const allExtensions = Array.from(new Set([...buildInExtensions, ...globalExtensions, ...userExtensions])); + console.log(allExtensions); - - return response.send(extensions); + return response.send(allExtensions); }); diff --git a/src/users.js b/src/users.js index 96e21c5ab..36a5d62b3 100644 --- a/src/users.js +++ b/src/users.js @@ -782,6 +782,34 @@ function createRouteHandler(directoryFn) { }; } +/** + * Creates a route handler for serving extensions. + * @param {(req: import('express').Request) => string} directoryFn A function that returns the directory path to serve files from + * @returns {import('express').RequestHandler} + */ +function createExtensionsRouteHandler(directoryFn) { + return async (req, res) => { + try { + const directory = directoryFn(req); + const filePath = decodeURIComponent(req.params[0]); + + const existsLocal = fs.existsSync(path.join(directory, filePath)); + if (existsLocal) { + return res.sendFile(filePath, { root: directory }); + } + + const existsGlobal = fs.existsSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, filePath)); + if (existsGlobal) { + return res.sendFile(filePath, { root: PUBLIC_DIRECTORIES.globalExtensions }); + } + + return res.sendStatus(404); + } catch (error) { + return res.sendStatus(500); + } + }; +} + /** * Verifies that the current user is an admin. * @param {import('express').Request} request Request object @@ -872,4 +900,4 @@ router.use('/User%20Avatars/*', createRouteHandler(req => req.user.directories.a router.use('/assets/*', createRouteHandler(req => req.user.directories.assets)); router.use('/user/images/*', createRouteHandler(req => req.user.directories.userImages)); router.use('/user/files/*', createRouteHandler(req => req.user.directories.files)); -router.use('/scripts/extensions/third-party/*', createRouteHandler(req => req.user.directories.extensions)); +router.use('/scripts/extensions/third-party/*', createExtensionsRouteHandler(req => req.user.directories.extensions)); From c33649753b7ab730712fac522b432c0749fbb728 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Dec 2024 18:12:27 +0200 Subject: [PATCH 011/222] Improve extension type indication --- public/css/extensions-panel.css | 2 +- public/scripts/extensions.js | 202 +++++++++++++++++++++----------- src/endpoints/extensions.js | 18 +-- 3 files changed, 146 insertions(+), 76 deletions(-) diff --git a/public/css/extensions-panel.css b/public/css/extensions-panel.css index 40185b490..18783a614 100644 --- a/public/css/extensions-panel.css +++ b/public/css/extensions-panel.css @@ -90,7 +90,7 @@ label[for="extensions_autoconnect"] { border-radius: 10px; align-items: center; justify-content: space-between; - gap: 10px; + gap: 5px; } .extensions_info .extension_name { diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index c8b27333d..776b7e775 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -21,7 +21,11 @@ export { /** @type {string[]} */ export let extensionNames = []; -/** @type {Record} */ +/** + * Holds the type of each extension. + * Don't use this directly, use getExtensionType instead! + * @type {Record} + */ export let extensionTypes = {}; let manifests = {}; @@ -198,6 +202,16 @@ function showHideExtensionsMenu() { // Periodically check for new extensions const menuInterval = setInterval(showHideExtensionsMenu, 1000); +/** + * Gets the type of an extension based on its external ID. + * @param {string} externalId External ID of the extension (excluding or including the leading 'third-party/') + * @returns {string} Type of the extension (global, local, system, or empty string if not found) + */ +function getExtensionType(externalId) { + const id = Object.keys(extensionTypes).find(id => id === externalId || (id.startsWith('third-party') && id.endsWith(externalId))); + return id ? extensionTypes[id] : ''; +} + async function doExtrasFetch(endpoint, args) { if (!args) { args = {}; @@ -457,63 +471,72 @@ function updateStatus(success) { $('#extensions_status').attr('class', _class); } +/** + * Adds a CSS file for an extension. + * @param {string} name Extension name + * @param {object} manifest Extension manifest + * @returns {Promise} When the CSS is loaded + */ function addExtensionStyle(name, manifest) { - if (manifest.css) { - return new Promise((resolve, reject) => { - const url = `/scripts/extensions/${name}/${manifest.css}`; - - if ($(`link[id="${name}"]`).length === 0) { - const link = document.createElement('link'); - link.id = name; - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = url; - link.onload = function () { - resolve(); - }; - link.onerror = function (e) { - reject(e); - }; - document.head.appendChild(link); - } - }); + if (!manifest.css) { + return Promise.resolve(); } - return Promise.resolve(); + return new Promise((resolve, reject) => { + const url = `/scripts/extensions/${name}/${manifest.css}`; + + if ($(`link[id="${name}"]`).length === 0) { + const link = document.createElement('link'); + link.id = name; + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = url; + link.onload = function () { + resolve(); + }; + link.onerror = function (e) { + reject(e); + }; + document.head.appendChild(link); + } + }); } +/** + * Loads a JS file for an extension. + * @param {string} name Extension name + * @param {object} manifest Extension manifest + * @returns {Promise} When the script is loaded + */ function addExtensionScript(name, manifest) { - if (manifest.js) { - return new Promise((resolve, reject) => { - const url = `/scripts/extensions/${name}/${manifest.js}`; - let ready = false; - - if ($(`script[id="${name}"]`).length === 0) { - const script = document.createElement('script'); - script.id = name; - script.type = 'module'; - script.src = url; - script.async = true; - script.onerror = function (err) { - reject(err, script); - }; - script.onload = script.onreadystatechange = function () { - // console.log(this.readyState); // uncomment this line to see which ready states are called. - if (!ready && (!this.readyState || this.readyState == 'complete')) { - ready = true; - resolve(); - } - }; - document.body.appendChild(script); - } - }); + if (!manifest.js) { + return Promise.resolve(); } - return Promise.resolve(); + return new Promise((resolve, reject) => { + const url = `/scripts/extensions/${name}/${manifest.js}`; + let ready = false; + + if ($(`script[id="${name}"]`).length === 0) { + const script = document.createElement('script'); + script.id = name; + script.type = 'module'; + script.src = url; + script.async = true; + script.onerror = function (err) { + reject(err); + }; + script.onload = function () { + if (!ready) { + ready = true; + resolve(); + } + }; + document.body.appendChild(script); + } + }); } - - /** * Generates HTML string for displaying an extension in the UI. * @@ -526,6 +549,22 @@ function addExtensionScript(name, manifest) { * @return {string} - The HTML string that represents the extension. */ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { + function getExtensionIcon() { + const type = getExtensionType(name); + switch (type) { + case 'global': + return ''; + case 'local': + return ''; + case 'system': + return ''; + default: + return ''; + } + } + + const isUserAdmin = isAdmin(); + const extensionIcon = getExtensionIcon(); const displayName = manifest.display_name; let displayVersion = manifest.version ? ` v${manifest.version}` : ''; const externalId = name.replace('third-party', ''); @@ -540,6 +579,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, let deleteButton = isExternal ? `` : ''; let updateButton = isExternal ? `` : ''; + let moveButton = isExternal && isUserAdmin ? `` : ''; let modulesInfo = ''; if (isActive && Array.isArray(manifest.optional)) { @@ -565,6 +605,9 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
${toggleElement}
+
+ ${extensionIcon} +
${originHtml} @@ -577,6 +620,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
${updateButton} + ${moveButton} ${deleteButton}
`; @@ -622,6 +666,7 @@ function getModuleInformation() { * Generates the HTML strings for all extensions and displays them in a popup. */ async function showExtensionsDetails() { + const abortController = new AbortController(); let popupPromise; try { const htmlDefault = $('

Built-in Extensions:

'); @@ -688,13 +733,14 @@ async function showExtensionsDetails() { }, }); popupPromise = popup.show(); - checkForUpdatesManual().finally(() => htmlLoading.remove()); + checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove()); } catch (error) { toastr.error('Error loading extensions. See browser console for details.'); console.error(error); } if (popupPromise) { await popupPromise; + abortController.abort(); } if (requiresReload) { showLoader(); @@ -702,7 +748,6 @@ async function showExtensionsDetails() { } } - /** * Handles the click event for the update button of an extension. * This function makes a POST request to '/update_extension' with the extension's name. @@ -712,7 +757,7 @@ async function showExtensionsDetails() { async function onUpdateClick() { const isCurrentUserAdmin = isAdmin(); const extensionName = $(this).data('name'); - const isGlobal = extensionTypes[extensionName] === 'global'; + const isGlobal = getExtensionType(extensionName) === 'global'; if (isGlobal && !isCurrentUserAdmin) { toastr.error(t`You don't have permission to update global extensions.`); return; @@ -734,7 +779,7 @@ async function updateExtension(extensionName, quiet) { headers: getRequestHeaders(), body: JSON.stringify({ extensionName, - global: extensionTypes[extensionName] === 'global', + global: getExtensionType(extensionName) === 'global', }), }); @@ -765,7 +810,7 @@ async function updateExtension(extensionName, quiet) { async function onDeleteClick() { const extensionName = $(this).data('name'); const isCurrentUserAdmin = isAdmin(); - const isGlobal = extensionTypes[extensionName] === 'global'; + const isGlobal = getExtensionType(extensionName) === 'global'; if (isGlobal && !isCurrentUserAdmin) { toastr.error(t`You don't have permission to delete global extensions.`); return; @@ -778,6 +823,18 @@ async function onDeleteClick() { } } +async function onMoveClick() { + const extensionName = $(this).data('name'); + const isCurrentUserAdmin = isAdmin(); + const isGlobal = getExtensionType(extensionName) === 'global'; + if (isGlobal && !isCurrentUserAdmin) { + toastr.error(t`You don't have permission to move extensions.`); + return; + } + + toastr.info('Not implemented yet'); +} + /** * Deletes an extension via the API. * @param {string} extensionName Extension name to delete @@ -789,7 +846,7 @@ export async function deleteExtension(extensionName) { headers: getRequestHeaders(), body: JSON.stringify({ extensionName, - global: extensionTypes[extensionName] === 'global', + global: getExtensionType(extensionName) === 'global', }), }); } catch (error) { @@ -806,16 +863,21 @@ export async function deleteExtension(extensionName) { * Fetches the version details of a specific extension. * * @param {string} extensionName - The name of the extension. + * @param {AbortSignal} [abortSignal] - The signal to abort the operation. * @return {Promise} - An object containing the extension's version details. * This object includes the currentBranchName, currentCommitHash, isUpToDate, and remoteUrl. * @throws {error} - If there is an error during the fetch operation, it logs the error to the console. */ -async function getExtensionVersion(extensionName) { +async function getExtensionVersion(extensionName, abortSignal) { try { const response = await fetch('/api/extensions/version', { method: 'POST', headers: getRequestHeaders(), - body: JSON.stringify({ extensionName }), + body: JSON.stringify({ + extensionName, + global: getExtensionType(extensionName) === 'global', + }), + signal: abortSignal, }); const data = await response.json(); @@ -900,13 +962,18 @@ export function doDailyExtensionUpdatesCheck() { }, 1); } -async function checkForUpdatesManual() { +/** + * Performs a manual check for updates on all 3rd-party extensions. + * @param {AbortSignal} abortSignal Signal to abort the operation + * @returns {Promise} + */ +async function checkForUpdatesManual(abortSignal) { const promises = []; for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party'))) { const externalId = id.replace('third-party', ''); const promise = new Promise(async (resolve, reject) => { try { - const data = await getExtensionVersion(externalId); + const data = await getExtensionVersion(externalId, abortSignal); const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`); if (extensionBlock) { if (data.isUpToDate === false) { @@ -969,7 +1036,7 @@ async function checkForExtensionUpdates(force) { const promises = []; for (const [id, manifest] of Object.entries(manifests)) { - const isGlobal = extensionTypes[id] === 'global'; + const isGlobal = getExtensionType(id) === 'global'; if (isGlobal && !isCurrentUserAdmin) { console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); continue; @@ -1012,7 +1079,7 @@ async function autoUpdateExtensions(forceAll) { const isCurrentUserAdmin = isAdmin(); const promises = []; for (const [id, manifest] of Object.entries(manifests)) { - const isGlobal = extensionTypes[id] === 'global'; + const isGlobal = getExtensionType(id) === 'global'; if (isGlobal && !isCurrentUserAdmin) { console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); continue; @@ -1043,9 +1110,9 @@ async function runGenerationInterceptors(chat, contextSize) { for (const manifest of Object.values(manifests).sort((a, b) => a.loading_order - b.loading_order)) { const interceptorKey = manifest.generate_interceptor; - if (typeof window[interceptorKey] === 'function') { + if (typeof globalThis[interceptorKey] === 'function') { try { - await window[interceptorKey](chat, contextSize, abort); + await globalThis[interceptorKey](chat, contextSize, abort); } catch (e) { console.error(`Failed running interceptor for ${manifest.display_name}`, e); } @@ -1124,7 +1191,7 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { let global = false; const installForAllButton = { - text: t`Install for all`, + text: t`Install for all users`, appendAtEnd: false, action: async () => { global = true; @@ -1153,10 +1220,11 @@ export async function initExtensions() { $('#extensions_autoconnect').on('input', autoConnectInputHandler); $('#extensions_details').on('click', showExtensionsDetails); $('#extensions_notify_updates').on('input', notifyUpdatesInputHandler); - $(document).on('click', '.toggle_disable', onDisableExtensionClick); - $(document).on('click', '.toggle_enable', onEnableExtensionClick); - $(document).on('click', '.btn_update', onUpdateClick); - $(document).on('click', '.btn_delete', onDeleteClick); + $(document).on('click', '.extensions_info .extension_block .toggle_disable', onDisableExtensionClick); + $(document).on('click', '.extensions_info .extension_block .toggle_enable', onEnableExtensionClick); + $(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick); + $(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick); + $(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick); /** * Handles the click event for the third-party extension import button. diff --git a/src/endpoints/extensions.js b/src/endpoints/extensions.js index ebad7da06..de829291f 100644 --- a/src/endpoints/extensions.js +++ b/src/endpoints/extensions.js @@ -246,26 +246,28 @@ router.get('/discover', jsonParser, function (request, response) { } // Get all folders in system extensions folder, excluding third-party - const buildInExtensions = fs + const builtInExtensions = fs .readdirSync(PUBLIC_DIRECTORIES.extensions) .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.extensions, f)).isDirectory()) .filter(f => f !== 'third-party') .map(f => ({ type: 'system', name: f })); - // Get all folders in global extensions folder - const globalExtensions = fs - .readdirSync(PUBLIC_DIRECTORIES.globalExtensions) - .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory()) - .map(f => ({ type: 'global', name: `third-party/${f}` })); - // Get all folders in local extensions folder const userExtensions = fs .readdirSync(path.join(request.user.directories.extensions)) .filter(f => fs.statSync(path.join(request.user.directories.extensions, f)).isDirectory()) .map(f => ({ type: 'local', name: `third-party/${f}` })); + // Get all folders in global extensions folder + // In case of a conflict, the extension will be loaded from the user folder + const globalExtensions = fs + .readdirSync(PUBLIC_DIRECTORIES.globalExtensions) + .filter(f => fs.statSync(path.join(PUBLIC_DIRECTORIES.globalExtensions, f)).isDirectory()) + .map(f => ({ type: 'global', name: `third-party/${f}` })) + .filter(f => !userExtensions.some(e => e.name === f.name)); + // Combine all extensions - const allExtensions = Array.from(new Set([...buildInExtensions, ...globalExtensions, ...userExtensions])); + const allExtensions = [...builtInExtensions, ...userExtensions, ...globalExtensions]; console.log(allExtensions); return response.send(allExtensions); From 83965fb611b84e769c62c8428366941b45cda218 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Dec 2024 18:42:37 +0200 Subject: [PATCH 012/222] Implement move extensions --- public/scripts/extensions.js | 66 +++++++++++++++++++++++++++++++----- src/endpoints/extensions.js | 47 +++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 776b7e775..52ea725d6 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -465,7 +465,7 @@ async function connectToApi(baseUrl) { function updateStatus(success) { connectedToApi = success; - const _text = success ? 'Connected to API' : 'Could not connect to API'; + const _text = success ? t`Connected to API` : t`Could not connect to API`; const _class = success ? 'success' : 'failure'; $('#extensions_status').text(_text); $('#extensions_status').attr('class', _class); @@ -723,7 +723,7 @@ async function showExtensionsDetails() { } if (stateChanged) { waitingForSave = true; - const toast = toastr.info('The page will be reloaded shortly...', 'Extensions state changed'); + const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`); await saveSettings(); toastr.clear(toast); waitingForSave = false; @@ -735,7 +735,7 @@ async function showExtensionsDetails() { popupPromise = popup.show(); checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove()); } catch (error) { - toastr.error('Error loading extensions. See browser console for details.'); + toastr.error(t`Error loading extensions. See browser console for details.`); console.error(error); } if (popupPromise) { @@ -817,7 +817,7 @@ async function onDeleteClick() { } // use callPopup to create a popup for the user to confirm before delete - const confirmation = await callGenericPopup(`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {}); + const confirmation = await callGenericPopup(t`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {}); if (confirmation === POPUP_RESULT.AFFIRMATIVE) { await deleteExtension(extensionName); } @@ -832,7 +832,57 @@ async function onMoveClick() { return; } - toastr.info('Not implemented yet'); + const source = getExtensionType(extensionName); + const destination = source === 'global' ? 'local' : 'global'; + + const confirmationHeader = t`Move extension`; + const confirmationText = source == 'global' + ? t`Are you sure you want to move ${extensionName} to your local extensions? This will make it available only for you.` + : t`Are you sure you want to move ${extensionName} to the global extensions? This will make it available for all users.`; + + const confirmation = await Popup.show.confirm(confirmationHeader, confirmationText); + + if (!confirmation) { + return; + } + + $(this).find('i').addClass('fa-spin'); + await moveExtension(extensionName, source, destination); +} + +/** + * Moves an extension via the API. + * @param {string} extensionName Extension name + * @param {string} source Source type + * @param {string} destination Destination type + * @returns {Promise} + */ +async function moveExtension(extensionName, source, destination) { + try { + const result = await fetch('/api/extensions/move', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + extensionName, + source, + destination, + }), + }); + + if (!result.ok) { + const text = await result.text(); + toastr.error(text || result.statusText, t`Extension move failed`, { timeOut: 5000 }); + console.error('Extension move failed', result.status, result.statusText, text); + return; + } + + toastr.success(t`Extension ${extensionName} moved.`); + await loadExtensionSettings({}, false, false); + await Popup.util.popups.find(popup => popup.content.querySelector('.extensions_info'))?.completeCancelled(); + showExtensionsDetails(); + } catch (error) { + console.error('Error:', error); + } } /** @@ -853,7 +903,7 @@ export async function deleteExtension(extensionName) { console.error('Error:', error); } - toastr.success(`Extension ${extensionName} deleted`); + toastr.success(t`Extension ${extensionName} deleted`); showExtensionsDetails(); // reload the page to remove the extension from the list location.reload(); @@ -896,7 +946,7 @@ async function getExtensionVersion(extensionName, abortSignal) { export async function installExtension(url, global) { console.debug('Extension installation started', url); - toastr.info('Please wait...', 'Installing extension'); + toastr.info(t`Please wait...`, t`Installing extension`); const request = await fetch('/api/extensions/install', { method: 'POST', @@ -909,7 +959,7 @@ export async function installExtension(url, global) { if (!request.ok) { const text = await request.text(); - toastr.warning(text || request.statusText, 'Extension installation failed', { timeOut: 5000 }); + toastr.warning(text || request.statusText, t`Extension installation failed`, { timeOut: 5000 }); console.error('Extension installation failed', request.status, request.statusText, text); return; } diff --git a/src/endpoints/extensions.js b/src/endpoints/extensions.js index de829291f..67b96cc47 100644 --- a/src/endpoints/extensions.js +++ b/src/endpoints/extensions.js @@ -80,6 +80,7 @@ router.post('/install', jsonParser, async (request, response) => { const { url, global } = request.body; if (global && !request.user.profile.admin) { + console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`); return response.status(403).send('Forbidden: No permission to install global extensions.'); } @@ -123,6 +124,7 @@ router.post('/update', jsonParser, async (request, response) => { const { extensionName, global } = request.body; if (global && !request.user.profile.admin) { + console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`); return response.status(403).send('Forbidden: No permission to update global extensions.'); } @@ -153,6 +155,50 @@ router.post('/update', jsonParser, async (request, response) => { } }); +router.post('/move', jsonParser, async (request, response) => { + try { + const { extensionName, source, destination } = request.body; + + if (!extensionName || !source || !destination) { + return response.status(400).send('Bad Request. Not all required parameters are provided.'); + } + + if (!request.user.profile.admin) { + console.warn(`User ${request.user.profile.handle} does not have permission to move extensions.`); + return response.status(403).send('Forbidden: No permission to move extensions.'); + } + + const sourceDirectory = source === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; + const destinationDirectory = destination === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; + const sourcePath = path.join(sourceDirectory, sanitize(extensionName)); + const destinationPath = path.join(destinationDirectory, sanitize(extensionName)); + + if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) { + console.error(`Source directory does not exist at ${sourcePath}`); + return response.status(404).send('Source directory does not exist.'); + } + + if (fs.existsSync(destinationPath)) { + console.error(`Destination directory already exists at ${destinationPath}`); + return response.status(409).send('Destination directory already exists.'); + } + + if (source === destination) { + console.error('Source and destination directories are the same'); + return response.status(409).send('Source and destination directories are the same.'); + } + + fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true }); + fs.rmSync(sourcePath, { recursive: true, force: true }); + console.log(`Extension has been moved from ${sourcePath} to ${destinationPath}`); + + return response.sendStatus(204); + } catch (error) { + console.log('Moving extension failed', error); + return response.status(500).send('Internal Server Error. Try again later.'); + } +}); + /** * HTTP POST handler function to get the current git commit hash and branch name for a given extension. * It checks whether the repository is up-to-date with the remote, and returns the status along with @@ -211,6 +257,7 @@ router.post('/delete', jsonParser, async (request, response) => { const { extensionName, global } = request.body; if (global && !request.user.profile.admin) { + console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`); return response.status(403).send('Forbidden: No permission to delete global extensions.'); } From 126616d5390d751cffe048fd776854025a1a885f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:31:16 +0200 Subject: [PATCH 013/222] Refactor and JSDoc extensions.js --- .dockerignore | 1 + .npmignore | 1 + public/css/extensions-panel.css | 5 -- public/scripts/extensions.js | 150 ++++++++++++++++++-------------- 4 files changed, 87 insertions(+), 70 deletions(-) diff --git a/.dockerignore b/.dockerignore index 99976ae65..9e0a20629 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,3 +12,4 @@ access.log /data /cache .DS_Store +/public/scripts/extensions/third-party diff --git a/.npmignore b/.npmignore index 150ad23aa..0c99b6680 100644 --- a/.npmignore +++ b/.npmignore @@ -11,3 +11,4 @@ access.log .github .vscode .git +/public/scripts/extensions/third-party diff --git a/public/css/extensions-panel.css b/public/css/extensions-panel.css index 18783a614..5860b2eab 100644 --- a/public/css/extensions-panel.css +++ b/public/css/extensions-panel.css @@ -116,11 +116,6 @@ input.extension_missing[type="checkbox"] { opacity: 0.5; } -#extensions_list .disabled { - text-decoration: line-through; - color: lightgray; -} - .update-button { margin-right: 10px; display: inline-flex; diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 52ea725d6..9416c38e2 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -8,19 +8,16 @@ import { isSubsetOf, setValueByPath } from './utils.js'; import { getContext } from './st-context.js'; import { isAdmin } from './user.js'; import { t } from './i18n.js'; +import { debounce_timeout } from './constants.js'; + export { getContext, getApiUrl, - loadExtensionSettings, - runGenerationInterceptors, - doExtrasFetch, - modules, - extension_settings, - ModuleWorkerWrapper, }; /** @type {string[]} */ export let extensionNames = []; + /** * Holds the type of each extension. * Don't use this directly, use getExtensionType instead! @@ -28,13 +25,35 @@ export let extensionNames = []; */ export let extensionTypes = {}; -let manifests = {}; -const defaultUrl = 'http://localhost:5100'; +/** + * A list of active modules provided by the Extras API. + * @type {string[]} + */ +export let modules = []; -let saveMetadataTimeout = null; +/** + * A set of active extensions. + * @type {Set} + */ +let activeExtensions = new Set(); + +const getApiUrl = () => extension_settings.apiUrl; +let connectedToApi = false; + +/** + * Holds manifest data for each extension. + * @type {Record} + */ +let manifests = {}; + +/** + * Default URL for the Extras API. + */ +const defaultUrl = 'http://localhost:5100'; let requiresReload = false; let stateChanged = false; +let saveMetadataTimeout = null; export function saveMetadataDebounced() { const context = getContext(); @@ -59,9 +78,9 @@ export function saveMetadataDebounced() { } console.debug('Saving metadata...'); - newContext.saveMetadata(); + await newContext.saveMetadata(); console.debug('Saved metadata...'); - }, 1000); + }, debounce_timeout.relaxed); } /** @@ -91,7 +110,7 @@ export function renderExtensionTemplateAsync(extensionName, templateId, template } // Disables parallel updates -class ModuleWorkerWrapper { +export class ModuleWorkerWrapper { constructor(callback) { this.isBusy = false; this.callback = callback; @@ -115,7 +134,7 @@ class ModuleWorkerWrapper { } } -const extension_settings = { +export const extension_settings = { apiUrl: defaultUrl, apiKey: '', autoConnect: false, @@ -180,12 +199,6 @@ const extension_settings = { disabled_attachments: [], }; -let modules = []; -let activeExtensions = new Set(); - -const getApiUrl = () => extension_settings.apiUrl; -let connectedToApi = false; - function showHideExtensionsMenu() { // Get the number of menu items that are not hidden const hasMenuItems = $('#extensionsMenu').children().filter((_, child) => $(child).css('display') !== 'none').length > 0; @@ -212,7 +225,13 @@ function getExtensionType(externalId) { return id ? extensionTypes[id] : ''; } -async function doExtrasFetch(endpoint, args) { +/** + * Performs a fetch of the Extras API. + * @param {string|URL} endpoint Extras API endpoint + * @param {RequestInit} args Request arguments + * @returns {Promise} Response from the fetch + */ +export async function doExtrasFetch(endpoint, args = {}) { if (!args) { args = {}; } @@ -231,8 +250,7 @@ async function doExtrasFetch(endpoint, args) { }); } - const response = await fetch(endpoint, args); - return response; + return await fetch(endpoint, args); } /** @@ -267,6 +285,11 @@ function onEnableExtensionClick() { enableExtension(name, false); } +/** + * Enables an extension by name. + * @param {string} name Extension name + * @param {boolean} [reload=true] If true, reload the page after enabling the extension + */ export async function enableExtension(name, reload = true) { extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name); stateChanged = true; @@ -278,6 +301,11 @@ export async function enableExtension(name, reload = true) { } } +/** + * Disables an extension by name. + * @param {string} name Extension name + * @param {boolean} [reload=true] If true, reload the page after disabling the extension + */ export async function disableExtension(name, reload = true) { extension_settings.disabledExtensions.push(name); stateChanged = true; @@ -289,6 +317,11 @@ export async function disableExtension(name, reload = true) { } } +/** + * Loads manifest.json files for extensions. + * @param {string[]} names Array of extension names + * @returns {Promise>} Object with extension names as keys and their manifests as values + */ async function getManifests(names) { const obj = {}; const promises = []; @@ -316,6 +349,10 @@ async function getManifests(names) { return obj; } +/** + * Tries to activate all available extensions that are not already active. + * @returns {Promise} + */ async function activateExtensions() { const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); const promises = []; @@ -323,36 +360,25 @@ async function activateExtensions() { for (let entry of extensions) { const name = entry[0]; const manifest = entry[1]; - const elementExists = document.getElementById(name) !== null; - if (elementExists || activeExtensions.has(name)) { + if (activeExtensions.has(name)) { continue; } - // all required modules are active (offline extensions require none) - if (isSubsetOf(modules, manifest.requires)) { + const meetsModuleRequirements = !Array.isArray(manifest.requires) || isSubsetOf(modules, manifest.requires); + const isDisabled = extension_settings.disabledExtensions.includes(name); + + if (meetsModuleRequirements && !isDisabled) { try { - const isDisabled = extension_settings.disabledExtensions.includes(name); - const li = document.createElement('li'); - - if (!isDisabled) { - const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]); - await promise - .then(() => activeExtensions.add(name)) - .catch(err => console.log('Could not activate extension: ' + name, err)); - promises.push(promise); - } - else { - li.classList.add('disabled'); - } - - li.id = name; - li.innerText = manifest.display_name; - - $('#extensions_list').append(li); + console.debug('Activating extension', name); + const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]); + await promise + .then(() => activeExtensions.add(name)) + .catch(err => console.log('Could not activate extension', name, err)); + promises.push(promise); } catch (error) { - console.error(`Could not activate extension: ${name}`); + console.error('Could not activate extension', name); console.error(error); } } @@ -362,8 +388,8 @@ async function activateExtensions() { } async function connectClickHandler() { - const baseUrl = $('#extensions_url').val(); - extension_settings.apiUrl = String(baseUrl); + const baseUrl = String($('#extensions_url').val()); + extension_settings.apiUrl = baseUrl; const testApiKey = $('#extensions_api_key').val(); extension_settings.apiKey = String(testApiKey); saveSettingsDebounced(); @@ -423,21 +449,11 @@ function notifyUpdatesInputHandler() { } } -/* $(document).on('click', function (e) { - const target = $(e.target); - if (target.is(dropdown)) return; - if (target.is(button) && dropdown.is(':hidden')) { - dropdown.toggle(200); - popper.update(); - } - if (target !== dropdown && - target !== button && - dropdown.is(":visible")) { - dropdown.hide(200); - } - }); -} */ - +/** + * Connects to the Extras API. + * @param {string} baseUrl Extras API base URL + * @returns {Promise} + */ async function connectToApi(baseUrl) { if (!baseUrl) { return; @@ -453,7 +469,7 @@ async function connectToApi(baseUrl) { const data = await getExtensionsResult.json(); modules = data.modules; await activateExtensions(); - eventSource.emit(event_types.EXTRAS_CONNECTED, modules); + await eventSource.emit(event_types.EXTRAS_CONNECTED, modules); } updateStatus(getExtensionsResult.ok); @@ -463,6 +479,10 @@ async function connectToApi(baseUrl) { } } +/** + * Updates the status of Extras API connection. + * @param {boolean} success Whether the connection was successful + */ function updateStatus(success) { connectedToApi = success; const _text = success ? t`Connected to API` : t`Could not connect to API`; @@ -977,7 +997,7 @@ export async function installExtension(url, global) { * @param {boolean} versionChanged Is this a version change? * @param {boolean} enableAutoUpdate Enable auto-update */ -async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) { +export async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) { if (settings.extension_settings) { Object.assign(extension_settings, settings.extension_settings); } @@ -1149,7 +1169,7 @@ async function autoUpdateExtensions(forceAll) { * @param {number} contextSize Context size * @returns {Promise} True if generation should be aborted */ -async function runGenerationInterceptors(chat, contextSize) { +export async function runGenerationInterceptors(chat, contextSize) { let aborted = false; let exitImmediately = false; From 0638953a201ca54e674a7fbe1cc6402137a22988 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:34:01 +0200 Subject: [PATCH 014/222] Clean-up debug logs --- public/scripts/filters.js | 1 - public/scripts/power-user.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/scripts/filters.js b/public/scripts/filters.js index f07b4e33f..eb5329d8a 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -448,6 +448,5 @@ export class FilterHelper { for (const cache of Object.values(this.fuzzySearchCaches)) { cache.resultMap.clear(); } - console.log('All fuzzy search caches cleared'); } } diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 4c17b10de..7ebf2ffce 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -1756,7 +1756,7 @@ async function loadContextSettings() { } else { $element.val(power_user.context[control.property]); } - console.log(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`); + console.debug(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`); // If the setting already exists, no need to duplicate it // TODO: Maybe check the power_user object for the setting instead of a flag? @@ -1767,7 +1767,7 @@ async function loadContextSettings() { } else { power_user.context[control.property] = value; } - console.log(`Setting ${$element.prop('id')} to ${value}`); + console.debug(`Setting ${$element.prop('id')} to ${value}`); if (!CSS.supports('field-sizing', 'content') && $(this).is('textarea')) { await resetScrollHeight($(this)); } From 1ecc65f5fecb057e2fa67dadffc15475649330a6 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:49:21 +0200 Subject: [PATCH 015/222] Log failed image decoding --- src/endpoints/characters.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 9c117cb31..eebabea59 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -158,7 +158,8 @@ async function tryReadImage(imgPath, crop) { return image; } // If it's an unsupported type of image (APNG) - just read the file as buffer - catch { + catch (error) { + console.log(`Failed to read image: ${imgPath}`, error); return fs.readFileSync(imgPath); } } From e21931b6eb37c0e56b321ce23e3eff445446575a Mon Sep 17 00:00:00 2001 From: Rivelle Date: Sun, 8 Dec 2024 18:27:44 +0800 Subject: [PATCH 016/222] Update zh-tw.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translated previously incomplete sections. Refined certain terms for improved professionalism and neutrality. Key adjustments include: 1. The previous translator rendered "Persona" as "玩家角色" (player character). This has been revised to "使用者" (user) to avoid giving SillyTavern the impression of being a game. 2. Simplified "角色人物卡" (character profile card) to "角色卡" (character card), as current character cards may represent non-human entities, non-living objects, or even settings. Removing "人物" (character) does not impact clarity. 3. Unified the translation of "Tokenizer" as "分詞器" (tokenizer), which is more common in professional discussions in Traditional Chinese. 4. Translated "Global" as "全域" (global), aligning better with technical terminology. 5. Revised certain translations to better match their intended functionalities, such as: - "Reduced Motion" is more accurately described as "減少介面的動畫效果" (reduce interface animations). - "Auto-Expand Message Actions" is now "展開訊息快速編輯選單工具" (expand quick edit menu for messages). --- public/locales/zh-tw.json | 757 +++++++++++++++++++++++++++----------- 1 file changed, 551 insertions(+), 206 deletions(-) diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index 66d2d06b6..dcaee9e25 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -2,9 +2,9 @@ "Favorite": "我的最愛", "Tag": "標籤", "Duplicate": "複製", - "Persona": "玩家角色人物", + "Persona": "使用者角色", "Delete": "刪除", - "AI Response Configuration": "AI 回應配置", + "AI Response Configuration": "設定 AI 回應", "AI Configuration panel will stay open": "上鎖 = AI 設定面板將保持開啟", "clickslidertips": "點選滑桿下的數字可手動輸入。", "MAD LAB MODE ON": "瘋狂實驗室模式", @@ -30,10 +30,10 @@ "response legth(tokens)": "回應長度(符記數)", "Streaming": "串流", "Streaming_desc": "生成時逐位顯示回應。當此功能關閉時,回應將在完成後一次顯示。", - "context size(tokens)": "上下文大小(符記數)", + "context size(tokens)": "上下文長度(符記數)", "unlocked": "解鎖", - "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過8192個符記的上下文大小時啟用此功能", - "Max prompt cost:": "最多提示詞費用", + "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符記的上下文長度時啟用此功能", + "Max prompt cost:": "最大提示詞費用", "Display the response bit by bit as it is generated.": "生成時逐位顯示回應。", "When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在完成後一次性顯示。", "Temperature": "溫度", @@ -50,9 +50,9 @@ "Medium": "中等", "Aggressive": "積極", "Very aggressive": "非常積極", - "Unlocked Context Size": "解鎖的上下文大小", + "Unlocked Context Size": "解鎖的上下文長度", "Unrestricted maximum value for the context slider": "上下文滑桿的無限制最大值", - "Context Size (tokens)": "上下文大小(符記數)", + "Context Size (tokens)": "上下文長度(符記數)", "Max Response Length (tokens)": "最大回應長度(符記數)", "Multiple swipes per generation": "每次生成多次滑動", "Enable OpenAI completion streaming": "啟用 OpenAI 補充串流", @@ -80,7 +80,7 @@ "Scenario Format Template": "情境格式範本", "Personality Format Template": "個性格式範本", "Group Nudge Prompt Template": "群組推動提示詞範本", - "Sent at the end of the group chat history to force reply from a specific character.": "在群組聊天歷史結束時發送以強制特定角色人物回覆", + "Sent at the end of the group chat history to force reply from a specific character.": "在群組聊天歷史結束時發送以強制特定角色回覆", "New Chat": "新聊天", "Restore new chat prompt": "還原新聊天的提示詞", "Set at the beginning of the chat history to indicate that a new chat is about to start.": "設定在聊天歷史的開頭以表明即將開始新的聊天", @@ -91,10 +91,10 @@ "Set at the beginning of Dialogue examples to indicate that a new example chat is about to start.": "設定在對話範例的開頭以表明即將開始新的範例聊天", "Continue nudge": "繼續輔助提示詞", "Set at the end of the chat history when the continue button is pressed.": "按下繼續按鈕時設定在聊天歷史的末尾", - "Replace empty message": "取代空訊息", + "Replace empty message": "取代空白訊息", "Send this text instead of nothing when the text box is empty.": "當文字方塊為空時,發送此字串而不是空白。", "Seed": "種子", - "Set to get deterministic results. Use -1 for random seed.": "設置以獲取確定性結果。使用 -1 作為隨機種子", + "Set to get deterministic results. Use -1 for random seed.": "設定以獲取確定性結果。使用 -1 作為隨機種子", "Temperature controls the randomness in token selection": "溫度控制符記選擇中的隨機性", "Top_K_desc": "Top K 設定可以選擇的最高符記數量。\n例如,Top K 為 20,這意味著只保留排名前 20 的符記(無論它們的機率是多樣還是有限的)。\n設定為 0 以停用。", "Top_P_desc": "Top P(又名核心取樣)會將所有頂級符記加總,直到達到目標百分比。\n例如,如果前兩個符記都是 25%,而 Top P 設為 0.5,那麼只有前兩個符記會被考慮。\n設定為 1.0 以停用。", @@ -129,7 +129,7 @@ "CFG Scale": "CFG 比例", "Negative Prompt": "負面提示詞", "Add text here that would make the AI generate things you don't want in your outputs.": "在這裡新增文字,使 AI 生成您不希望在輸出中出現的內容。", - "Used if CFG Scale is unset globally, per chat or character": "如果CFG Scale未在全域、每個聊天或角色人物中設定,則使用", + "Used if CFG Scale is unset globally, per chat or character": "如果CFG Scale未在全域、每個聊天或角色中設定,則使用", "Mirostat Tau": "Tau", "Mirostat LR": "Mirostat 學習率", "Min Length": "最小長度", @@ -147,7 +147,7 @@ "Eta_Cutoff_desc": "Eta 截斷是特殊 Eta 取樣技術的主要參數。\n單位為 1e-4;合理值為 3。\n設為 0 以停用。\n詳情請參見 Hewitt 等人(2022)撰寫的論文《Truncation Sampling as Language Model Desmoothing》。", "rep.pen decay": "重複懲罰衰減", "Encoder Rep. Pen.": "編碼器重複懲罰", - "No Repeat Ngram Size": "無重複Ngram大小", + "No Repeat Ngram Size": "無重複 Ngram 大小", "Skew": "Skew", "Max Tokens Second": "最大符記/秒", "Smooth Sampling": "平滑取樣", @@ -165,7 +165,7 @@ "Penalty Range": "懲罰範圍", "DRY_Sequence_Breakers_desc": "序列停止繼續配對的符記,使用引號包裹字串並以逗號分隔清單。", "Sequence Breakers": "序列中斷器", - "JSON-serialized array of strings.": "序列化JSON的字串陣列。", + "JSON-serialized array of strings.": "序列化 JSON 的字串陣列。", "Dynamic Temperature": "動態溫度", "Scale Temperature dynamically per token, based on the variation of probabilities": "根據機率變化,動態調整每個符記的溫度", "Minimum Temp": "最低溫度", @@ -177,15 +177,15 @@ "Variability parameter for Mirostat outputs": "Mirostat 輸出的變異性參數", "Mirostat Eta": "Eta", "Learning rate of Mirostat": "Mirostat 的學習率", - "Beam search": "波束搜尋", + "Beam search": "Beam search(波束搜尋)", "Helpful tip coming soon.": "更多有用提示訊息即將推出。", "Number of Beams": "波束數量", "Length Penalty": "長度懲罰", "Early Stopping": "提前停止", "Contrastive search": "對比搜尋", - "Contrastive_search_txt": "一種取樣器,通過利用大多數 LLM 的表示空間的等向性,鼓勵多樣性的同時保持一致性。詳情請參閱 Su 等人於2022年發表的論文《A Contrastive Framework for Neural Text Generation》。", + "Contrastive_search_txt": "一種取樣器,通過利用大多數 LLM 的表示空間的等向性,鼓勵多樣性的同時保持一致性。詳情請參閱 Su 等人於 2022 年發表的論文《A Contrastive Framework for Neural Text Generation》。", "Penalty Alpha": "懲罰 Alpha", - "Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "對比搜尋正則化項的強度。設定為0以停用CS", + "Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "對比搜尋正則化項的強度。設定為 0 以停用CS", "Do Sample": "進行取樣", "Add BOS Token": "新增 BOS 符記", "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "在提示詞的開頭新增 bos_token。停用此功能可以使回應更具創造性", @@ -211,16 +211,16 @@ "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。", "Sampler Priority": "取樣器優先順序", "Ooba only. Determines the order of samplers.": "僅適用於 Ooba。決定取樣器的順序。", - "Character Names Behavior": "角色人物名稱行為", - "Helps the model to associate messages with characters.": "幫助模型將訊息與角色人物關聯起來。", + "Character Names Behavior": "角色名稱行為", + "Helps the model to associate messages with characters.": "幫助模型將訊息與角色關聯起來。", "None": "無", - "character_names_default": "除了團體和過去的玩家角色人物外。否則,請確保在提示中提供名字。", - "Don't add character names.": "不要新增角色人物名稱", + "character_names_default": "除了團體和過去的使用者角色外。否則,請確保在提示中提供名字。", + "Don't add character names.": "不要新增角色名稱", "Completion": "補充", "character_names_completion": "字元限制:僅限拉丁字母數字和底線。不適用於所有來源,特別是:Claude、MistralAI、Google。", - "Add character names to completion objects.": "新增角色人物名稱來補充物件", + "Add character names to completion objects.": "新增角色名稱來補充物件", "Message Content": "訊息內容", - "Prepend character names to message contents.": "在訊息內容前新增角色人物名稱", + "Prepend character names to message contents.": "在訊息內容前新增角色名稱", "Continue Postfix": "繼續後綴", "The next chunk of the continued message will be appended using this as a separator.": "繼續訊息的下一塊將使用此作為分隔符附加", "Space": "空格", @@ -233,7 +233,7 @@ "Continue prefill": "繼續預先填充", "Continue sends the last message as assistant role instead of system message with instruction.": "繼續將最後的訊息作為助理角色發送,而不是帶有指令的系統訊息。", "Squash system messages": "合併系統訊息", - "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括範例對話)。可能會提高某些模型的一致性。", + "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括對話範例)。可能會提高某些模型的一致性。", "Enable function calling": "啟用函數調用", "Send inline images": "發送內嵌圖片", "image_inlining_hint_1": "如果模型支援(例如: GPT-4V、Claude 3 或 Llava 13B),則在提示詞中發送圖片。\n使用任何訊息上的", @@ -243,26 +243,26 @@ "openai_inline_image_quality_auto": "自動", "openai_inline_image_quality_low": "低", "openai_inline_image_quality_high": "高", - "Use AI21 Tokenizer": "使用 AI21 符記器", - "Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.": "對於 Jurassic 模型使用適當的符記器,比 GPT 的更高效", - "Use Google Tokenizer": "使用 Google 符記器", - "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "通過 Google 模型的 API 使用適當的符記器。提示詞處理速度較慢,但提供更準確的符記計數。", + "Use AI21 Tokenizer": "使用 AI21 分詞器", + "Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.": "對於 Jurassic 模型使用適當的分詞器,比 GPT 的更高效", + "Use Google Tokenizer": "使用 Google 分詞器", + "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "通過 Google 模型的 API 使用適當的分詞器。提示詞處理速度較慢,但提供更準確的符記計數。", "Use system prompt": "使用系統提示詞", "(Gemini 1.5 Pro/Flash only)": "(僅限於 Gemini 1.5 Pro/Flash)", - "Merges_all_system_messages_desc_1": "合併所有系統訊息,直到第一條非系統角色的訊息,並通過 google 的", + "Merges_all_system_messages_desc_1": "合併所有系統訊息,直到第一則非系統角色的訊息,並通過 google 的", "Merges_all_system_messages_desc_2": "字段發送,而不是與其餘提示詞內容一起發送。", "Assistant Prefill": "預先填充助理訊息", "Start Claude's answer with...": "開始 Claude 的回答...", "Assistant Impersonation Prefill": "助理扮演時的預先填充", "Use system prompt (Claude 2.1+ only)": "使用系統提示詞(僅限 Claude 2.1+)", "Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.": "為支援的模型發送系統提示詞。停用時,使用者訊息將新增到提示詞的開頭。", - "User first message": "使用者第一條訊息", - "Restore User first message": "還原使用者第一個訊息", + "User first message": "使用者第一則訊息", + "Restore User first message": "還原使用者第一則訊息", "Human message": "人類訊息、指令等。\n當空白時不加入任何內容,也就是需要一個帶有使用者角色的新提示詞。", "New preset": "新預設", "Delete preset": "刪除預設", - "View / Edit bias preset": "查看/編輯偏見預設", - "Add bias entry": "新增偏見條目", + "View / Edit bias preset": "查看/編輯 Bias 預設", + "Add bias entry": "新增 Bias 條目", "Most tokens have a leading space.": "大多數符記有前導空格", "API Connections": "API 連線", "Text Completion": "文字補充", @@ -274,14 +274,14 @@ "Review the Privacy statement": "檢視隱私聲明", "Register a Horde account for faster queue times": "註冊Horde帳號以縮短排隊時間", "Learn how to contribute your idle GPU cycles to the Horde": "了解如何將閒置的 GPU 週期貢獻給 Horde", - "Adjust context size to worker capabilities": "根據 worker 的能力調整上下文大小", + "Adjust context size to worker capabilities": "根據 worker 的能力調整上下文長度", "Adjust response length to worker capabilities": "根據 worker 的能力調整回應長度", "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "僅將已批准的 worker 排隊,可以幫助處理不良回應。可能會延長回應時間。", "Trusted workers only": "僅限受信任的 worker", "API key": "API 金鑰", "Get it here:": "在這裡獲取:", - "Register": "登記", - "View my Kudos": "查看我的稱讚", + "Register": "註冊", + "View my Kudos": "瀏覽我的讚賞記錄", "Enter": "輸入", "to use anonymous mode.": "以使用匿名模式。", "Clear your API key": "清除您的 API 金鑰", @@ -372,7 +372,7 @@ "Model Order": "模型順序", "Alphabetically": "按字母順序", "Price": "價格(最便宜的)", - "Context Size": "上下文大小", + "Context Size": "上下文長度", "Group by vendors": "按供應商分組", "Group by vendors Description": "將 OpenAI 、 Anthropic 等等的模型放各自供應商的群組中。可以與排序功能結合使用。", "Allow fallback routes": "允許備援路徑", @@ -412,7 +412,7 @@ "Chat Start": "聊天開始符號", "Add Chat Start and Example Separator to a list of stopping strings.": "將聊天開始和範例分隔符號加入終止字串中。", "Use as Stop Strings": "用作停止字串", - "context_allow_jailbreak": "如果在角色人物卡中定義了越獄,且啟用了「偏好角色人物卡越獄」,則會在提示詞的結尾加入越獄內容。\n這不建議用於文字完成模型,因為可能導致不良的輸出結果。", + "context_allow_jailbreak": "如果在角色卡中定義了越獄,且啟用了「偏好角色卡越獄」,則會在提示詞的結尾加入越獄內容。\n這不建議用於文字完成模型,因為可能導致不良的輸出結果。", "Allow Jailbreak": "允許越獄", "Context Order": "上下文順序", "Summary": "摘要", @@ -429,9 +429,9 @@ "Activation Regex": "啟用正規表示式", "Wrap Sequences with Newline": "用換行符包裹序列", "Replace Macro in Sequences": "取代序列中的巨集", - "Skip Example Dialogues Formatting": "跳過範例對話的格式設定", + "Skip Example Dialogues Formatting": "跳過對話範例的格式設定", "Include Names": "包含名稱", - "Force for Groups and Personas": "強制用於群組和玩家角色人物", + "Force for Groups and Personas": "強制用於群組和使用者角色", "System Prompt": "系統提示詞", "Instruct Mode Sequences": "指示模式序列", "System Prompt Wrapping": "系統提示詞換行", @@ -448,7 +448,7 @@ "Assistant Message Prefix": "助理訊息前綴", "Inserted after an Assistant message.": "插入在助理訊息之後。", "Assistant Message Suffix": "助理訊息後綴", - "Inserted before a System (added by slash commands or extensions) message.": "插入在系統(透過斜線命令或擴充套件增加)訊息之前。", + "Inserted before a System (added by slash commands or extensions) message.": "插入在系統(透過斜線命令或擴充功能增加)訊息之前。", "System Message Prefix": "系統訊息前綴", "Inserted after a System message.": "插入在系統訊息之後", "System Message Suffix": "系統訊息後綴", @@ -467,14 +467,14 @@ "User Filler Message": "使用者填充訊息", "Context Formatting": "上下文格式", "(Saved to Context Template)": "(已儲存到上下文範本)", - "Always add character's name to prompt": "總是將角色人物名稱新增到提示詞中", + "Always add character's name to prompt": "總是將角色名稱新增到提示詞中", "Generate only one line per request": "每次請求僅生成一行", "Trim Incomplete Sentences": "修剪不完整的句子", "Include Newline": "包含換行符號", "Misc. Settings": "其他設定", "Collapse Consecutive Newlines": "折疊連續的換行符號", "Trim spaces": "修剪空格", - "Tokenizer": "符記化工具", + "Tokenizer": "分詞器(Tokenizer)", "Token Padding": "符記填充", "Start Reply With": "開始回覆", "AI reply prefix": "AI 回覆前綴", @@ -492,7 +492,7 @@ "Worlds/Lorebooks": "世界/知識書", "Active World(s) for all chats": "所有聊天啟用中的世界書", "-- World Info not found --": "-- 未找到世界資訊 --", - "Global World Info/Lorebook activation settings": "全球世界資訊/傳說書啟動設置", + "Global World Info/Lorebook activation settings": "全域世界資訊/傳說書啟動設定", "Click to expand": "點擊展開", "Scan Depth": "掃描深度", "Context %": "上下文百分比", @@ -504,7 +504,7 @@ "(0 = unlimited, use budget)": "(0 = 無限制, 使用預算)", "Insertion Strategy": "插入策略", "Sorted Evenly": "均等排序", - "Character Lore First": "角色人物知識書優先", + "Character Lore First": "角色知識書優先", "Global Lore First": "全域知識書優先", "Entries can activate other entries by mentioning their keywords": "條目可以通過提及其關鍵字來啟用其他條目", "Recursive Scan": "遞迴掃描", @@ -553,12 +553,12 @@ "Admin Panel": "管理面板", "Logout": "登出", "Search Settings": "搜尋設定", - "UI Theme": "介面佈景主題", - "Import a theme file": "匯入布景主題檔案", - "Export a theme file": "匯出布景主題檔案", + "UI Theme": "介面主題", + "Import a theme file": "匯入主題檔案", + "Export a theme file": "匯出主題檔案", "Delete a theme": "刪除主題", "Update a theme file": "更新主題檔案", - "Save as a new theme": "儲存成新的布景主題", + "Save as a new theme": "儲存成新的主題", "Avatar Style": "頭像樣式", "Circle": "圓形", "Square": "方形", @@ -568,7 +568,7 @@ "Bubbles": "氣泡", "Document": "文件", "Specify colors for your theme.": "為您的主題指定顏色", - "Theme Colors": "佈景主題顏色", + "Theme Colors": "介面主題顏色", "Main Text": "主要文字", "Italics Text": "斜體文字", "Underlined Text": "帶底線的文字", @@ -588,15 +588,15 @@ "Text Shadow Width": "文字陰影寬度", "Strength of the text shadows": "文字陰影的強度", "Disables animations and transitions": "停用動畫和過渡效果", - "Reduced Motion": "減少動作", + "Reduced Motion": "減少動畫效果", "removes blur from window backgrounds": "從視窗背景中移除模糊效果,以加快算繪速度。", "No Blur Effect": "無模糊效果", "Remove text shadow effect": "移除文字陰影效果。", "No Text Shadows": "無文字陰影", "Reduce chat height, and put a static sprite behind the chat window": "減少聊天高度,並在聊天視窗後放置一個靜態 Sprite。", - "Waifu Mode": "Waifu 模式", + "Waifu Mode": "視覺小說模式", "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "始終顯示聊天訊息的訊息動作上下文項目的完整列表,而不是將它們隱藏在「...」後面。", - "Auto-Expand Message Actions": "自動展開訊息動作", + "Auto-Expand Message Actions": "展開訊息快速編輯選單", "Alternative UI for numeric sampling parameters with fewer steps": "數值取樣參數的替代 UI,步驟更少", "Zen Sliders": "Zen 滑桿", "Entirely unrestrict all numeric sampling parameters": "完全解除所有數值取樣參數的限制。", @@ -615,32 +615,32 @@ "Show Message Token Count": "顯示訊息符記數量", "Single-row message input area. Mobile only, no effect on PC": "單行訊息輸入區域。僅適用於行動裝置版。", "Compact Input Area (Mobile)": "緊湊的輸入區域(行動版)", - "In the Character Management panel, show quick selection buttons for favorited characters": "在角色人物管理面板中,顯示加入到最愛角色人物的快速選擇按鈕。", - "Characters Hotswap": "角色人物快速選擇", + "In the Character Management panel, show quick selection buttons for favorited characters": "在角色管理面板中,顯示加入到最愛角色的快速選擇按鈕。", + "Characters Hotswap": "角色卡快捷選單", "Enable magnification for zoomed avatar display.": "啟用放大顯示頭像", "Avatar Hover Magnification": "頭像懸停時的放大倍數", "Enables a magnification effect on hover when you display the zoomed avatar after clicking an avatar's image in chat.": "當你在聊天中點選頭像的圖片後,這會啟用滑鼠懸停時的放大效果。", - "Show tagged character folders in the character list": "在角色人物列表中顯示標籤角色人物資料夾。", + "Show tagged character folders in the character list": "在角色列表中顯示標籤角色資料夾。", "Tags as Folders": "標籤作為資料夾", "Tags_as_Folders_desc": "最近更改:標籤必須在標籤管理選單中標記為資料夾才能顯示為此類。點選這裡打開。", - "Character Handling": "角色人物處理", - "If set in the advanced character definitions, this field will be displayed in the characters list.": "如果在進階角色人物定義中設定,這個欄位將顯示在角色人物清單中。", - "Char List Subheader": "角色人物列表子標題", - "Character Version": "角色人物版本", + "Character Handling": "角色處理", + "If set in the advanced character definitions, this field will be displayed in the characters list.": "如果在進階角色定義中設定,這個欄位將顯示在角色清單中。", + "Char List Subheader": "角色列表子標題", + "Character Version": "角色版本", "Created by": "建立者", - "Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊配對,並通過所有資料欄位在列表中搜尋角色人物,而不僅僅是通過名稱子字串。", - "Advanced Character Search": "進階角色人物搜尋", - "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果選中並且角色人物卡包含提示詞覆寫(系統提示詞),則使用該提示詞。", - "Prefer Character Card Prompt": "偏好角色人物卡提示詞", - "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "如果選中並且角色人物卡包含越獄覆寫(歷史指示後),則使用該提示詞。", - "Prefer Character Card Jailbreak": "偏好角色人物卡越獄", - "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和調整匯入的角色人物圖像大小。未勾選時將會裁剪/調整大小到 512x768。", + "Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊配對,並通過所有資料欄位在列表中搜尋角色,而不僅僅是通過名稱子字串。", + "Advanced Character Search": "進階角色搜尋", + "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果選中並且角色卡包含提示詞覆寫(系統提示詞),則使用該提示詞。", + "Prefer Character Card Prompt": "偏好角色卡提示詞", + "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "如果選中並且角色卡包含越獄覆寫(歷史指示後),則使用該提示詞。", + "Prefer Character Card Jailbreak": "偏好角色卡越獄", + "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和調整匯入的角色圖像大小。未勾選時將會裁剪/調整大小到 512x768。", "Never resize avatars": "永不調整頭像大小", - "Show actual file names on the disk, in the characters list display only": "僅在角色人物列表顯示實際檔案名稱。", + "Show actual file names on the disk, in the characters list display only": "僅在角色列表顯示實際檔案名稱。", "Show avatar filenames": "顯示頭像檔案名", - "Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored": "在角色人物匯入時提示詞匯入嵌入的卡片標籤。否則,嵌入的標籤將被忽略。", + "Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored": "在角色匯入時提示詞匯入嵌入的卡片標籤。否則,嵌入的標籤將被忽略。", "Import Card Tags": "匯入卡片中的標籤", - "Hide character definitions from the editor panel behind a spoiler button": "在編輯器面板中將角色人物定義隱藏在劇透按鈕後面。", + "Hide character definitions from the editor panel behind a spoiler button": "在編輯器面板中將角色定義隱藏在劇透按鈕後面。", "Spoiler Free Mode": "無劇透模式", "Miscellaneous": "其他", "Reload and redraw the currently open chat": "重新載入並重繪目前開啟的聊天", @@ -653,10 +653,10 @@ "Play a sound when a message generation finishes": "訊息生成完成時播放音效。", "Message Sound": "訊息音效", "Only play a sound when ST's browser tab is unfocused": "僅在 ST 的瀏覽器分頁未聚焦時播放音效。", - "Background Sound Only": "僅背景音效", + "Background Sound Only": "僅作為背景音效", "Reduce the formatting requirements on API URLs": "降低 API URL 的格式要求。", "Relaxed API URLS": "寬鬆的 API URL 格式", - "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "當新角色人物含有知識書時,詢問是否要匯入嵌入的世界資訊/知識書。如果未選中,則會顯示簡短的訊息。", + "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "當新角色含有知識書時,詢問是否要匯入嵌入的世界資訊/知識書。如果未選中,則會顯示簡短的訊息。", "Lorebook Import Dialog": "匯入知識書對話框", "Restore unsaved user input on page refresh": "在頁面刷新時還原未儲存的使用者輸入。", "Restore User Input": "還原使用者輸入", @@ -664,7 +664,7 @@ "Movable UI Panels": "可移動的 UI 面板", "MovingUI preset. Predefined/saved draggable positions": "MovingUI 預設。預先定義/儲存可拖動位置。", "MUI Preset": "MUI 預設", - "Save movingUI changes to a new file": "將移動UI變更儲存到新檔案", + "Save movingUI changes to a new file": "將移動 UI 變更儲存到新檔案", "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置", "Apply a custom CSS style to all of the ST GUI": "將自訂 CSS 樣式應用於所有 ST GUI", "Custom CSS": "自訂 CSS", @@ -683,7 +683,7 @@ "Disabled": "停用", "Automatic (PC)": "自動(PC)", "Press Send to continue": "按下傳送繼續", - "Show a button in the input area to ask the AI to continue (extend) its last message": "在輸入區域顯示一個按鈕,請求 AI 繼續(擴充)其最後一條訊息", + "Show a button in the input area to ask the AI to continue (extend) its last message": "在輸入區域顯示一個按鈕,請求 AI 繼續(擴充)其最後一則訊息", "Quick 'Continue' button": "快速「繼續」按鈕", "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在最後的聊天訊息中顯示箭頭按鈕,以生成替代的 AI 回應。適用於 PC 和行動裝置版", "Swipes": "滑動", @@ -722,60 +722,60 @@ "Starts with": "開始於", "Includes": "包含", "Fuzzy": "模糊", - "Sets the style of the autocomplete.": "設置自動完成的樣式", + "Sets the style of the autocomplete.": "設定自動完成的樣式", "Autocomplete Style": "自動完成樣式", - "Follow Theme": "參照佈景主題", + "Follow Theme": "參照介面主題", "Dark": "深色", - "Sets the font size of the autocomplete.": "設置自動完成的字體大小", - "Sets the width of the autocomplete.": "設置自動完成的寬度", + "Sets the font size of the autocomplete.": "設定自動完成的字體大小", + "Sets the width of the autocomplete.": "設定自動完成的寬度", "Autocomplete Width": "自動完成寬度", "chat input box": "聊天輸入框", "entire chat width": "整個聊天寬度", "full window width": "全視窗寬度", "STscript Settings": "STscript 設定", - "Sets default flags for the STscript parser.": "設置 STscript 解析器的預設標誌", + "Sets default flags for the STscript parser.": "設定 STscript 解析器的預設標誌", "Parser Flags": "解析器標誌", "Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "切換到更嚴格的字元跳脫,允許所有分隔符號使用反斜線跳脫,反斜線自己也可以跳脫。", "STRICT_ESCAPING": "STRICT_ESCAPING", "Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.": "將所有 {{getvar::}} 和 {{getglobalvar::}} 巨集取代為作用域變量以避免雙重巨集取代", "REPLACE_GETVAR": "REPLACE_GETVAR", - "Change Background Image": "更換背景圖片", + "Change Background Image": "變更背景圖片", "Filter": "篩選", "Automatically select a background based on the chat context": "根據聊天上下文自動選擇背景", "Auto-select": "自動選擇", "System Backgrounds": "系統背景圖片", "Chat Backgrounds": "聊天背景圖片", - "bg_chat_hint_1": "使用擴充套件", + "bg_chat_hint_1": "使用擴充功能", "bg_chat_hint_2": "所產生的背景圖片將會出現在這裡。", - "Extensions": "擴充套件", - "Notify on extension updates": "擴充套件更新時通知", - "Manage extensions": "管理擴充套件", - "Import Extension From Git Repo": "從 Git 倉庫匯入擴充套件", - "Install extension": "安裝擴充套件", + "Extensions": "擴充功能", + "Notify on extension updates": "擴充功能更新時通知", + "Manage extensions": "管理擴充功能", + "Import Extension From Git Repo": "從 Git 倉庫匯入擴充功能", + "Install extension": "安裝擴充功能", "Extras API:": "Extras API:", "Auto-connect": "自動連線", "Extras API URL": "Extras API URL", "Extras API key (optional)": "Extras API 金鑰(選填)", - "Persona Management": "玩家角色人物管理", + "Persona Management": "使用者角色管理", "How do I use this?": "我該如何使用這個?", "Click for stats!": "點選查看統計!", "Usage Stats": "使用統計", - "Backup your personas to a file": "將您的玩家角色人物備份到檔案中", + "Backup your personas to a file": "將您的使用者角色備份到檔案中", "Backup": "備份", - "Restore your personas from a file": "從檔案還原您的角色人物", + "Restore your personas from a file": "從檔案還原您的角色", "Restore": "還原", - "Create a dummy persona": "建立一個假玩家角色人物", + "Create a dummy persona": "建立一個虛構使用者角色", "Create": "建立", "Toggle grid view": "切換網格視圖", - "No persona description": "無玩家角色人物描述", + "No persona description": "無使用者角色描述", "Name": "名稱", "Enter your name": "輸入您的名字", - "Click to set a new User Name": "點選以設置新使用者名稱", - "Click to lock your selected persona to the current chat. Click again to remove the lock.": "點選以將所選玩家角色人物上鎖到目前聊天。再次點選以移除上鎖。", - "Click to set user name for all messages": "點選以設置所有訊息的使用者名稱", - "Persona Description": "玩家角色人物描述", + "Click to set a new User Name": "點選以設定新使用者名稱", + "Click to lock your selected persona to the current chat. Click again to remove the lock.": "點選以將所選使用者角色上鎖到目前聊天。再次點選以移除上鎖。", + "Click to set user name for all messages": "點選以設定所有訊息的使用者名稱", + "Persona Description": "使用者角色描述", "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "範例:[{{user}} 是一個 28 歲的羅馬尼亞貓娘。]", - "Tokens persona description": "符記角色人物描述", + "Tokens persona description": "符記角色描述", "Position:": "位置:", "In Story String / Prompt Manager": "在故事字串 / 提示詞管理器", "Top of Author's Note": "作者備註的頂部", @@ -786,11 +786,11 @@ "System": "系統", "User": "使用者", "Assistant": "助理", - "Show notifications on switching personas": "切換角色人物時顯示通知", - "Character Management": "角色人物管理", - "Locked = Character Management panel will stay open": "上鎖 = 角色人物管理面板將保持開啟", - "Select/Create Characters": "選擇/建立角色人物", - "Favorite characters to add them to HotSwaps": "將角色人物加入到最愛來新增到快速切換", + "Show notifications on switching personas": "切換角色時顯示通知", + "Character Management": "角色管理", + "Locked = Character Management panel will stay open": "上鎖 = 角色管理面板將保持開啟", + "Select/Create Characters": "選擇/建立角色", + "Favorite characters to add them to HotSwaps": "將角色加入到最愛來新增到快速切換", "Token counts may be inaccurate and provided just for reference.": "符記計數可能不準確,僅供參考。", "Total tokens": "符記總計", "Calculating...": "計算中...", @@ -798,23 +798,23 @@ "Permanent tokens": "永久符記", "Permanent": "永久", "About Token 'Limits'": "關於符記數「限制」", - "Toggle character info panel": "切換角色人物資訊面板", - "Name this character": "為此角色人物命名", + "Toggle character info panel": "切換角色資訊面板", + "Name this character": "為此角色命名", "extension_token_counter": "符記:", - "Click to select a new avatar for this character": "點選以選擇此角色人物的新頭像", + "Click to select a new avatar for this character": "點選以選擇此角色的新頭像", "Add to Favorites": "新增至我的最愛", "Advanced Definition": "進階定義", - "Character Lore": "角色人物知識書", + "Character Lore": "角色知識書", "Chat Lore": "聊天知識", "Export and Download": "匯出和下載", - "Duplicate Character": "複製角色人物", - "Create Character": "建立角色人物", - "Delete Character": "刪除角色人物", + "Duplicate Character": "複製角色", + "Create Character": "建立角色", + "Delete Character": "刪除角色", "More...": "更多...", "Link to World Info": "連結到世界資訊", "Import Card Lore": "匯入卡片中的知識書", "Scenario Override": "情境覆寫", - "Convert to Persona": "轉換為玩家角色人物", + "Convert to Persona": "轉換為使用者角色", "Rename": "重新命名", "Link to Source": "連結到來源", "Replace / Update": "取代 / 更新", @@ -822,15 +822,15 @@ "Search / Create Tags": "搜尋/建立標籤", "View all tags": "查看所有標籤", "Creator's Notes": "建立者備註", - "Show / Hide Description and First Message": "顯示/隱藏描述和第一條訊息", - "Character Description": "角色人物描述", - "Click to allow/forbid the use of external media for this character.": "點選以允許/禁止此角色人物使用外部媒體", + "Show / Hide Description and First Message": "顯示/隱藏描述和第一則訊息", + "Character Description": "角色描述", + "Click to allow/forbid the use of external media for this character.": "點選以允許/禁止此角色使用外部媒體", "Ext. Media": "外部媒體權限", - "Describe your character's physical and mental traits here.": "在此描述角色人物的身體和心理特徵。", + "Describe your character's physical and mental traits here.": "在此描述角色的身體和心理特徵。", "First message": "初始訊息", - "Click to set additional greeting messages": "點選以設置額外的問候訊息", + "Click to set additional greeting messages": "點選以設定額外的問候訊息", "Alt. Greetings": "額外問候語", - "This will be the first message from the character that starts every chat.": "這將是每次聊天開始時角色人物發送的第一條訊息。", + "This will be the first message from the character that starts every chat.": "這將是每次聊天開始時角色發送的第一則訊息。", "Group Controls": "群組控制", "Chat Name (Optional)": "聊天名稱(選填)", "Click to select a new avatar for this group": "點選以選擇此群組的新頭像", @@ -838,9 +838,9 @@ "Natural order": "自然順序", "List order": "清單順序", "Group generation handling mode": "群組生成處理模式", - "Swap character cards": "交換角色人物卡", - "Join character cards (exclude muted)": "加入角色人物卡(排除靜音)", - "Join character cards (include muted)": "加入角色人物卡(包括靜音)", + "Swap character cards": "交換角色卡", + "Join character cards (exclude muted)": "加入角色卡(排除靜音)", + "Join character cards (include muted)": "加入角色卡(包括靜音)", "Inserted before each part of the joined fields.": "插入在合併欄位的每一部分之前。", "Join Prefix": "加入前綴", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and with the name of the part (e.g.: description, personality, scenario, etc.)": "當選擇“加入角色卡”時,角色的所有對應欄位將連接在一起。\r這意味著,例如在故事字串中,所有角色描述都將連接到一個大文字中。\r如果您希望分隔這些字段,可以在此處定義前綴或後綴。\r\r該值支援普通宏,並且還將 {{char}} 替換為相關字元的名稱,並將 替換為部分的名稱(例如:描述、個性、場景等)", @@ -855,11 +855,11 @@ "Hide Muted Member Sprites": "隱藏靜音成員精靈", "Current Members": "目前成員", "Add Members": "新增成員", - "Create New Character": "建立新角色人物", - "Import Character from File": "從檔案匯入角色人物", + "Create New Character": "建立新角色", + "Import Character from File": "從檔案匯入角色", "Import content from external URL": "從外部 URL 匯入內容", "Create New Chat Group": "建立新聊天群組", - "Characters sorting order": "角色人物排序依據", + "Characters sorting order": "角色排序依據", "A-Z": "A-Z", "Z-A": "Z-A", "Newest": "最新", @@ -871,10 +871,10 @@ "Most tokens": "最多符記", "Least tokens": "最少符記", "Random": "隨機", - "Toggle character grid view": "切換角色人物網格視圖", - "Bulk_edit_characters": "批量編輯角色人物\n\n點選以切換角色人物\nShift + 點選以選擇/取消選擇一範圍的角色人物\n右鍵以查看動作", - "Bulk select all characters": "全選所有角色人物", - "Bulk delete characters": "批量刪除角色人物", + "Toggle character grid view": "切換角色網格視圖", + "Bulk_edit_characters": "批量編輯角色\n\n點選以切換角色\nShift + 點選以選擇/取消選擇一範圍的角色\n右鍵以查看動作", + "Bulk select all characters": "全選所有角色", + "Bulk delete characters": "批量刪除角色", "popup-button-save": "儲存", "popup-button-yes": "是", "popup-button-no": "否", @@ -885,37 +885,37 @@ "(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)", "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。", "Main Prompt": "主要提示詞", - "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此處的任何內容將取代此角色人物使用的預設主要提示詞。(v2 規範:system_prompt)", - "Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "此處的任何內容將取代此角色人物使用的預設越獄提示詞。(v2 規範:post_history_instructions)", + "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此處的任何內容將取代此角色使用的預設主要提示詞。(v2 規範:system_prompt)", + "Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "此處的任何內容將取代此角色使用的預設越獄提示詞。(v2 規範:post_history_instructions)", "Creator's Metadata (Not sent with the AI prompt)": "建立者的中繼資料(不會與 AI 提示詞一起發送)", "Creator's Metadata": "建立者的中繼資料", "(Not sent with the AI Prompt)": "(不與 AI 提示詞一起發送)", "Everything here is optional": "此處所有內容均為選填", - "(Botmaker's name / Contact Info)": "(機器人製作者的名字 / 聯繫資訊)", - "(If you want to track character versions)": "(如果您想追蹤角色人物版本)", - "(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述機器人,提供使用技巧,或列出已測試的聊天模型。這將顯示在角色人物列表中。)", + "(Botmaker's name / Contact Info)": "(機器人建立者的名字 / 聯繫資訊)", + "(If you want to track character versions)": "(如果您想追蹤角色版本)", + "(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述機器人,提供使用技巧,或列出已測試的聊天模型。這將顯示在角色列表中。)", "Tags to Embed": "嵌入標籤", "(Write a comma-separated list of tags)": "(使用逗號分隔每個標籤)", "Personality summary": "個性摘要", "(A brief description of the personality)": "(個性的簡短描述)", "Scenario": "情境", - "(Circumstances and context of the interaction)": "(互動的情況和上下文)", - "Character's Note": "角色人物筆記", + "(Circumstances and context of the interaction)": "(互動情形與聊天背景)", + "Character's Note": "角色筆記", "(Text to be inserted in-chat @ designated depth and role)": "(要在聊天中插入的文字 @ 指定深度和角色)", "@ Depth": "@ 深度", "Role": "角色", "Talkativeness": "健談度", - "How often the character speaks in group chats!": "角色人物在群組聊天中說話的頻率!", + "How often the character speaks in group chats!": "角色在群組聊天中說話的頻率!", "How often the character speaks in": "角色說話的頻率", "group chats!": "群組聊天!", - "Shy": "害羞", + "Shy": "寡言", "Normal": "正常", "Chatty": "健談", "Examples of dialogue": "對話範例", - "Important to set the character's writing style.": "設定角色人物的寫作樣式很重要。", + "Important to set the character's writing style.": "設定角色的寫作樣式很重要。", "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天對話範例。每個範例以新的行並以「START」開始。)", "Save": "儲存", - "Chat History": "聊天紀錄", + "Chat History": "聊天記錄", "Import Chat": "匯入聊天", "Copy to system backgrounds": "複製到系統背景圖片", "Rename background": "重新命名背景圖片", @@ -930,12 +930,12 @@ "chat_world_template_txt": "選定的世界資訊將附加到此聊天。\n在生成 AI 回覆時,它將與全域和角色知識書中的條目結合。", "Select a World Info file for": "選擇世界資訊檔案", "Primary Lorebook": "主要知識書", - "A selected World Info will be bound to this character as its own Lorebook.": "選定的世界資訊將作為此角色人物的知識書附加到此角色人物。", + "A selected World Info will be bound to this character as its own Lorebook.": "選定的世界資訊將作為此角色的知識書附加到此角色。", "When generating an AI reply, it will be combined with the entries from a global World Info selector.": "生成 AI 回覆時,將與全域世界資訊選擇器中的條目結合。", - "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "匯出角色人物時,也會匯出嵌入 JSON 資料中的選定知識書檔案。", + "Exporting a character would also export the selected Lorebook file embedded in the JSON data.": "匯出角色時,也會匯出嵌入 JSON 資料中的選定知識書檔案。", "Additional Lorebooks": "額外的知識書", - "Associate one or more auxillary Lorebooks with this character.": "將一個或多個輔助知識書與此角色人物關聯。", - "NOTE: These choices are optional and won't be preserved on character export!": "注意:這些選項是選填的,在角色人物匯出時不會保留!", + "Associate one or more auxillary Lorebooks with this character.": "將一個或多個輔助知識書與此角色關聯。", + "NOTE: These choices are optional and won't be preserved on character export!": "注意:這些選項是選填的,在角色匯出時不會保留!", "Rename chat file": "重新命名聊天檔案", "Export JSONL chat file": "匯出 JSONL 聊天檔案", "Download chat as plain text document": "將聊天下載為純文字檔案", @@ -944,14 +944,14 @@ "Use tag as folder": "將標籤用作資料夾", "Delete tag": "刪除標籤", "Entry Title/Memo": "條目標題/備註", - "WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized❌ Disabled": "WI條目狀態:🔵常數🟢正常🔗向量❌停用", + "WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized❌ Disabled": "世界資訊條目狀態:🔵常數 🟢正常 🔗向量 ❌停用", "WI_Entry_Status_Constant": "🔵", "WI_Entry_Status_Normal": "🟢", "WI_Entry_Status_Vectorized": "🔗", "WI_Entry_Status_Disabled": "❌", - "T_Position": "↑角色人物:角色人物定義之前\n↓角色人物:角色人物定義之後\n↑備註:作者備註之前\n↓備註:作者備註之後\n@深度", - "Before Char Defs": "角色人物定義之前", - "After Char Defs": "角色人物定義之後", + "T_Position": "↑角色:角色定義之前\n↓角色:角色定義之後\n↑備註:作者備註之前\n↓備註:作者備註之後\n@深度", + "Before Char Defs": "角色定義之前", + "After Char Defs": "角色定義之後", "Before EM": "範例訊息之前", "After EM": "範例訊息之後", "Before AN": "作者備註之前", @@ -992,9 +992,9 @@ "Prevent further recursion (this entry will not activate others)": "防止進一步遞迴(此條目不會啟用其他條目)", "Delay until recursion (this entry can only be activated on recursive checking)": "延遲遞迴(此條目只能在遞迴檢查時啟用)", "What this keyword should mean to the AI, sent verbatim": "這個關鍵字對 AI 應意味著什麼,逐字發送", - "Filter to Character(s)": "角色人物篩選", - "Character Exclusion": "角色人物排除", - "-- Characters not found --": "-- 未找到角色人物 --", + "Filter to Character(s)": "角色篩選", + "Character Exclusion": "角色排除", + "-- Characters not found --": "-- 未找到角色 --", "Inclusion Group": "包含的群組", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "如果觸發多個條目,包含群組可確保一次僅啟動一組中的一個條目。\r支援多個以逗號分隔的群組。\r\r文件:世界資訊 - 包容性集團", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "優先考慮此條目:選取後,此條目將在所有選擇中優先。\r如果有多個優先級,則選擇「順序」最高的一個。", @@ -1015,7 +1015,7 @@ "Injection position. Next to other prompts (relative) or in-chat (absolute).": "注入位置。與其他提示詞相鄰(相對位置)或在聊天中(絕對位置)。", "prompt_manager_relative": "相對位置", "prompt_manager_depth": "深度", - "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最後一條訊息之後,1 = 在最後一條訊息之前,以此類推。", + "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最後一則訊息之後,1 = 在最後一則訊息之前,以此類推。", "Prompt": "提示詞", "The prompt to be sent.": "要發送的提示詞。", "This prompt cannot be overridden by character cards, even if overrides are preferred.": "即使啟用偏好覆寫,此提示詞也不能被角色卡片覆寫。", @@ -1050,32 +1050,32 @@ "welcome_message_part_6": "加入", "Discord server": "不和諧伺服器", "welcome_message_part_7": "取得公告和資訊。", - "SillyTavern is aimed at advanced users.": "SillyTavern 旨在進階使用者", + "SillyTavern is aimed at advanced users.": "SillyTavern 專為進階使用者設計", "If you're new to this, enable the simplified UI mode below.": "如果您是新手,請啟用下方的簡化 UI 模式", "Change it later in the 'User Settings' panel.": "稍後可在「使用者設定」面板中變更", "Enable simple UI mode": "啟用簡單 UI 模式", - "Looking for AI characters?": "正在尋找 AI 角色人物?", + "Looking for AI characters?": "正在尋找 AI 角色?", "onboarding_import": "匯入", "from supported sources or view": "從支援的來源或檢視", - "Sample characters": "範例角色人物", - "Your Persona": "您的玩家角色人物", - "Before you get started, you must select a persona name.": "在開始之前,您必須選擇一個玩家角色人物名稱", + "Sample characters": "範例角色", + "Your Persona": "您的使用者角色", + "Before you get started, you must select a persona name.": "在開始之前,您必須選擇一個使用者角色名稱", "welcome_message_part_8": "這個隨時可以透過", "welcome_message_part_9": "圖示來變更。", - "Persona Name:": "玩家角色人物名稱", - "Temporarily disable automatic replies from this character": "暫時停用此角色人物的自動回覆", - "Enable automatic replies from this character": "啟用此角色人物的自動回覆", - "Trigger a message from this character": "觸發此角色人物的訊息", + "Persona Name:": "使用者角色名稱", + "Temporarily disable automatic replies from this character": "暫時停用此角色的自動回覆", + "Enable automatic replies from this character": "啟用此角色的自動回覆", + "Trigger a message from this character": "觸發此角色的訊息", "Move up": "上移", "Move down": "下移", - "View character card": "查看角色人物卡", + "View character card": "查看角色卡", "Remove from group": "從群組中移除", "Add to group": "新增到群組", "Alternate Greetings": "額外問候語", - "Alternate_Greetings_desc": "這些將在開始新聊天時顯示為第一條訊息的滑動選項。\n群組成員可以選擇其中之一來開始對話。", + "Alternate_Greetings_desc": "這些將在開始新聊天時顯示為第一則訊息的滑動選項。\n群組成員可以選擇其中之一來開始對話。", "Alternate Greetings Hint": "額外問候語的提示訊息", - "(This will be the first message from the character that starts every chat)": "(這將是每次聊天開始時角色人物發送的第一條訊息)", - "Forbid Media Override explanation": "當前角色/群組在聊天中使用外部媒體的能力。", + "(This will be the first message from the character that starts every chat)": "(這將是每次聊天開始時角色發送的第一則訊息)", + "Forbid Media Override explanation": "此角色/群組在聊天中使用外部媒體的能力。", "Forbid Media Override subtitle": "禁止媒體覆寫副標題", "Always forbidden": "總是禁止", "Always allowed": "總是允許", @@ -1090,10 +1090,10 @@ "Insertion Frequency": "插入頻率", "(0 = Disable, 1 = Always)": "(0 = 停用, 1 = 永久)", "User inputs until next insertion:": "使用者輸入直到下一次插入:", - "Character Author's Note (Private)": "角色人物作者備註(私人)", - "Won't be shared with the character card on export.": "不會在匯出時與角色人物卡共享", - "Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.": "將自動新增為此角色人物的作者備註。將在群組中使用,但在群組聊天開啟時無法修改", - "Use character author's note": "使用角色人物作者備註", + "Character Author's Note (Private)": "角色作者備註(私人)", + "Won't be shared with the character card on export.": "不會在匯出時與角色卡共享", + "Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.": "將自動新增為此角色的作者備註。將在群組中使用,但在群組聊天開啟時無法修改", + "Use character author's note": "使用角色作者備註", "Replace Author's Note": "取代作者註釋", "Default Author's Note": "預設作者註釋", "Will be automatically added as the Author's Note for all new chats.": "將自動新增為所有新聊天的作者備註", @@ -1101,24 +1101,24 @@ "1 = disabled": "1 = 停用", "write short replies, write replies using past tense": "撰寫簡短回覆,使用過去式撰寫回覆", "Positive Prompt": "正面提示詞", - "Use character CFG scales": "使用角色人物 CFG 比例", - "Character CFG": "角色人物 CFG", - "Will be automatically added as the CFG for this character.": "將自動新增為此角色人物的 CFG", + "Use character CFG scales": "使用角色 CFG 比例", + "Character CFG": "角色 CFG", + "Will be automatically added as the CFG for this character.": "將自動新增為此角色的 CFG", "Global CFG": "全域 CFG", "Will be used as the default CFG options for every chat unless overridden.": "將作為每次聊天的預設 CFG 選項,除非被覆寫", "CFG Prompt Cascading": "CFG 提示詞級聯", "Combine positive/negative prompts from other boxes.": "結合其他文字方塊中的正/負提示詞", - "For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "例如,勾選聊天、全域和角色人物框將所有負面提示詞合併為一個逗號分隔的字串", + "For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "例如,勾選聊天、全域和角色框將所有負面提示詞合併為一個逗號分隔的字串", "Always Include": "總是包含", "Chat Negatives": "聊天負面提示詞", - "Character Negatives": "角色人物負面提示詞", + "Character Negatives": "角色負面提示詞", "Global Negatives": "全域負面提示詞", "Custom Separator:": "自訂分隔符:", "Insertion Depth:": "插入深度:", "Token Probabilities": "符記機率", "Select a token to see alternatives considered by the AI.": "選擇一個符記以檢視 AI 考慮的替代方案", "Not connected to API!": "未連線到 API!", - "Type a message, or /? for help": "輸入一個訊息,或輸入 /? 取得支援", + "Type a message, or /? for help": "輸入一則訊息,或輸入 /? 取得支援", "Continue script execution": "繼續腳本執行", "Pause script execution": "暫停腳本執行", "Abort script execution": "中止腳本執行", @@ -1138,13 +1138,13 @@ "Impersonate": "AI 扮演使用者", "Continue": "繼續", "Bind user name to that avatar": "將使用者名稱附加到該頭像", - "Change persona image": "更改玩家角色人物圖片", - "Select this as default persona for the new chats.": "選擇此作為新聊天的預設玩家角色人物。", - "Delete persona": "刪除玩家角色人物", + "Change persona image": "更改使用者角色圖片", + "Select this as default persona for the new chats.": "選擇此作為新聊天的預設使用者角色。", + "Delete persona": "刪除使用者角色", "These characters are the winners of character design contests and have outstandable quality.": "這些角色都是角色設計比賽的優勝者,品質卓越。", "Contest Winners": "比賽獲勝者", "These characters are the finalists of character design contests and have remarkable quality.": "這些角色都是角色設計比賽的入圍作品,品質卓越。", - "Featured Characters": "特色角色人物", + "Featured Characters": "特色角色", "Attach a File": "附加檔案", "Open Data Bank": "打開資料儲藏庫", "Enter a URL or the ID of a Fandom wiki page to scrape:": "輸入 URL 或 Fandom 維基頁面的 ID 來抓取:", @@ -1155,7 +1155,7 @@ "File per article": "每篇文章一個檔案", "Each article will be saved as a separate file.": "每篇文章將另存為一個檔案。", "Data Bank": "資料儲藏庫", - "These files will be available for extensions that support attachments (e.g. Vector Storage).": "這些檔案將可用於支援附件的擴充套件(例如向量存儲)。", + "These files will be available for extensions that support attachments (e.g. Vector Storage).": "這些檔案將可用於支援附件的擴充功能(例如向量存儲)。", "Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.": "支援的檔案類型:純文字,PDF,Markdown,HTML,EPUB。", "Drag and drop files here to upload.": "拖放檔案到這裡以上傳。", "Date (Newest First)": "日期(最新優先)", @@ -1168,12 +1168,12 @@ "Select All": "全選", "Select None": "選擇無", "Global Attachments": "全域附件", - "These files are available for all characters in all chats.": "這些檔案在所有聊天中的所有角色人物中可用。", - "Character Attachments": "角色人物附件", - "These files are available the current character in all chats they are in.": "這些檔案在當前角色人物參與的所有聊天中可用。", + "These files are available for all characters in all chats.": "這些檔案在所有聊天中的所有角色中可用。", + "Character Attachments": "角色附件", + "These files are available the current character in all chats they are in.": "這些檔案在此角色參與的所有聊天中可用。", "Saved locally. Not exported.": "本機儲存。不匯出。", "Chat Attachments": "聊天附件", - "These files are available to all characters in the current chat.": "這些檔案在當前聊天中的所有角色人物中可用。", + "These files are available to all characters in the current chat.": "這些檔案在此聊天中的所有角色中可用。", "Enter a base URL of the MediaWiki to scrape.": "輸入要抓取的 MediaWiki 的基礎 URL。", "Don't include the page name!": "不要包括頁面名稱!", "Enter web URLs to scrape (one per line):": "輸入要抓取的網頁 URL(每行一個):", @@ -1183,14 +1183,14 @@ "ext_sum_with": "總結一下:", "ext_sum_main_api": "主要API", "ext_sum_current_summary": "目前摘要:", - "ext_sum_restore_previous": "恢復上一個", + "ext_sum_restore_previous": "還原上一個", "ext_sum_memory_placeholder": "摘要將在此處產生...", "Trigger a summary update right now.": "立即觸發摘要更新。", "ext_sum_force_text": "現在總結一下", "Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "停用自動摘要更新。暫停時,摘要保持原樣。您仍然可以透過按下「立即匯總」按鈕強制更新(僅適用於 Main API)。", "ext_sum_pause": "暫停", "Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "從要總結的文本中省略世界資訊和作者備註。僅在使用主要 API 時有效。額外 API 總是省略 WI/AN。", - "ext_sum_no_wi_an": "無無線網路/無線網路", + "ext_sum_no_wi_an": "無法連接到網際網路", "ext_sum_settings_tip": "編輯摘要提示、插入位置等", "ext_sum_settings": "摘要設定", "ext_sum_prompt_builder": "提示產生器", @@ -1198,10 +1198,10 @@ "ext_sum_prompt_builder_1": "原始、阻塞", "ext_sum_prompt_builder_2_desc": "擴充功能將使用尚未匯總的訊息建立自己的提示。產生摘要時不會阻止聊天。並非所有後端都支援此模式。", "ext_sum_prompt_builder_2": "原始、非阻塞", - "ext_sum_prompt_builder_3_desc": "擴充功能將使用常規主提示產生器並將摘要請求新增至其中作為最後一個系統訊息。", + "ext_sum_prompt_builder_3_desc": "擴充功能將使用常規主提示產生器並將摘要請求新增至其中作為最後一則系統訊息。", "ext_sum_prompt_builder_3": "經典,阻塞", "Summary Prompt": "摘要提示詞", - "ext_sum_restore_default_prompt_tip": "恢復預設提示", + "ext_sum_restore_default_prompt_tip": "還原預設提示", "ext_sum_prompt_placeholder": "這個提示詞將發送給 AI 以請求生成摘要。{{words}} 將解析為「字數」參數。", "ext_sum_target_length_1": "目標摘要長度", "ext_sum_target_length_2": "(", @@ -1225,7 +1225,7 @@ "ext_sum_injection_position": "注射位置", "How many messages before the current end of the chat.": "在聊天目前結束前有多少訊息。", "ext_regex_title": "正規表示式", - "ext_regex_new_global_script": "+ 全球", + "ext_regex_new_global_script": "+ 全域", "ext_regex_new_scoped_script": "+ 範圍", "ext_regex_import_script": "匯入腳本", "ext_regex_global_scripts": "全域腳本", @@ -1260,28 +1260,28 @@ "Run On Edit": "編輯時執行", "ext_regex_substitute_regex_desc": "在執行「尋找正規表示式」前取代 {{macros}}", "Substitute Regex": "取代正規表示式", - "ext_regex_import_target": "導入至:", + "ext_regex_import_target": "匯入至:", "ext_regex_disable_script": "停用腳本", "ext_regex_enable_script": "啟用腳本", "ext_regex_edit_script": "編輯腳本", "ext_regex_move_to_global": "移至全域腳本", "ext_regex_move_to_scoped": "移至作用域腳本", - "ext_regex_export_script": "導出腳本", + "ext_regex_export_script": "匯出腳本", "ext_regex_delete_script": "刪除腳本", "Trigger Stable Diffusion": "觸發 Stable Diffusion", - "sd_Yourself": "角色人物", - "sd_Your_Face": "角色人物的臉", - "sd_Me": "玩家", + "sd_Yourself": "角色", + "sd_Your_Face": "角色的臉", + "sd_Me": "使用者", "sd_The_Whole_Story": "整篇故事", - "sd_The_Last_Message": "最後一個訊息", - "sd_Raw_Last_Message": "最後一個原始訊息", + "sd_The_Last_Message": "最後一則訊息", + "sd_Raw_Last_Message": "最後一則原始訊息", "sd_Background": "背景", "Image Generation": "圖片生成", "sd_refine_mode": "允許在傳送至生成 API 前,手動編輯提示詞字串", "sd_refine_mode_txt": "生成前編輯提示詞", "sd_interactive_mode": "當發送「給我一張貓的圖片」這類訊息時,自動生成圖片。", "sd_interactive_mode_txt": "互動模式", - "sd_multimodal_captioning": "根據使用者和角色人物的頭像,使用多模態模型描述生成肖像提示詞。", + "sd_multimodal_captioning": "根據使用者和角色的頭像,使用多模態模型描述生成肖像提示詞。", "sd_multimodal_captioning_txt": "對肖像使用多模態模型描述", "sd_expand": "使用文字生成模型自動擴充提示詞。", "sd_expand_txt": "自動擴充提示詞", @@ -1342,11 +1342,11 @@ "Common prompt prefix": "通用提示詞前綴", "sd_prompt_prefix_placeholder": "使用 {prompt} 指定生成的提示詞將被插入的位置。", "Negative common prompt prefix": "負面通用提示詞前綴", - "Character-specific prompt prefix": "角色人物特定提示詞前綴", + "Character-specific prompt prefix": "角色特定提示詞前綴", "Won't be used in groups.": "不會用在群組", - "sd_character_prompt_placeholder": "描述當前選擇角色人物的特徵。這些特徵將增加在通用提示詞前綴之後。\n例如:女性、綠色眼睛、棕色頭髮、粉紅色襯衫。", - "Character-specific negative prompt prefix": "角色人物特定負面提示詞前綴", - "sd_character_negative_prompt_placeholder": "不應出現在選定角色人物上的任何特徵。這些特徵將增加在負面通用提示詞前綴之後。例如:珠寶、鞋子、眼鏡。", + "sd_character_prompt_placeholder": "描述該選擇角色的特徵。這些特徵將增加在通用提示詞前綴之後。\n例如:女性、綠色眼睛、棕色頭髮、粉紅色襯衫。", + "Character-specific negative prompt prefix": "角色特定負面提示詞前綴", + "sd_character_negative_prompt_placeholder": "不應出現在選定角色上的任何特徵。這些特徵將增加在負面通用提示詞前綴之後。例如:珠寶、鞋子、眼鏡。", "Shareable": "可分享", "Image Prompt Templates": "圖片提示詞範本", "Vectors Model Warning": "向量模型警告", @@ -1371,7 +1371,7 @@ "Warning:": "警告:", "This action is irreversible.": "此動作不可逆轉。", "Type the user's handle below to confirm:": "在下方輸入使用者的控制代碼以確認:", - "Import Characters": "導入角色", + "Import Characters": "匯入角色", "Enter the URL of the content to import": "輸入要匯入的內容的 URL", "Supported sources:": "支持的來源:", "char_import_1": "Chub角色(直接連結或ID)", @@ -1383,15 +1383,15 @@ "char_import_6": "直接 PNG 連結(請參閱", "char_import_7": "對於允許的主機)", "char_import_8": "RisuRealm角色(直接連結)", - "Supports importing multiple characters.": "支援導入多個字元。", + "Supports importing multiple characters.": "支援匯入多個字元。", "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。", "Export for character": "匯出字符", "Export prompts for this character, including their order.": "匯出該角色的提示,包括其順序。", - "Export all": "全部導出", + "Export all": "全部匯出", "Export all your prompts to a file": "將所有提示匯出到文件", "Insert prompt": "插入提示", "Delete prompt": "刪除提示", - "Import a prompt list": "導入提示列表", + "Import a prompt list": "匯入提示列表", "Export this prompt list": "匯出此提示列表", "Reset current character": "重置目前字符", "New prompt": "新提示", @@ -1403,12 +1403,12 @@ "Settings Snapshots": "設定值快照", "Record a snapshot of your current settings.": "記錄目前設定的快照。", "Make a Snapshot": "建立快照", - "Restore this snapshot": "恢復此快照", + "Restore this snapshot": "還原此快照", "Hi,": "嗨,", "To enable multi-account features, restart the SillyTavern server with": "要啟用多帳號功能,請在 config.yaml 文件中將", "set to true in the config.yaml file.": "設為 true,然後重啟 SillyTavern 伺服器。", "Account Info": "帳號資訊", - "To change your user avatar, use the buttons below or select a default persona in the Persona Management menu.": "要更改您的使用者頭像,請使用下方按鈕或在玩家角色管理選單中選擇一個預設人物。", + "To change your user avatar, use the buttons below or select a default persona in the Persona Management menu.": "要更改您的使用者頭像,請使用下方按鈕或在使用者角色管理選單中選擇一個預設人物。", "Set your custom avatar.": "設定您的頭像。", "Remove your custom avatar.": "移除您的頭像。", "Handle:": "使用者名稱:", @@ -1428,12 +1428,12 @@ "Want to update?": "想要更新嗎?", "How to start chatting?": "如何開始聊天?", "Click _space": "點選", - "and select a": "並選擇一個", + "and select a": "並擇一", "Chat API": "聊天 API", - "and pick a character.": "並選擇一個角色人物。", + "and pick a character.": "並選擇一個角色。", "You can browse a list of bundled characters in the": "您可以在", - "Download Extensions & Assets": "下載擴充套件和資產", - "menu within": "選單中瀏覽內建角色人物列表", + "Download Extensions & Assets": "下載擴充功能和資產", + "menu within": "選單中瀏覽內建角色列表", "Confused or lost?": "困惑或迷路了嗎?", "click these icons!": "點選這些圖示!", "in the chat bar": "到聊天欄中", @@ -1443,4 +1443,349 @@ "Join the SillyTavern Discord": "加入 SillyTavern Discord", "Post a GitHub issue": "發布 GitHub 問題", "Contact the developers": "聯繫開發者" + "(-1 for random)": "(-1 表示隨機)", + "(Optional)": "(可選)", + "(use _space": "(使用", + "[no_connection_text]api_no_connection": "未連線⋯", + "[no_desc_text]No model description": "[無描述]", + "[no_items_text]openai_logit_bias_no_items": "無項目", + "[placeholder]Any contents here will replace the default Post-History Instructions used for this character. (v2 specpost_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 specpost_history_instructions)", + "[placeholder]comma delimited,no spaces between": "逗號分割,無需空格", + "[placeholder]e.g. black-forest-labs/FLUX.1-dev": "例如:black-forest-labs/FLUX.1-dev", + "[placeholder]Example: gpt-3.5-turbo": "例如:gpt-3.5-turbo", + "[placeholder]Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1", + "[popup-button-crop]popup-button-crop": "裁剪", + "[title](disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)", + "[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)", + "[title]A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。", + "[title]A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用區域擴展的倍數。", + "[title]Abort current image generation task": "終止目前的圖片生成任務", + "[title]Add Character and User names to a list of stopping strings.": "將角色和使用者角色名稱添加至停止字符列表。", + "[title]Alignment for rank nodes.": "對排名節點的對齊方式。", + "[title]Always show the node full info panel at the bottom left of the timeline view. When off, show it near the node.": "始終將節點的完整資訊面板顯示在時間軸視圖的左下角。關閉時,將顯示在節點附近。", + "[title]Always show the node tooltip at the bottom left of the timeline view. When off, show it near the node.": "始終將節點的工具提示欄顯示在時間軸視圖的左下角。關閉時,將顯示在節點附近。", + "[title]Apply current sorting as Order": "應用此排序為順序", + "[title]Cap the number of entry activation recursions": "限制入口啟動的遞歸次數", + "[title]Caption": "標題", + "[title]Close popup": "關閉彈出視窗", + "[title]Color configuration for Timelines when 'Use UI Theme' in Style Settings is off.": "關閉「使用介面主題」的時間線顏色。", + "[title]context_allow_post_history_instructions": "在文本完成模式中包含聊天歷史後指示(Post-History Instructions),但可能導致不良輸出。", + "[title]Create a new connection profile": "建立新的連線設定檔", + "[title]Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定義導入卡片時應採取的動作。選擇「詢問」將始終顯示對話框。", + "[title]delay_until_recursion_level": "定義遞迴掃描的延遲層級。\r最初僅匹配第一層(數字最小的層級)。\r未找到匹配時,下一層將成為可匹配的層級。\r此過程會重複,直到所有層級都被檢查完畢。\r與「延遲至遞歸」設定相關聯。", + "[title]Delete a connection profile": "刪除連線設定檔", + "[title]Delete template": "刪除模板", + "[title]Delete the template": "刪除此模板", + "[title]Disabling is not recommended.": "不建議禁用。", + "[title]Display swipe numbers for all messages, not just the last.": "顯示所有訊息的滑動編號,而不僅是最後一條訊息。", + "[title]Duplicate persona": "複製使用者角色", + "[title]Edit a connection profile": "編輯連線設定檔", + "[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "啟用自動選擇輸入框中的文本,適用於彈出輸入框及其他自定義輸入框。", + "[title]Entries with a cooldown can't be activated N messages after being triggered.": "設有冷卻時間的條目於觸發後的 N 條訊息內無法再次啟用。", + "[title]Entries with a delay can't be activated until there are N messages present in the chat.": "有延遲的條目需等待聊天中出現 N 條訊息後才能啟用。", + "[title]Expand swipe nodes when the timeline view first opens, and whenever the graph is refreshed. When off, you can expand them by long-pressing a node, or by pressing the Toggle Swipes button.": "時間線視圖首次打開或刷新時展開滑動節點。關閉時可通過長按節點或點選「切換滑動」按鈕展開。", + "[title]Export Advanced Formatting settings": "匯出高級格式設定", + "[title]Export template": "匯出模板", + "[title]Find similar characters": "尋找相似角色", + "[title]Height of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時的節點像素高度。", + "[title]How the automatic graph builder assigns a rank (layout depth) to graph nodes.": "自動圖表生成器分配圖節點等級(佈局深度)的方式。", + "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "勾選後,將使用角色卡中的聊天歷史後指示(Post-History Instructions)覆蓋。", + "[title]Import Advanced Formatting settings": "匯入進階格式設定\n\n您也可以提供舊版檔案作為提示詞和上下文範本使用。", + "[title]Import template": "匯入模板", + "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "在群組聊天中,突出顯示該生成回應的角色及順序。", + "[title]Include names with each message into the context for scanning": "將每條訊息的名稱納入掃描上下文", + "[title]Inserted before the first User's message": "插入到第一條使用者訊息前。", + "[title]instruct_enabled": "啟用指令模式(Instruct Mode)", + "[title]instruct_last_input_sequence": "插入到最後一條使用者訊息之前。", + "[title]instruct_template_activation_regex_desc": "連接 API 或選擇模型時,若模型名稱符合所提供的正規表達式,則自動啟動該指令模板(Instruct Template)。", + "[title]Load Asset List": "載入資源列表", + "[title]load_asset_list_desc": "根據資源列表文件載入擴展及資源。\n\n該字段中的默認資源 URL 指向官方的擴展及資源的列表。\n可在此插入您的自定義資源列表。\n\n若需安裝單個第三方擴展,請使用右上角的「安裝擴展」按鈕。", + "[title]markdown_hotkeys_desc": "啟用熱鍵以在某些文本輸入框中插入 Markdown 格式字符。詳見「/help hotkeys」。", + "[title]Not all samplers supported.": "並非所有採樣器均受支援。", + "[title]Open the timeline view. Same as the slash command '/tl'.": "打開時間線視圖,與斜杠指令「/tl」相同。", + "[title]Penalize sequences based on their length.": "根據序列長度進行懲罰。", + "[title]Reload a connection profile": "重新載入連線設定檔", + "[title]Rename current preset": "重新命名此預設", + "[title]Rename current prompt": "重新命名此提示詞", + "[title]Rename current template": "重新密名此模板", + "[title]Reset all Timelines settings to their default values.": "將所有時間軸設定重置為預設值。", + "[title]Restore current prompt": "還原目前的提示詞", + "[title]Restore current template": "還原目前的模板", + "[title]Save prompt as": "另存提示詞為", + "[title]Save template as": "另存模板為", + "[title]sd_adetailer_face": "在生成過程中使用 ADetailer 臉部模型。需在後端安裝 ADetailer 擴展。", + "[title]sd_free_extend": "自動使用目前選定的 LLM 擴展自由模式主題提示詞(不包括肖像或背景)。", + "[title]sd_function_tool": "使用功能工具自動檢測意圖以生成圖片。", + "[title]Seed_desc": "用於生成確定性和可重現輸出的隨機種子。設定為 -1 時將使用隨機種子。", + "[title]Select your current Context Template": "選擇您目前的上下文模板", + "[title]Select your current Instruct Template": "選擇您目前的指令模板", + "[title]Select your current System Prompt": "選擇您目前的系統提示詞", + "[title]Separation between adjacent edges in the same rank.": "同一層級中相鄰邊之間的間距。", + "[title]Separation between adjacent nodes in the same rank.": "同一層級中相鄰節點之間的間距。", + "[title]Separation between each rank in the layout.": "佈局中各層級之間的間距。", + "[title]Settings for the visual appearance of the Timelines graph.": "時間線圖形的視覺外觀設置。", + "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "在輸入框中添加按鈕,讓 AI 模仿您的角色身份發送一則訊息。", + "[title]Show a legend for colors corresponding to different characters and chat checkpoints.": "顯示一個圖例,標註不同角色和對話檢查點對應的顏色。", + "[title]Show the AI character's avatar as the graph root node. When off, the root node is blank.": "將 AI 角色的頭像作為圖形的根節點;關閉時,根節點為空。", + "[title]Sticky entries will stay active for N messages after being triggered.": "觸發後,置頂條目將在接下來的 N 條訊息中保持活躍。", + "[title]stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} 和 {{getglobalvar::}} 巨集的字面巨集樣值被自動解析。\n例如,{{newline}} 將保持為字面字串 {{newline}}。\n\n(此功能通過內部將 {{getvar::}} 和 {{getglobalvar::}} 巨集替換為具範圍的變數來實現。)", + "[title]Style and routing of graph edges.": "圖形邊的樣式和路徑。", + "[title]Swap width and height": "交換寬度與高度", + "[title]Swipe left": "向左滑動", + "[title]Swipe right": "向右滑動", + "[title]Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry": "切換角色/標籤篩選,將列出的角色和標籤從此條目的匹配中排除", + "[title]sysprompt_enabled": "啟用系統提示詞", + "[title]The number of sequences generated at each step with Beam Search.": "在 Beam Search 中每一步生成的序列數量。", + "[title]The visual appearance of a node in the graph.": "圖形中節點的視覺外觀。", + "[title]Update a connection profile": "更新連線設定檔", + "[title]Update current prompt": "更新此提示詞", + "[title]Update current template": "更新此模板", + "[title]Use GPU acceleration for positioning the full info panel that appears when you click a node. If the tooltip arrow tends to disappear, turning this off may help.": "啟用 GPU 加速來定位點擊節點時出現的完整資訊面板。若發現工具提示箭頭經常消失,可考慮關閉此功能。", + "[title]Use the colors of the ST GUI theme, instead of the colors configured in Color Settings specifically for this extension.": "以 ST GUI 主題的顏色,取代「顏色設定」中針對此擴展特別配置的顏色。", + "[title]View connection profile details": "查看連線設定檔詳情", + "[title]When enabled, nodes that have swipes splitting off of them will appear subtly larger, in addition to having the double border.": "啟用後,具分支滑動的節點將顯示雙重邊框,還會略微放大。", + "[title]WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized": "世界資訊條目狀態:\\r🔵 恆定\\r🟢 正常\\r🔗 向量化", + "[title]Width of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時,節點的像素寬度。", + "[title]world_button_title": "角色背景設定\\n\\n點擊以載入\\nShift+ 點擊開啟「連結至世界資訊」彈窗", + "# of Beams": "# of Beams", + "01.AI API Key": "01.AI API 金鑰", + "01.AI Model": "01.AI 模型", + "取消": "取消", + "Additional Parameters": "其他參數", + "All": "全部", + "Allow fallback models": "允許回退模型", + "Allow fallback providers": "允許回退供應商", + "Allow Post-History Instructions": "允許聊天歷史後指示", + "Allow reverse proxy": "允許反向代理", + "Alternate Greeting #": "備選問候語 #", + "alternate_greetings_hint_1": "點擊", + "alternate_greetings_hint_2": "按鈕開始!", + "Always": "總是", + "ANY support requests will be REFUSED if you are using a proxy.": "使用代理時,所有支援請求均不予受理。", + "API": "API", + "API Key": "API 金鑰", + "Ask": "詢問", + "Ask every time": "每次都詢問", + "Assets URL": "資源 URL", + "Assistant Message Sequences": "助理訊息序列", + "Assistant Prefix": "助理訊息前綴", + "Assistant Suffix": "助理訊息後綴", + "at the end of the URL!": "在 URL 末尾!", + "Audio Playback Speed": "音檔播放速度", + "Auto-select Input Text": "自動選擇輸入文本", + "Automatically caption images": "自動為圖片添加字幕", + "Auxiliary": "輔助", + "Background Image": "背景圖片", + "Block Entropy API Key": "Block Entropy API 金鑰", + "Can be set manually or with an _space": "可以手動設置或使用_space", + "Caption Prompt": "字幕提示詞", + "category": "類別", + "Character Expressions": "角色情緒立繪", + "Character Node Color": "角色節點顏色", + "character_names_none": "避免使用角色名稱作為前綴。此功能在群組中可能表現不理想,需謹慎考慮。", + "Characters": "角色", + "Chat Message Visibility (by source)": "聊天訊息可見性(按來源)", + "Chat vectorization settings": "聊天向量化設定", + "Checked: all entries except ❌ status can be activated.": "勾選:除 ❌ 狀態外的所有條目均可啟動。", + "Checkpoint Color": "檢查點節點邊框顏色", + "Chunk boundary": "Chunk 邊界", + "Chunk overlap (%)": "Chunk 重疊 (%)", + "Chunk size (chars)": "Chunk 大小(字符數)", + "class": "所有類別", + "Classifier API": "分類器 API", + "Click to set": "點擊以設定", + "CLIP Skip": "CLIP 跳過", + "Completion Object": "完成對象", + "Conf": "設定檔", + "Connection Profile": "連線設定檔", + "Cooldown": "冷卻時間", + "Create new folder in the _space": "在 _space 中創建新文件夾", + "currently_loaded": "[目前已載入]", + "currently_selected": "[目前已選取]", + "Custom (OpenAI-compatible)": "自定義(相容 OpenAI)", + "Custom Expressions": "自定義情緒圖像", + "Data Bank files": "數據庫文件", + "Default / Fallback Expression": "默認/回退表情", + "Delay": "延遲", + "Delay until recursion (can only be activated on recursive checking)": "遞迴掃描延遲(僅在啟用遞迴掃描時可用)", + "Do not proceed if you do not agree to this!": "若不接受此條款,請勿繼續!", + "Edge Color": "邊緣顏色", + "Edit captions before saving": "在保存前編輯字幕", + "Enable for files": "為文件啟用", + "Enable for World Info": "為世界資訊啟用", + "enable_functions_desc_1": "允許使用", + "enable_functions_desc_2": "功能工具", + "enable_functions_desc_3": "可供多種擴展利用,實現更多功能。", + "Enabled for all entries": "對所有條目啟用", + "Enabled for chat messages": "對聊天訊息啟用", + "Endpoint URL": "端點 URL", + "Enter a Model ID": "輸入模型 ID", + "Example: https://****.endpoints.huggingface.cloud": "例如:https://****.endpoints.huggingface.cloud", + "Exclude": "排除", + "Exclude Top Choices (XTC)": "排除頂部選項(XTC)", + "Existing": "現有項目", + "expression_label_pattern": "[情緒_標籤].[圖檔_格式]", + "ext_translate_auto_mode": "自動模式", + "ext_translate_btn_chat": "翻譯聊天內容", + "ext_translate_btn_input": "翻譯輸入內容", + "ext_translate_clear": "清除翻譯", + "ext_translate_mode_both": "翻譯輸入和回應", + "ext_translate_mode_inputs": "僅翻譯輸入", + "ext_translate_mode_none": "無翻譯", + "ext_translate_mode_provider": "提供者模式", + "ext_translate_mode_responses": "僅翻譯回應", + "ext_translate_target_lang": "目標語言", + "ext_translate_title": "聊天翻譯", + "Extensions Menu": "擴展選單", + "Extras": "擴充功能", + "Extras API": "擴充功能 API", + "Featherless Model Selection": "無羽模型選擇", + "File vectorization settings": "檔案向量化設定", + "Filter to Characters or Tags": "角色或標籤篩選", + "First User Prefix": "第一使用者前綴", + "folder of your user data directory and name it as the name of the character.": "使用者資料目錄中的資料夾名稱應與角色名稱一致。", + "Group Scoring": "群組評分", + "Groups and Past Personas": "群組與過去的使用者角色設定", + "Hint:": "提示:", + "Hint: Set the URL in the API connection settings.": "提示:在 API 連線設置中設定URL。", + "Horde": "Horde", + "HuggingFace Token": "HuggingFace 符記", + "Image Captioning": "圖片標註", + "Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)", + "Injection Position": "插入位置", + "Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。", + "Injection Template": "插入範本", + "Insert#": "插入#", + "Instruct Sequences": "指令序列", + "Instruct Template": "指令範本", + "Interactive Mode": "互動模式", + "Karras": "Karras", + "Keep model in memory": "將模型保存在記憶體中", + "Keyboard": "鍵盤:", + "KoboldAI Horde Website": "KoboldAI Horde 網站", + "Last User Prefix": "最後用戶前綴", + "Linear": "線性", + "LLM": "LLM", + "LLM Prompt": "LLM 提示詞", + "Load a custom asset list or select": "載入或選擇自定義資源列表", + "Load an asset list": "載入資源列表", + "Local": "本地", + "Local (Transformers)": "本地(Transformers)", + "macro)": "巨集)", + "Main API": "主要 API", + "Markdown Hotkeys": "Markdown 快捷鍵", + "Master Export": "主匯出", + "Master Import": "主匯入", + "Max Entries": "最大條目數", + "Max Recursion Steps": "最大遞迴步數", + "Message attachments": "訊息附件", + "Message Template": "訊息範本", + "Model ID": "模型 ID", + "mui_reset": "重置", + "Multimodal (OpenAI / Anthropic / llama / Google)": "多模態(OpenAI/Anthropic/llama/Google)", + "must be set in Tabby's config.yml to switch models.": "須在 Tabby's config.yml 中設置以切換模型。", + "Names as Stop Strings": "將名稱用作停止字串", + "Never": "從不", + "NomicAI API Key": "NomicAI API 金鑰", + "Non-recursable (will not be activated by another)": "不可遞迴(不會被其他條目啟動)", + "None (disabled)": "無(已禁用)", + "OK": "確定", + "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "舊訊息會在聊天時逐步向量化。\n若要處理所有先前訊息,請點擊下方按鈕。", + "Only used when Main API or WebLLM Extension is selected.": "僅在選擇主要 API 或 WebLLM 擴展時使用。", + "Open a chat to see the character expressions.": "開啟聊天以查看角色表情。", + "Post-History Instructions": "聊天歷史後指示", + "Prefer Character Card Instructions": "偏好角色卡聊天歷史後指示", + "Prioritize": "優先處理", + "Prompt Content": "提示詞內容", + "prompt_manager_in_chat": "聊天中的提示詞管理", + "prompt_post_processing_none": "無", + "Purge Vectors": "清除向量", + "Put images with expressions there. File names should follow the pattern:": "將情緒圖片放置於此,檔案名稱應遵循以下格式:", + "Quad": "四元數", + "Query messages": "查詢訊息", + "Quick Impersonate button": "快速模擬按鈕", + "Recursion Level": "遞迴層級", + "Remove all image overrides": "移除所有圖片覆蓋", + "Restore default": "", + "Retain#": "保留#", + "Retrieve chunks": "檢索 chunks", + "Sampler Order": "取樣順序", + "Score threshold": "分數閾值", + "sd_free_extend_small": "(互動/指令)", + "sd_free_extend_txt": "擴展自由模式提示詞", + "sd_function_tool_txt": "使用功能工具", + "sd_prompt_-1": "聊天訊息範本", + "sd_prompt_-2": "功能工具提示描述", + "sd_prompt_0": "角色(您自己)", + "sd_prompt_1": "使用者(我)", + "sd_prompt_10": "肖像(多模態模式)", + "sd_prompt_11": "自由模式(LLM 擴展)", + "sd_prompt_2": "場景(完整故事)", + "sd_prompt_3": "原始最後訊息", + "sd_prompt_4": "最後訊息", + "sd_prompt_5": "肖像(您的臉)", + "sd_prompt_7": "背景", + "sd_prompt_8": "角色(多模態模式)", + "sd_prompt_9": "使用者(多模態模式)", + "Select a Model": "選擇模型", + "Select the API for classifying expressions.": "選擇分類表情的 API。", + "Select with Enter": "按 Enter 選擇", + "Select with Tab": "按 Tab 選擇", + "Select with Tab or Enter": "按 Tab 或 Enter 選擇", + "Separators as Stop Strings": "以分隔符作為停止字串", + "Set the default and fallback expression being used when no matching expression is found.": "設定在無法配對到表情時所使用的預設和替代圖片。", + "Set your API keys and endpoints in the API Connections tab first.": "請先在「API 連接」頁面中設定您的 API 密鑰和端點。", + "Show default images (emojis) if sprite missing": "無對應圖片時,顯示為預設表情符號 (emoji)", + "Show group chat queue": "顯示群組聊天隊列", + "Size threshold (KB)": "大小閾值 (KB)", + "Slash Command": "斜線命令", + "space_ slash command.": "斜線命令。", + "Sprite Folder Override": "表情立繪資料夾覆蓋", + "Sprite set:": "立繪集:", + "Show Gallery": "查看畫廊", + "Sticky": "固定", + "Style Preset": "預設樣式", + "Summarize chat messages for vector generation": "摘要聊天訊息以進行向量化處理", + "Summarize chat messages when sending": "傳送時摘要聊天內容", + "Swipe # for All Messages": "為所有訊息分配滑動編號 #", + "System Message Sequences": "系統訊息順序", + "System Prefix": "系統訊息前綴", + "System Prompt Sequences": "系統提示詞順序", + "System Suffix": "系統訊息後綴", + "Tabby Model": "Tabby 模型", + "tag_import_all": "全部匯入", + "tag_import_none": "不匯入", + "Text Generation WebUI (oobabooga)": "文字生成 WebUI (oobabooga)", + "The server MUST be started with the --embedding flag to use this feature!": "若要使用此功能,伺服器必須啟動時加上 --embedding 標誌。", + "Threshold": "閾值", + "to install 3rd party extensions.": "用於安裝第三方擴充功能。", + "Top": "頂部", + "Translate text to English before classification": "在分類前將文本翻譯為英文。", + "Uncheck to hide the extensions messages in chat prompts.": "不勾選即可隱藏聊天提示詞中的擴充功能訊息。", + "Unchecked: only entries with ❌ status can be activated.": "未勾選時:僅允許啟用狀態為 ❌ 的條目。", + "Unified Sampling": "統一取樣(Unified Sampling)", + "Upload sprite pack (ZIP)": "打包上傳立繪(ZIP 格式)", + "Use a forward slash to specify a subfolder. Example: _space": "使用「/」來設置子目錄,例如:_space", + "Use ADetailer (Face)": "使用 ADetailer 進行臉部處理。", + "Use an admin API key.": "使用管理員的 API 密鑰。", + "Use global": "啟用全域設定", + "User Message Sequences": "使用者訊息順序", + "User Node Color": "使用者節點顏色", + "User Prefix": "使用者訊息前綴", + "User Suffix": "使用者訊息後綴", + "Using a proxy that youre not running yourself is a risk to your data privacy.": "使用非自行管理的代理服務存在數據隱私洩漏風險。", + "Vector Storage": "向量存儲", + "Vector Summarization": "向量摘要", + "Vectorization Model": "向量生成模型", + "Vectorization Source": "向量化來源", + "Vectorize All": "向量化全部數據", + "View Stats": "查看統計資料", + "Warning: This might cause your sent messages to take a bit to process and slow down response time.": "警告:這可能會導致訊息處理速度變慢,並延長回應時間。", + "WarningThis might cause your sent messages to take a bit to process and slow down response time.": "警告:這將顯著減緩向量生成速度,因為所有消息都需先進行摘要。", + "WebLLM Extension": "WebLLM 擴充功能", + "Whole Words": "匹配完整單字", + "Will be used if the API doesnt support JSON schemas or function calling.": "若 API 不支持 JSON 模式或函數調用,將使用此設定。", + "World Info settings": "世界資訊設定", + "You are in offline mode. Click on the image below to set the expression.": "您目前為離線狀態,請點擊下方圖片進行表情設定。", + "You can find your API key in the Stability AI dashboard.": "API 密鑰可在 Stability AI 儀表板中查看。" } From 7098e17d2225358e4ede003b12895c8eedbf9441 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 8 Dec 2024 14:04:13 +0200 Subject: [PATCH 017/222] Change default value of maxTotalChatBackups --- default/config.yaml | 2 +- src/endpoints/chats.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index 22276d567..a3de53021 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -99,7 +99,7 @@ disableChatBackup: false # Number of backups to keep for each chat and settings file numberOfBackups: 50 # Maximum number of chat backups to keep per user (starting from the most recent). Set to -1 to keep all backups. -maxTotalChatBackups: 500 +maxTotalChatBackups: -1 # Interval in milliseconds to throttle chat backups per user chatBackupThrottleInterval: 10000 # Allowed hosts for card downloads diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index dbd20687b..70817e037 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -33,7 +33,7 @@ function backupChat(directory, name, chat) { removeOldBackups(directory, `chat_${name}_`); - const maxTotalChatBackups = Number(getConfigValue('maxTotalChatBackups', 500)); + const maxTotalChatBackups = Number(getConfigValue('maxTotalChatBackups', -1)); if (isNaN(maxTotalChatBackups) || maxTotalChatBackups < 0) { return; } From 210fac321b222420a26b6b37a320c02560e89094 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 8 Dec 2024 14:05:14 +0200 Subject: [PATCH 018/222] Move config reading to top-level --- src/endpoints/chats.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index 70817e037..1e7a00d08 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -11,6 +11,10 @@ import _ from 'lodash'; import { jsonParser, urlencodedParser } from '../express-common.js'; import { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } from '../util.js'; +const isBackupDisabled = getConfigValue('disableChatBackup', false); +const maxTotalChatBackups = Number(getConfigValue('maxTotalChatBackups', -1)); +const throttleInterval = getConfigValue('chatBackupThrottleInterval', 10_000); + /** * Saves a chat to the backups directory. * @param {string} directory The user's backups directory. @@ -19,7 +23,6 @@ import { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, */ function backupChat(directory, name, chat) { try { - const isBackupDisabled = getConfigValue('disableChatBackup', false); if (isBackupDisabled) { return; @@ -33,7 +36,6 @@ function backupChat(directory, name, chat) { removeOldBackups(directory, `chat_${name}_`); - const maxTotalChatBackups = Number(getConfigValue('maxTotalChatBackups', -1)); if (isNaN(maxTotalChatBackups) || maxTotalChatBackups < 0) { return; } @@ -52,7 +54,6 @@ const backupFunctions = new Map(); * @returns {function(string, string, string): void} Backup function */ function getBackupFunction(handle) { - const throttleInterval = getConfigValue('chatBackupThrottleInterval', 10_000); if (!backupFunctions.has(handle)) { backupFunctions.set(handle, _.throttle(backupChat, throttleInterval, { leading: true, trailing: true })); } From f98f83471a3722bfa970501048aa67ec99ca547f Mon Sep 17 00:00:00 2001 From: Alex Denton <125566609+alexdenton123@users.noreply.github.com> Date: Sun, 8 Dec 2024 23:31:03 +0100 Subject: [PATCH 019/222] Update index.js Changes for LLM API - Avoid sending partial API requests when streaming. - Avoid checking character length when using API as it can afford more characters. --- public/scripts/extensions/expressions/index.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 4130a5b05..fdc146c1a 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -697,6 +697,11 @@ async function moduleWorker() { return; } + // If using LLM api then check if streamingProcessor is finished to avoid sending multiple requests to the API + if (extension_settings.expressions.api === EXPRESSION_API.llm && context.streamingProcessor && !context.streamingProcessor.isFinished) { + return; + } + // API is busy if (inApiCall) { console.debug('Classification API is busy'); @@ -995,6 +1000,11 @@ function sampleClassifyText(text) { // Replace macros, remove asterisks and quotes let result = substituteParams(text).replace(/[*"]/g, ''); + // If using LLM api there is no need to check length of characters + if (extension_settings.expressions.api === EXPRESSION_API.llm) { + return result.trim(); + } + const SAMPLE_THRESHOLD = 500; const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2; From d9ee8aa3dcc84cbdb0a8ad82e2a200651989af4e Mon Sep 17 00:00:00 2001 From: Rivelle Date: Mon, 9 Dec 2024 14:26:46 +0800 Subject: [PATCH 020/222] Update zh-tw.json --- public/locales/zh-tw.json | 310 +++++++++++++++++++------------------- 1 file changed, 155 insertions(+), 155 deletions(-) diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index dcaee9e25..de1583607 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -27,10 +27,10 @@ "Instruct": "指示", "Prose Augmenter": "散文增強器", "Text Adventure": "文字冒險", - "response legth(tokens)": "回應長度(符記數)", + "response legth(tokens)": "回應長度(符記數)", "Streaming": "串流", "Streaming_desc": "生成時逐位顯示回應。當此功能關閉時,回應將在完成後一次顯示。", - "context size(tokens)": "上下文長度(符記數)", + "context size(tokens)": "上下文長度(符記數)", "unlocked": "解鎖", "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符記的上下文長度時啟用此功能", "Max prompt cost:": "最大提示詞費用", @@ -52,8 +52,8 @@ "Very aggressive": "非常積極", "Unlocked Context Size": "解鎖的上下文長度", "Unrestricted maximum value for the context slider": "上下文滑桿的無限制最大值", - "Context Size (tokens)": "上下文長度(符記數)", - "Max Response Length (tokens)": "最大回應長度(符記數)", + "Context Size (tokens)": "上下文長度(符記數)", + "Max Response Length (tokens)": "最大回應長度(符記數)", "Multiple swipes per generation": "每次生成多次滑動", "Enable OpenAI completion streaming": "啟用 OpenAI 補充串流", "Frequency Penalty": "頻率懲罰", @@ -112,7 +112,7 @@ "Eta": "Eta", "Mirostat_Eta_desc": "這個設定控制 Mirostat 的學習率。", "Ban EOS Token": "禁止 EOS 符記", - "Ban_EOS_Token_desc": "對於 KoboldCpp (以及可能也適用於 KoboldAI 的其他符記),禁止使用序列結束 (End-of-Sequence, EOS) 符記。這個設定對於故事創作很有幫助,但不應該在聊天和指令模式中使用。", + "Ban_EOS_Token_desc": "對於 KoboldCpp(以及可能也適用於 KoboldAI 的其他符記),禁止使用序列結束 (End-of-Sequence, EOS) 符記。這個設定對於故事創作很有幫助,但不應該在聊天和指令模式中使用。", "GBNF Grammar": "GBNF 語法", "Type in the desired custom grammar": "輸入所需的自定義語法", "Samplers Order": "取樣器順序", @@ -138,20 +138,20 @@ "Top A Sampling": "Top A 取樣", "CFG": "CFG", "Neutralize Samplers": "中和取樣器", - "Set all samplers to their neutral/disabled state.": "將所有取樣器設定為中性/停用狀態。", + "Set all samplers to their neutral/disabled state.": "將所有取樣器設定為中性/停用狀態。", "Sampler Select": "選擇取樣器", "Customize displayed samplers or add custom samplers.": "自訂顯示的取樣器或新增自訂取樣器。", "Epsilon Cutoff": "Epsilon 截斷", "Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled": "Epsilon 截斷設定排除符記的機率下限", "Eta Cutoff": "Eta 截斷", - "Eta_Cutoff_desc": "Eta 截斷是特殊 Eta 取樣技術的主要參數。\n單位為 1e-4;合理值為 3。\n設為 0 以停用。\n詳情請參見 Hewitt 等人(2022)撰寫的論文《Truncation Sampling as Language Model Desmoothing》。", + "Eta_Cutoff_desc": "Eta 截斷是特殊 Eta 取樣技術的主要參數。\n單位為 1e-4;合理值為 3。\n設為 0 以停用。\n詳情請參見 Hewitt 等人於 2022 年撰寫的論文《Truncation Sampling as Language Model Desmoothing》。", "rep.pen decay": "重複懲罰衰減", "Encoder Rep. Pen.": "編碼器重複懲罰", "No Repeat Ngram Size": "無重複 Ngram 大小", "Skew": "Skew", - "Max Tokens Second": "最大符記/秒", + "Max Tokens Second": "最大符記/秒", "Smooth Sampling": "平滑取樣", - "Smooth_Sampling_desc": "允許您使用二次/三次變換來調整分佈。較低的平滑因子值將更具創造性,通常在 0.2-0.3 之間是最佳點(假設曲線=1)。較高的平滑曲線值會使曲線更陡峭,這將更加激烈地懲罰低概率選擇。1.0 的曲線值相當於僅使用平滑因子。", + "Smooth_Sampling_desc": "允許您使用二次/三次變換來調整分佈。較低的平滑因子值將更具創造性,通常在 0.2-0.3 之間是最佳點(假設曲線=1)。較高的平滑曲線值會使曲線更陡峭,這將更加激烈地懲罰低概率選擇。1.0 的曲線值相當於僅使用平滑因子。", "Smoothing Factor": "平滑因子", "Smoothing Curve": "平滑曲線", "DRY_Repetition_Penalty_desc": "DRY 會懲罰那些將輸入的結尾擴充為已在先前輸入中出現過序列的符記。將乘法器設為 0 以停用。", @@ -171,7 +171,7 @@ "Minimum Temp": "最低溫度", "Maximum Temp": "最高溫度", "Exponent": "指數", - "Mirostat (mode=1 is only for llama.cpp)": "Mirostat(mode=1僅適用於llama.cpp)", + "Mirostat (mode=1 is only for llama.cpp)": "Mirostat(mode=1 僅適用於 llama.cpp)", "Mirostat_desc": "Mirostat 是輸出困惑度的恆溫器。\nMirostat 會將輸出的困惑度與輸入的困惑度相配對,\n從而避免了重複陷阱(在這個陷阱中,隨著自回歸推理產生字串,輸出的困惑度趨於零)和混亂陷阱(困惑度發散)。\n有關詳細資訊,請參閱 Basu 等人於 2020 年發表的論文《A Neural Text Decoding Algorithm that Directly Controls Perplexity》。\n模式選擇 Mirostat 版本。0=停用,1=Mirostat 1.0(僅限 llama.cpp),2=Mirostat 2.0。", "Mirostat Mode": "Mode", "Variability parameter for Mirostat outputs": "Mirostat 輸出的變異性參數", @@ -185,7 +185,7 @@ "Contrastive search": "對比搜尋", "Contrastive_search_txt": "一種取樣器,通過利用大多數 LLM 的表示空間的等向性,鼓勵多樣性的同時保持一致性。詳情請參閱 Su 等人於 2022 年發表的論文《A Contrastive Framework for Neural Text Generation》。", "Penalty Alpha": "懲罰 Alpha", - "Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "對比搜尋正則化項的強度。設定為 0 以停用CS", + "Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "對比搜尋正則化項的強度。設定為 0 以停用 CS", "Do Sample": "進行取樣", "Add BOS Token": "新增 BOS 符記", "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "在提示詞的開頭新增 bos_token。停用此功能可以使回應更具創造性", @@ -198,7 +198,7 @@ "Speculative Ngram": "推測性 Ngram", "Use a different speculative decoding method without a draft model": "使用不含草稿模型的不同推測性解碼方法。", "Spaces Between Special Tokens": "特殊符記之間的空格", - "LLaMA / Mistral / Yi models only": "僅限 LLaMA / Mistral / Yi 模型", + "LLaMA / Mistral / Yi models only": "僅限 LLaMA/Mistral/Yi 模型", "Example: some text [42, 69, 1337]": "範例:\n一些文字 [42, 69, 1337]", "Classifier Free Guidance. More helpful tip coming soon": "無分類器導引。更多有用提示訊息即將推出", "Scale": "比例", @@ -233,10 +233,10 @@ "Continue prefill": "繼續預先填充", "Continue sends the last message as assistant role instead of system message with instruction.": "繼續將最後的訊息作為助理角色發送,而不是帶有指令的系統訊息。", "Squash system messages": "合併系統訊息", - "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括對話範例)。可能會提高某些模型的一致性。", + "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "將連續的系統訊息合併為一個(不包括對話範例)。可能會提高某些模型的一致性。", "Enable function calling": "啟用函數調用", "Send inline images": "發送內嵌圖片", - "image_inlining_hint_1": "如果模型支援(例如: GPT-4V、Claude 3 或 Llava 13B),則在提示詞中發送圖片。\n使用任何訊息上的", + "image_inlining_hint_1": "如果模型支援(例如:GPT-4V、Claude 3 或 Llava 13B),則在提示詞中發送圖片。\n使用任何訊息上的", "image_inlining_hint_2": "動作或", "image_inlining_hint_3": "選單來附加圖片文件到聊天中。", "Inline Image Quality": "內嵌圖片品質", @@ -248,20 +248,20 @@ "Use Google Tokenizer": "使用 Google 分詞器", "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "通過 Google 模型的 API 使用適當的分詞器。提示詞處理速度較慢,但提供更準確的符記計數。", "Use system prompt": "使用系統提示詞", - "(Gemini 1.5 Pro/Flash only)": "(僅限於 Gemini 1.5 Pro/Flash)", + "(Gemini 1.5 Pro/Flash only)": "(僅限於 Gemini 1.5 Pro/Flash)", "Merges_all_system_messages_desc_1": "合併所有系統訊息,直到第一則非系統角色的訊息,並通過 google 的", "Merges_all_system_messages_desc_2": "字段發送,而不是與其餘提示詞內容一起發送。", "Assistant Prefill": "預先填充助理訊息", - "Start Claude's answer with...": "開始 Claude 的回答...", + "Start Claude's answer with...": "開始 Claude 的回答⋯", "Assistant Impersonation Prefill": "助理扮演時的預先填充", - "Use system prompt (Claude 2.1+ only)": "使用系統提示詞(僅限 Claude 2.1+)", + "Use system prompt (Claude 2.1+ only)": "使用系統提示詞(僅限 Claude 2.1+)", "Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.": "為支援的模型發送系統提示詞。停用時,使用者訊息將新增到提示詞的開頭。", "User first message": "使用者第一則訊息", "Restore User first message": "還原使用者第一則訊息", "Human message": "人類訊息、指令等。\n當空白時不加入任何內容,也就是需要一個帶有使用者角色的新提示詞。", "New preset": "新預設", "Delete preset": "刪除預設", - "View / Edit bias preset": "查看/編輯 Bias 預設", + "View / Edit bias preset": "查看/編輯 Bias 預設", "Add bias entry": "新增 Bias 條目", "Most tokens have a leading space.": "大多數符記有前導空格", "API Connections": "API 連線", @@ -289,7 +289,7 @@ "Models": "模型", "Refresh models": "重新整理模型", "-- Horde models not loaded --": "-- Horde 模型未載入 --", - "Not connected...": "尚未連線...", + "Not connected...": "尚未連線⋯", "API url": "API URL", "Example: http://127.0.0.1:5000/api ": "範例:http://127.0.0.1:5000/api ", "Connect": "連線", @@ -298,9 +298,9 @@ "Get your NovelAI API Key": "取得您的 NovelAI API 金鑰", "Enter it in the box below": "請在下面的框中輸入", "Novel AI Model": "NovelAI 模型", - "No connection...": "沒有連線...", + "No connection...": "沒有連線⋯", "API Type": "API 類型", - "Default (completions compatible)": "預設(相容補充)", + "Default (completions compatible)": "預設(相容補充)", "TogetherAI API Key": "TogetherAI API 金鑰", "TogetherAI Model": "TogetherAI 模型", "-- Connect to the API --": "-- 連線到 API --", @@ -317,10 +317,10 @@ "Mancer Model": "Mancer 模型", "Make sure you run it with": "確保您使用以下方式執行它", "flag": "旗標", - "API key (optional)": "API 金鑰(可選)", + "API key (optional)": "API 金鑰(可選)", "Server url": "伺服器 URL", "Example: 127.0.0.1:5000": "範例:127.0.0.1:5000", - "Custom model (optional)": "自訂模型(選填)", + "Custom model (optional)": "自訂模型(選填)", "vllm-project/vllm": "vllm-project/vllm", "vLLM API key": "vLLM API 金鑰", "Example: 127.0.0.1:8000": "範例:127.0.0.1:8000", @@ -334,7 +334,7 @@ "Ollama Model": "Ollama 模型", "Download": "下載", "Tabby API key": "Tabby API 金鑰", - "koboldcpp API key (optional)": "KoboldCpp API 金鑰(可選)", + "koboldcpp API key (optional)": "KoboldCpp API 金鑰(可選)", "Example: 127.0.0.1:5001": "範例:127.0.0.1:5001", "Authorize": "授權", "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用 OAuth 流程取得您的 OpenRouter API 符記。您將被重新導向到 openrouter.ai", @@ -348,11 +348,11 @@ "Proxy Name": "代理伺服器名稱", "This will show up as your saved preset.": "這將顯示為您儲存的預設", "Proxy Server URL": "代理伺服器 URL", - "Alternative server URL (leave empty to use the default value).": "替代伺服器 URL(留空以使用預設值)。", - "Remove your real OAI API Key from the API panel BEFORE typing anything into this box": "在此框中輸入任何內容之前,從API面板中刪除您的實際OAI API金鑰", + "Alternative server URL (leave empty to use the default value).": "替代伺服器 URL(留空以使用預設值)。", + "Remove your real OAI API Key from the API panel BEFORE typing anything into this box": "在此框中輸入任何內容之前,從 API 面板中刪除您的實際 OAI API 金鑰", "We cannot provide support for problems encountered while using an unofficial OpenAI proxy": "我們無法為使用非官方 OpenAI 代理伺服器時遇到的問題提供支援", "Doesn't work? Try adding": "不起作用?嘗試新增", - "at the end!": "在最後!", + "at the end!": "在最後!", "Proxy Password": "代理伺服器密碼", "Will be used as a password for the proxy instead of API key.": "將用作代理的密碼,而不是 API 金鑰", "Peek a password": "顯示密碼", @@ -364,17 +364,17 @@ "Use Proxy password field instead. This input will be ignored.": "請改用代理密碼欄位。此輸入將被忽略", "OpenAI Model": "OpenAI 模型", "Bypass API status check": "繞過 API 狀態檢查", - "Show External models (provided by API)": "顯示外部模型(由 API 提供)", + "Show External models (provided by API)": "顯示外部模型(由 API 提供)", "Get your key from": "取得您的鑰匙", "Anthropic's developer console": "Anthropic 的開發者控制台", "Claude Model": "Claude 模型", "Window AI Model": "Window AI 模型", "Model Order": "模型順序", "Alphabetically": "按字母順序", - "Price": "價格(最便宜的)", + "Price": "價格(最便宜的)", "Context Size": "上下文長度", "Group by vendors": "按供應商分組", - "Group by vendors Description": "將 OpenAI 、 Anthropic 等等的模型放各自供應商的群組中。可以與排序功能結合使用。", + "Group by vendors Description": "將 OpenAI、Anthropic 等模型放各自供應商的群組中。可以與排序功能結合使用。", "Allow fallback routes": "允許備援路徑", "Allow fallback routes Description": "如果選擇的模型無法滿足要求,會自動選擇替代模型。", "Scale API Key": "Scale API 金鑰", @@ -392,7 +392,7 @@ "Perplexity Model": "Perplexity 模型", "Cohere API Key": "Cohere API 金鑰", "Cohere Model": "Cohere 模型", - "Custom Endpoint (Base URL)": "自訂端點(Base URL)", + "Custom Endpoint (Base URL)": "自訂端點(Base URL)", "Custom API Key": "自訂 API 金鑰", "Available Models": "可用模型", "Prompt Post-Processing": "提示詞後處理", @@ -457,16 +457,16 @@ "Misc. Sequences": "其他序列", "Inserted before the first Assistant's message.": "插入在第一個助理的訊息之前。", "First Assistant Prefix": "首個助理前綴", - "instruct_last_output_sequence": "插入在最後一個助理的訊息之前,或在生成 AI 回覆時作為最後一行提示詞(除了中立/系統角色)。", + "instruct_last_output_sequence": "插入在最後一個助理的訊息之前,或在生成 AI 回覆時作為最後一行提示詞(除了中立/系統角色)。", "Last Assistant Prefix": "末尾助理前綴", - "Will be inserted as a last prompt line when using system/neutral generation.": "在使用系統/中立生成時作為最後一行提示詞插入。", + "Will be inserted as a last prompt line when using system/neutral generation.": "在使用系統/中立生成時作為最後一行提示詞插入。", "System Instruction Prefix": "系統指令前綴", "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "如果生成了停止序列,包括該序列以及之後的所有內容將從輸出中刪除。", "Stop Sequence": "停止序列", "Will be inserted at the start of the chat history if it doesn't start with a User message.": "如果聊天歷史不是以使用者訊息開始,將在聊天歷史的開頭插入。", "User Filler Message": "使用者填充訊息", "Context Formatting": "上下文格式", - "(Saved to Context Template)": "(已儲存到上下文範本)", + "(Saved to Context Template)": "(已儲存到上下文範本)", "Always add character's name to prompt": "總是將角色名稱新增到提示詞中", "Generate only one line per request": "每次請求僅生成一行", "Trim Incomplete Sentences": "修剪不完整的句子", @@ -474,7 +474,7 @@ "Misc. Settings": "其他設定", "Collapse Consecutive Newlines": "折疊連續的換行符號", "Trim spaces": "修剪空格", - "Tokenizer": "分詞器(Tokenizer)", + "Tokenizer": "分詞器 Tokenizer", "Token Padding": "符記填充", "Start Reply With": "開始回覆", "AI reply prefix": "AI 回覆前綴", @@ -486,22 +486,22 @@ "Replace Macro in Custom Stopping Strings": "取代自訂停止字串中的巨集", "Auto-Continue": "自動繼續", "Allow for Chat Completion APIs": "允許聊天補充 API", - "Target length (tokens)": "目標長度(符記)", + "Target length (tokens)": "目標長度(符記)", "World Info": "世界資訊", "Locked = World Editor will stay open": "上鎖 = 世界編輯器將保持開啟", - "Worlds/Lorebooks": "世界/知識書", + "Worlds/Lorebooks": "世界/知識書", "Active World(s) for all chats": "所有聊天啟用中的世界書", "-- World Info not found --": "-- 未找到世界資訊 --", - "Global World Info/Lorebook activation settings": "全域世界資訊/傳說書啟動設定", + "Global World Info/Lorebook activation settings": "全域世界資訊/知識書啟動設定", "Click to expand": "點擊展開", "Scan Depth": "掃描深度", "Context %": "上下文百分比", "Budget Cap": "預算上限", - "(0 = disabled)": "(0 = 停用)", + "(0 = disabled)": "(0 = 停用)", "Scan chronologically until reached min entries or token budget.": "按時間順序掃描直到達到最小條目或符記預算", "Min Activations": "最小啟動次數", "Max Depth": "最大深度", - "(0 = unlimited, use budget)": "(0 = 無限制, 使用預算)", + "(0 = unlimited, use budget)": "(0 = 無限制,使用預算)", "Insertion Strategy": "插入策略", "Sorted Evenly": "均等排序", "Character Lore First": "角色知識書優先", @@ -523,12 +523,12 @@ "Open all Entries": "開啟所有條目", "Close all Entries": "關閉所有條目", "New Entry": "新條目", - "Fill empty Memo/Titles with Keywords": "用關鍵字填充空的備忘錄/標題", + "Fill empty Memo/Titles with Keywords": "用關鍵字填充空的備忘錄/標題", "Import World Info": "匯入世界資訊", "Export World Info": "匯出世界資訊", "Duplicate World Info": "複製世界資訊", "Delete World Info": "刪除世界資訊", - "Search...": "搜尋...", + "Search...": "搜尋⋯", "Search": "搜尋", "Priority": "優先順序", "Custom": "自訂", @@ -595,7 +595,7 @@ "No Text Shadows": "無文字陰影", "Reduce chat height, and put a static sprite behind the chat window": "減少聊天高度,並在聊天視窗後放置一個靜態 Sprite。", "Waifu Mode": "視覺小說模式", - "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "始終顯示聊天訊息的訊息動作上下文項目的完整列表,而不是將它們隱藏在「...」後面。", + "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "始終顯示聊天訊息的訊息動作上下文項目的完整列表,而不是將它們隱藏在「⋯」後面。", "Auto-Expand Message Actions": "展開訊息快速編輯選單", "Alternative UI for numeric sampling parameters with fewer steps": "數值取樣參數的替代 UI,步驟更少", "Zen Sliders": "Zen 滑桿", @@ -614,7 +614,7 @@ "Show the number of tokens in each message in the chat log": "在聊天記錄中顯示每條訊息中的符記數量", "Show Message Token Count": "顯示訊息符記數量", "Single-row message input area. Mobile only, no effect on PC": "單行訊息輸入區域。僅適用於行動裝置版。", - "Compact Input Area (Mobile)": "緊湊的輸入區域(行動版)", + "Compact Input Area (Mobile)": "緊湊的輸入區域(行動版)", "In the Character Management panel, show quick selection buttons for favorited characters": "在角色管理面板中,顯示加入到最愛角色的快速選擇按鈕。", "Characters Hotswap": "角色卡快捷選單", "Enable magnification for zoomed avatar display.": "啟用放大顯示頭像", @@ -630,11 +630,11 @@ "Created by": "建立者", "Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊配對,並通過所有資料欄位在列表中搜尋角色,而不僅僅是通過名稱子字串。", "Advanced Character Search": "進階角色搜尋", - "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果選中並且角色卡包含提示詞覆寫(系統提示詞),則使用該提示詞。", - "Prefer Character Card Prompt": "偏好角色卡提示詞", - "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "如果選中並且角色卡包含越獄覆寫(歷史指示後),則使用該提示詞。", + "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果選中並且角色卡包含提示詞覆寫(系統提示詞),則使用該提示詞。", + "Prefer Character Card Prompt": "偏好角色卡主要提示詞", + "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "如果選中並且角色卡包含越獄覆寫(聊天歷史後指示),則使用該提示詞。", "Prefer Character Card Jailbreak": "偏好角色卡越獄", - "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和調整匯入的角色圖像大小。未勾選時將會裁剪/調整大小到 512x768。", + "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和調整匯入的角色圖像大小。未勾選時將會裁剪/調整大小到 512x768。", "Never resize avatars": "永不調整頭像大小", "Show actual file names on the disk, in the characters list display only": "僅在角色列表顯示實際檔案名稱。", "Show avatar filenames": "顯示頭像檔案名", @@ -656,23 +656,23 @@ "Background Sound Only": "僅作為背景音效", "Reduce the formatting requirements on API URLs": "降低 API URL 的格式要求。", "Relaxed API URLS": "寬鬆的 API URL 格式", - "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "當新角色含有知識書時,詢問是否要匯入嵌入的世界資訊/知識書。如果未選中,則會顯示簡短的訊息。", + "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "當新角色含有知識書時,詢問是否要匯入嵌入的世界資訊/知識書。如果未選中,則會顯示簡短的訊息。", "Lorebook Import Dialog": "匯入知識書對話框", "Restore unsaved user input on page refresh": "在頁面刷新時還原未儲存的使用者輸入。", "Restore User Input": "還原使用者輸入", "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允許通過拖動重新定位某些 UI 元素。僅適用於 PC 版。", "Movable UI Panels": "可移動的 UI 面板", - "MovingUI preset. Predefined/saved draggable positions": "MovingUI 預設。預先定義/儲存可拖動位置。", + "MovingUI preset. Predefined/saved draggable positions": "MovingUI 預設。預先定義/儲存可拖動位置。", "MUI Preset": "MUI 預設", "Save movingUI changes to a new file": "將移動 UI 變更儲存到新檔案", - "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置", + "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置", "Apply a custom CSS style to all of the ST GUI": "將自訂 CSS 樣式應用於所有 ST GUI", "Custom CSS": "自訂 CSS", "Expand the editor": "展開編輯器", - "Chat/Message Handling": "聊天/訊息處理", + "Chat/Message Handling": "聊天/訊息處理", "# Messages to Load": "每頁載入的訊息數", "The number of chat history messages to load before pagination.": "每頁載入的聊天歷史訊息數", - "(0 = All)": "(0 = 全部)", + "(0 = All)": "(0 = 全部)", "Streaming FPS": "串流 FPS", "Update speed of streamed text.": "更新串流文字的速度", "Example Messages Behavior": "訊息行為範例:", @@ -681,10 +681,10 @@ "Never include examples": "永不包含範例", "Send on Enter": "按下 Enter 鍵發送:", "Disabled": "停用", - "Automatic (PC)": "自動(PC)", + "Automatic (PC)": "自動(PC)", "Press Send to continue": "按下傳送繼續", - "Show a button in the input area to ask the AI to continue (extend) its last message": "在輸入區域顯示一個按鈕,請求 AI 繼續(擴充)其最後一則訊息", - "Quick 'Continue' button": "快速「繼續」按鈕", + "Show a button in the input area to ask the AI to continue (extend) its last message": "在輸入區域顯示一個按鈕,請求 AI 繼續(擴充)其最後一則訊息", + "Quick 'Continue' button": "快速「繼續回應」按鈕", "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在最後的聊天訊息中顯示箭頭按鈕,以生成替代的 AI 回應。適用於 PC 和行動裝置版", "Swipes": "滑動", "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允許在最後的聊天訊息上使用滑動手勢以觸發滑動生成。僅適用於行動裝置版。", @@ -766,7 +766,7 @@ "Restore": "還原", "Create a dummy persona": "建立一個虛構使用者角色", "Create": "建立", - "Toggle grid view": "切換網格視圖", + "Toggle grid view": "切換為網格視圖", "No persona description": "無使用者角色描述", "Name": "名稱", "Enter your name": "輸入您的名字", @@ -777,7 +777,7 @@ "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "範例:[{{user}} 是一個 28 歲的羅馬尼亞貓娘。]", "Tokens persona description": "符記角色描述", "Position:": "位置:", - "In Story String / Prompt Manager": "在故事字串 / 提示詞管理器", + "In Story String / Prompt Manager": "在故事字串/提示詞管理器", "Top of Author's Note": "作者備註的頂部", "Bottom of Author's Note": "作者備註的底部", "In-chat @ Depth": "聊天中 @ 深度", @@ -789,11 +789,11 @@ "Show notifications on switching personas": "切換角色時顯示通知", "Character Management": "角色管理", "Locked = Character Management panel will stay open": "上鎖 = 角色管理面板將保持開啟", - "Select/Create Characters": "選擇/建立角色", + "Select/Create Characters": "選擇/建立角色", "Favorite characters to add them to HotSwaps": "將角色加入到最愛來新增到快速切換", "Token counts may be inaccurate and provided just for reference.": "符記計數可能不準確,僅供參考。", "Total tokens": "符記總計", - "Calculating...": "計算中...", + "Calculating...": "計算中⋯", "Tokens": "符記", "Permanent tokens": "永久符記", "Permanent": "永久", @@ -810,21 +810,21 @@ "Duplicate Character": "複製角色", "Create Character": "建立角色", "Delete Character": "刪除角色", - "More...": "更多...", + "More...": "更多⋯", "Link to World Info": "連結到世界資訊", "Import Card Lore": "匯入卡片中的知識書", "Scenario Override": "情境覆寫", "Convert to Persona": "轉換為使用者角色", "Rename": "重新命名", "Link to Source": "連結到來源", - "Replace / Update": "取代 / 更新", + "Replace / Update": "取代/更新", "Import Tags": "匯入標籤", - "Search / Create Tags": "搜尋/建立標籤", + "Search / Create Tags": "搜尋/建立標籤", "View all tags": "查看所有標籤", "Creator's Notes": "建立者備註", - "Show / Hide Description and First Message": "顯示/隱藏描述和第一則訊息", + "Show / Hide Description and First Message": "顯示/隱藏描述和第一則訊息", "Character Description": "角色描述", - "Click to allow/forbid the use of external media for this character.": "點選以允許/禁止此角色使用外部媒體", + "Click to allow/forbid the use of external media for this character.": "點選以允許/禁止此角色使用外部媒體", "Ext. Media": "外部媒體權限", "Describe your character's physical and mental traits here.": "在此描述角色的身體和心理特徵。", "First message": "初始訊息", @@ -832,22 +832,22 @@ "Alt. Greetings": "額外問候語", "This will be the first message from the character that starts every chat.": "這將是每次聊天開始時角色發送的第一則訊息。", "Group Controls": "群組控制", - "Chat Name (Optional)": "聊天名稱(選填)", + "Chat Name (Optional)": "聊天名稱(選填)", "Click to select a new avatar for this group": "點選以選擇此群組的新頭像", "Group reply strategy": "群組回應策略", "Natural order": "自然順序", "List order": "清單順序", "Group generation handling mode": "群組生成處理模式", "Swap character cards": "交換角色卡", - "Join character cards (exclude muted)": "加入角色卡(排除靜音)", - "Join character cards (include muted)": "加入角色卡(包括靜音)", + "Join character cards (exclude muted)": "加入角色卡(排除靜音)", + "Join character cards (include muted)": "加入角色卡(包括靜音)", "Inserted before each part of the joined fields.": "插入在合併欄位的每一部分之前。", "Join Prefix": "加入前綴", - "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and with the name of the part (e.g.: description, personality, scenario, etc.)": "當選擇“加入角色卡”時,角色的所有對應欄位將連接在一起。\r這意味著,例如在故事字串中,所有角色描述都將連接到一個大文字中。\r如果您希望分隔這些字段,可以在此處定義前綴或後綴。\r\r該值支援普通宏,並且還將 {{char}} 替換為相關字元的名稱,並將 替換為部分的名稱(例如:描述、個性、場景等)", + "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and with the name of the part (e.g.: description, personality, scenario, etc.)": "當選擇「加入角色卡」時,角色的所有對應欄位將連接在一起。\r這意味著,例如在故事字串中,所有角色描述都將連接到一個大文字中。\r如果您希望分隔這些字段,可以在此處定義前綴或後綴。\r\r該值支援普通宏,並且還將 {{char}} 替換為相關字元的名稱,並將 替換為部分的名稱(例如:描述、個性、場景等)", "Inserted after each part of the joined fields.": "插入在合併欄位的每一部分之後。", "Join Suffix": "加入後綴", "Set a group chat scenario": "設定群組聊天情境", - "Click to allow/forbid the use of external media for this group.": "點選以允許/禁止此群組使用外部媒體", + "Click to allow/forbid the use of external media for this group.": "點選以允許/禁止此群組使用外部媒體", "Restore collage avatar": "還原預設頭像", "Allow self responses": "允許自我回應", "Auto Mode": "自動模式", @@ -871,8 +871,8 @@ "Most tokens": "最多符記", "Least tokens": "最少符記", "Random": "隨機", - "Toggle character grid view": "切換角色網格視圖", - "Bulk_edit_characters": "批量編輯角色\n\n點選以切換角色\nShift + 點選以選擇/取消選擇一範圍的角色\n右鍵以查看動作", + "Toggle character grid view": "切換為角色網格視圖", + "Bulk_edit_characters": "批量編輯角色\n\n點選以切換角色\nShift + 點選以選擇/取消選擇一範圍的角色\n右鍵以查看動作", "Bulk select all characters": "全選所有角色", "Bulk delete characters": "批量刪除角色", "popup-button-save": "儲存", @@ -882,26 +882,26 @@ "popup-button-import": "匯入", "Advanced Defininitions": "- 進階定義", "Prompt Overrides": "提示詞覆寫", - "(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)", + "(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)", "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。", "Main Prompt": "主要提示詞", - "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此處的任何內容將取代此角色使用的預設主要提示詞。(v2 規範:system_prompt)", - "Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "此處的任何內容將取代此角色使用的預設越獄提示詞。(v2 規範:post_history_instructions)", - "Creator's Metadata (Not sent with the AI prompt)": "建立者的中繼資料(不會與 AI 提示詞一起發送)", + "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此處的任何內容將取代此角色使用的預設主要提示詞。(v2 規範:system_prompt)", + "Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "此處的任何內容將取代此角色使用的預設越獄提示詞。(v2 規範:post_history_instructions)", + "Creator's Metadata (Not sent with the AI prompt)": "建立者的中繼資料(不會與 AI 提示詞一起發送)", "Creator's Metadata": "建立者的中繼資料", - "(Not sent with the AI Prompt)": "(不與 AI 提示詞一起發送)", + "(Not sent with the AI Prompt)": "(不與 AI 提示詞一起發送)", "Everything here is optional": "此處所有內容均為選填", - "(Botmaker's name / Contact Info)": "(機器人建立者的名字 / 聯繫資訊)", - "(If you want to track character versions)": "(如果您想追蹤角色版本)", - "(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述機器人,提供使用技巧,或列出已測試的聊天模型。這將顯示在角色列表中。)", + "(Botmaker's name / Contact Info)": "(機器人建立者的名字/聯絡資訊)", + "(If you want to track character versions)": "(若您想追蹤角色版本)", + "(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)": "(描述機器人,提供使用技巧,或列出已測試的聊天模型。這將顯示在角色列表中。)", "Tags to Embed": "嵌入標籤", - "(Write a comma-separated list of tags)": "(使用逗號分隔每個標籤)", + "(Write a comma-separated list of tags)": "(使用逗號分隔每個標籤)", "Personality summary": "個性摘要", - "(A brief description of the personality)": "(個性的簡短描述)", - "Scenario": "情境", - "(Circumstances and context of the interaction)": "(互動情形與聊天背景)", + "(A brief description of the personality)": "(關於角色性格的簡要描述)", + "Scenario": "情境設想", + "(Circumstances and context of the interaction)": "(互動情形與聊天背景)", "Character's Note": "角色筆記", - "(Text to be inserted in-chat @ designated depth and role)": "(要在聊天中插入的文字 @ 指定深度和角色)", + "(Text to be inserted in-chat @ designated depth and role)": "(要在聊天中插入的文字 @ 指定深度和角色)", "@ Depth": "@ 深度", "Role": "角色", "Talkativeness": "健談度", @@ -913,7 +913,7 @@ "Chatty": "健談", "Examples of dialogue": "對話範例", "Important to set the character's writing style.": "設定角色的寫作樣式很重要。", - "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天對話範例。每個範例以新的行並以「START」開始。)", + "(Examples of chat dialog. Begin each example with START on a new line.)": "(聊天對話範例。每個範例以新的行並以「START」開始。)", "Save": "儲存", "Chat History": "聊天記錄", "Import Chat": "匯入聊天", @@ -924,7 +924,7 @@ "Delete background": "刪除背景圖片", "Chat Scenario Override": "聊天情境覆寫", "Remove": "刪除", - "Type here...": "在此輸入...", + "Type here...": "在此輸入⋯", "Chat Lorebook": "聊天知識書", "Chat Lorebook for": "聊天知識書", "chat_world_template_txt": "選定的世界資訊將附加到此聊天。\n在生成 AI 回覆時,它將與全域和角色知識書中的條目結合。", @@ -943,7 +943,7 @@ "Drag to reorder tag": "拖動以重新排序標籤", "Use tag as folder": "將標籤用作資料夾", "Delete tag": "刪除標籤", - "Entry Title/Memo": "條目標題/備註", + "Entry Title/Memo": "條目標題/備註", "WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized❌ Disabled": "世界資訊條目狀態:🔵常數 🟢正常 🔗向量 ❌停用", "WI_Entry_Status_Constant": "🔵", "WI_Entry_Status_Normal": "🟢", @@ -962,11 +962,11 @@ "Depth": "深度", "Order:": "順序:", "Order": "順序", - "Trigger %:": "觸發%", + "Trigger %:": "觸發%", "Probability": "機率", "Duplicate world info entry": "複製世界資訊物件", "Delete world info entry": "刪除世界資訊物件", - "Comma separated (required)": "逗號分隔(必填)", + "Comma separated (required)": "逗號分隔(必填)", "Primary Keywords": "主要關鍵字", "Keywords or Regexes": "關鍵字或正規表示式", "Comma separated list": "逗號分隔列表", @@ -976,27 +976,27 @@ "AND ALL": "AND 所有", "NOT ALL": "NOT 所有", "NOT ANY": "NOT 任何", - "(ignored if empty)": "(如果為空則忽略)", + "(ignored if empty)": "(如果為空則忽略)", "Optional Filter": "選填過濾器", - "Keywords or Regexes (ignored if empty)": "關鍵字或正規表示式(如果為空則忽略)", - "Comma separated list (ignored if empty)": "逗號分隔列表(如果為空則忽略)", + "Keywords or Regexes (ignored if empty)": "關鍵字或正規表示式(如果為空則忽略)", + "Comma separated list (ignored if empty)": "逗號分隔列表(如果為空則忽略)", "Use global setting": "使用全域設定", "Case-Sensitive": "區分大小寫", "Yes": "是", "No": "否", "Can be used to automatically activate Quick Replies": "可用於自動啟用快速回覆", "Automation ID": "自動化 ID", - "( None )": "( 無 )", + "( None )": "(無)", "Content": "內容", "Exclude from recursion": "不可遞迴(此條目不會被其他條目啟用)", - "Prevent further recursion (this entry will not activate others)": "防止進一步遞迴(此條目不會啟用其他條目)", - "Delay until recursion (this entry can only be activated on recursive checking)": "延遲遞迴(此條目只能在遞迴檢查時啟用)", + "Prevent further recursion (this entry will not activate others)": "防止進一步遞迴(此條目不會啟用其他條目)", + "Delay until recursion (this entry can only be activated on recursive checking)": "延遲遞迴(此條目只能在遞迴檢查時啟用)", "What this keyword should mean to the AI, sent verbatim": "這個關鍵字對 AI 應意味著什麼,逐字發送", "Filter to Character(s)": "角色篩選", "Character Exclusion": "角色排除", "-- Characters not found --": "-- 未找到角色 --", "Inclusion Group": "包含的群組", - "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "如果觸發多個條目,包含群組可確保一次僅啟動一組中的一個條目。\r支援多個以逗號分隔的群組。\r\r文件:世界資訊 - 包容性集團", + "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "如果觸發多個條目,包含群組可確保一次僅啟動一組中的一個條目。\r支援多個以逗號分隔的群組。\r\r文件:世界資訊——包容性集團", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "優先考慮此條目:選取後,此條目將在所有選擇中優先。\r如果有多個優先級,則選擇「順序」最高的一個。", "Only one entry with the same label will be activated": "僅會啟用具有相同標籤的一個條目", "A relative likelihood of entry activation within the group": "群組內條目啟用的相對可能性", @@ -1051,9 +1051,9 @@ "Discord server": "不和諧伺服器", "welcome_message_part_7": "取得公告和資訊。", "SillyTavern is aimed at advanced users.": "SillyTavern 專為進階使用者設計", - "If you're new to this, enable the simplified UI mode below.": "如果您是新手,請啟用下方的簡化 UI 模式", + "If you're new to this, enable the simplified UI mode below.": "如果您是新手,請啟用下方的簡易 UI 模式", "Change it later in the 'User Settings' panel.": "稍後可在「使用者設定」面板中變更", - "Enable simple UI mode": "啟用簡單 UI 模式", + "Enable simple UI mode": "啟用簡易 UI 模式", "Looking for AI characters?": "正在尋找 AI 角色?", "onboarding_import": "匯入", "from supported sources or view": "從支援的來源或檢視", @@ -1074,8 +1074,8 @@ "Alternate Greetings": "額外問候語", "Alternate_Greetings_desc": "這些將在開始新聊天時顯示為第一則訊息的滑動選項。\n群組成員可以選擇其中之一來開始對話。", "Alternate Greetings Hint": "額外問候語的提示訊息", - "(This will be the first message from the character that starts every chat)": "(這將是每次聊天開始時角色發送的第一則訊息)", - "Forbid Media Override explanation": "此角色/群組在聊天中使用外部媒體的能力。", + "(This will be the first message from the character that starts every chat)": "(這將是每次聊天開始時角色發送的第一則訊息)", + "Forbid Media Override explanation": "此角色/群組在聊天中使用外部媒體的能力。", "Forbid Media Override subtitle": "禁止媒體覆寫副標題", "Always forbidden": "總是禁止", "Always allowed": "總是允許", @@ -1084,13 +1084,13 @@ "Unique to this chat": "此聊天獨有", "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "檢查點繼承其上級的註釋,之後可以單獨更改", "Include in World Info Scanning": "包括在世界資訊掃描中", - "Before Main Prompt / Story String": "在主要提示詞 / 故事字串之前", - "After Main Prompt / Story String": "在主要提示詞 / 故事字串之後", + "Before Main Prompt / Story String": "在主要提示詞/故事字串之前", + "After Main Prompt / Story String": "在主要提示詞/故事字串之後", "as": "作為", "Insertion Frequency": "插入頻率", - "(0 = Disable, 1 = Always)": "(0 = 停用, 1 = 永久)", + "(0 = Disable, 1 = Always)": "(0 = 停用, 1 = 永久)", "User inputs until next insertion:": "使用者輸入直到下一次插入:", - "Character Author's Note (Private)": "角色作者備註(私人)", + "Character Author's Note (Private)": "角色作者備註(私人)", "Won't be shared with the character card on export.": "不會在匯出時與角色卡共享", "Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.": "將自動新增為此角色的作者備註。將在群組中使用,但在群組聊天開啟時無法修改", "Use character author's note": "使用角色作者備註", @@ -1107,8 +1107,8 @@ "Global CFG": "全域 CFG", "Will be used as the default CFG options for every chat unless overridden.": "將作為每次聊天的預設 CFG 選項,除非被覆寫", "CFG Prompt Cascading": "CFG 提示詞級聯", - "Combine positive/negative prompts from other boxes.": "結合其他文字方塊中的正/負提示詞", - "For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "例如,勾選聊天、全域和角色框將所有負面提示詞合併為一個逗號分隔的字串", + "Combine positive/negative prompts from other boxes.": "結合其他文字方塊中的正/負提示詞", + "For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.": "例如:勾選聊天、全域和角色框將所有負面提示詞合併為一個逗號分隔的字串", "Always Include": "總是包含", "Chat Negatives": "聊天負面提示詞", "Character Negatives": "角色負面提示詞", @@ -1155,15 +1155,15 @@ "File per article": "每篇文章一個檔案", "Each article will be saved as a separate file.": "每篇文章將另存為一個檔案。", "Data Bank": "資料儲藏庫", - "These files will be available for extensions that support attachments (e.g. Vector Storage).": "這些檔案將可用於支援附件的擴充功能(例如向量存儲)。", + "These files will be available for extensions that support attachments (e.g. Vector Storage).": "這些檔案將可用於支援附件的擴充功能(例如向量存儲)。", "Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.": "支援的檔案類型:純文字,PDF,Markdown,HTML,EPUB。", "Drag and drop files here to upload.": "拖放檔案到這裡以上傳。", - "Date (Newest First)": "日期(最新優先)", - "Date (Oldest First)": "日期(最舊優先)", - "Name (A-Z)": "名稱(A-Z)", - "Name (Z-A)": "名稱(Z-A)", - "Size (Smallest First)": "尺寸(由小到大)", - "Size (Largest First)": "尺寸(由大到小)", + "Date (Newest First)": "日期(最新優先)", + "Date (Oldest First)": "日期(最舊優先)", + "Name (A-Z)": "名稱(A-Z)", + "Name (Z-A)": "名稱(Z-A)", + "Size (Smallest First)": "尺寸(由小到大)", + "Size (Largest First)": "尺寸(由大到小)", "Bulk Edit": "批次編輯", "Select All": "全選", "Select None": "選擇無", @@ -1176,7 +1176,7 @@ "These files are available to all characters in the current chat.": "這些檔案在此聊天中的所有角色中可用。", "Enter a base URL of the MediaWiki to scrape.": "輸入要抓取的 MediaWiki 的基礎 URL。", "Don't include the page name!": "不要包括頁面名稱!", - "Enter web URLs to scrape (one per line):": "輸入要抓取的網頁 URL(每行一個):", + "Enter web URLs to scrape (one per line):": "輸入要抓取的網頁 URL(每行一個):", "Enter a video URL to download its transcript.": "輸入影片 URL 來下載其文字記錄。", "Expression API": "表達 API", "Fallback Expression": "回退表達式", @@ -1184,12 +1184,12 @@ "ext_sum_main_api": "主要API", "ext_sum_current_summary": "目前摘要:", "ext_sum_restore_previous": "還原上一個", - "ext_sum_memory_placeholder": "摘要將在此處產生...", + "ext_sum_memory_placeholder": "將在此產生摘要⋯", "Trigger a summary update right now.": "立即觸發摘要更新。", "ext_sum_force_text": "現在總結一下", "Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "停用自動摘要更新。暫停時,摘要保持原樣。您仍然可以透過按下「立即匯總」按鈕強制更新(僅適用於 Main API)。", "ext_sum_pause": "暫停", - "Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "從要總結的文本中省略世界資訊和作者備註。僅在使用主要 API 時有效。額外 API 總是省略 WI/AN。", + "Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "從要總結的文本中省略世界資訊和作者備註。僅在使用主要 API 時有效。額外 API 總是省略世界資訊和作者備註。", "ext_sum_no_wi_an": "無法連接到網際網路", "ext_sum_settings_tip": "編輯摘要提示、插入位置等", "ext_sum_settings": "摘要設定", @@ -1206,9 +1206,9 @@ "ext_sum_target_length_1": "目標摘要長度", "ext_sum_target_length_2": "(", "ext_sum_target_length_3": "字)", - "ext_sum_api_response_length_1": "API響應長度", + "ext_sum_api_response_length_1": "API 響應長度", "ext_sum_api_response_length_2": "(", - "ext_sum_api_response_length_3": "代幣)", + "ext_sum_api_response_length_3": "個符記)", "ext_sum_0_default": "0 = 預設", "ext_sum_raw_max_msg": "[原始] 每個請求的最大訊息數", "ext_sum_0_unlimited": "0 = 無限制", @@ -1234,9 +1234,9 @@ "ext_regex_scoped_scripts_desc": "僅適用於此角色。儲存到卡片資料中。", "Regex Editor": "正規表示式編輯器", "Test Mode": "測試模式", - "ext_regex_desc": "正規表示式(Regex)是一種使用正規表示式尋找/取代字串的工具。如果您想了解更多,請點選標題旁邊的(?)", + "ext_regex_desc": "正規表示式(Regex)是一種使用正規表示式尋找/取代字串的工具。如果您想了解更多,請點選標題旁邊的「?」", "Input": "輸入", - "ext_regex_test_input_placeholder": "在此輸入...", + "ext_regex_test_input_placeholder": "在此輸入⋯", "Output": "輸出", "ext_regex_output_placeholder": "空的", "Script Name": "腳本名稱", @@ -1256,7 +1256,7 @@ "ext_regex_other_options": "其他選項", "Only Format Display": "僅格式化顯示", "ext_regex_only_format_prompt_desc": "聊天記錄不會更改,只會更改發送請求時的提示(生成時)。", - "Only Format Prompt (?)": "僅格式化提示詞(?)", + "Only Format Prompt (?)": "僅格式化提示詞(?)", "Run On Edit": "編輯時執行", "ext_regex_substitute_regex_desc": "在執行「尋找正規表示式」前取代 {{macros}}", "Substitute Regex": "取代正規表示式", @@ -1285,11 +1285,11 @@ "sd_multimodal_captioning_txt": "對肖像使用多模態模型描述", "sd_expand": "使用文字生成模型自動擴充提示詞。", "sd_expand_txt": "自動擴充提示詞", - "sd_snap": "對於具有特定長寬比的生成請求(如肖像、背景),將其調整至最接近的已知解析度,同時儘量保持絕對像素數(建議用於SDXL)。", + "sd_snap": "對於具有特定長寬比的生成請求(如肖像、背景),將其調整至最接近的已知解析度,同時儘量保持絕對像素數(建議用於 SDXL)。", "sd_snap_txt": "自動調整解析度", "Source": "來源", "sd_auto_url": "範例: {{auto_url}}", - "Authentication (optional)": "授權驗證(選填)", + "Authentication (optional)": "授權驗證(選填)", "Example: username:password": "範例:帳號:密碼", "Important:": "重要:", "sd_auto_auth_warning_1": "使用", @@ -1300,7 +1300,7 @@ "The server must be accessible from the SillyTavern host machine.": "伺服器必須能夠被 SillyTavern 主機存取。", "Hint: Save an API key in Horde KoboldAI API settings to use it here.": "提示訊息:在 Horde KoboldAI API 設定中儲存一個 API 金鑰,以便在此使用。", "Allow NSFW images from Horde": "允許來自 Horde 的 NSFW 圖片", - "Sanitize prompts (recommended)": "清理提示詞(建議)", + "Sanitize prompts (recommended)": "清理提示詞(建議)", "Automatically adjust generation parameters to ensure free image generations.": "自動調整生成參數以確保生成免費的圖片。", "Avoid spending Anlas": "避免消耗 Anlas 點數", "Opus tier": "Opus 級別", @@ -1318,12 +1318,12 @@ "Refine": "精煉", "Decrisper": "德克里斯珀", "Sampling steps": "取樣步數", - "Width": "寬度(W)", - "Height": "高度(H)", + "Width": "寬度(Width)", + "Height": "高度(Height)", "Resolution": "解析度", "Model": "模型", "Sampling method": "取樣方法", - "Karras (not all samplers supported)": "Karras(並非所有取樣器都支援)", + "Karras (not all samplers supported)": "Karras(並非所有取樣器均受支援)", "SMEA versions of samplers are modified to perform better at high resolution.": "SMEA 版本的取樣器經過修改,能在高解析度下表現更佳。", "SMEA": "SMEA", "DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.": "SMEA 取樣器的 DYN 變體通常會產生更多樣化的輸出,但在非常高的解析度下可能會失敗。", @@ -1334,7 +1334,7 @@ "Upscaler": "放大演算法", "Upscale by": "放大倍率", "Denoising strength": "重繪幅度", - "Hires steps (2nd pass)": "高解析步驟 (2nd pass)", + "Hires steps (2nd pass)": "高解析步驟(2nd pass)", "Preset for prompt prefix and negative prompt": "提示詞前綴和負面提示詞的預設設定", "Style": "樣式", "Save style": "儲存樣式", @@ -1359,7 +1359,7 @@ "User Handle:": "使用者控制代碼:", "Password:": "密碼:", "Confirm Password:": "確認密碼:", - "This will create a new subfolder...": "這將建立一個新的子資料夾...", + "This will create a new subfolder...": "這將建立一個新的子資料夾⋯", "Current Password:": "目前密碼:", "New Password:": "新密碼:", "Confirm New Password:": "確認新密碼:", @@ -1374,11 +1374,11 @@ "Import Characters": "匯入角色", "Enter the URL of the content to import": "輸入要匯入的內容的 URL", "Supported sources:": "支持的來源:", - "char_import_1": "Chub角色(直接連結或ID)", + "char_import_1": "Chub角色(直接連結或 ID)", "char_import_example": "例子:", - "char_import_2": "Chub Lorebook(直接連結或ID)", - "char_import_3": "JanitorAI 角色(直接連結或 UUID)", - "char_import_4": "Pygmalion.chat 角色(直接連結或 UUID)", + "char_import_2": "Chub Lorebook(直接連結或 ID)", + "char_import_3": "JanitorAI 角色(直接連結或 ID)", + "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)", "char_import_5": "AICharacterCard.com 角色(直接連結或 ID)", "char_import_6": "直接 PNG 連結(請參閱", "char_import_7": "對於允許的主機)", @@ -1406,7 +1406,7 @@ "Restore this snapshot": "還原此快照", "Hi,": "嗨,", "To enable multi-account features, restart the SillyTavern server with": "要啟用多帳號功能,請在 config.yaml 文件中將", - "set to true in the config.yaml file.": "設為 true,然後重啟 SillyTavern 伺服器。", + "set to true in the config.yaml file.": "設為 true 後重啟 SillyTavern 伺服器。", "Account Info": "帳號資訊", "To change your user avatar, use the buttons below or select a default persona in the Persona Management menu.": "要更改您的使用者頭像,請使用下方按鈕或在使用者角色管理選單中選擇一個預設人物。", "Set your custom avatar.": "設定您的頭像。", @@ -1442,8 +1442,8 @@ "Still have questions?": "還有問題嗎?", "Join the SillyTavern Discord": "加入 SillyTavern Discord", "Post a GitHub issue": "發布 GitHub 問題", - "Contact the developers": "聯繫開發者" - "(-1 for random)": "(-1 表示隨機)", + "Contact the developers": "聯繫開發者", + "(-1 for random)": "(-1 表示隨機)", "(Optional)": "(可選)", "(use _space": "(使用", "[no_connection_text]api_no_connection": "未連線⋯", @@ -1570,14 +1570,14 @@ "Assistant Message Sequences": "助理訊息序列", "Assistant Prefix": "助理訊息前綴", "Assistant Suffix": "助理訊息後綴", - "at the end of the URL!": "在 URL 末尾!", + "at the end of the URL!": "在 URL 末尾!", "Audio Playback Speed": "音檔播放速度", "Auto-select Input Text": "自動選擇輸入文本", "Automatically caption images": "自動為圖片添加字幕", "Auxiliary": "輔助", "Background Image": "背景圖片", "Block Entropy API Key": "Block Entropy API 金鑰", - "Can be set manually or with an _space": "可以手動設置或使用_space", + "Can be set manually or with an _space": "可以手動設置或使用 _space", "Caption Prompt": "字幕提示詞", "category": "類別", "Character Expressions": "角色情緒立繪", @@ -1589,7 +1589,7 @@ "Checked: all entries except ❌ status can be activated.": "勾選:除 ❌ 狀態外的所有條目均可啟動。", "Checkpoint Color": "檢查點節點邊框顏色", "Chunk boundary": "Chunk 邊界", - "Chunk overlap (%)": "Chunk 重疊 (%)", + "Chunk overlap (%)": "Chunk 重疊(%)", "Chunk size (chars)": "Chunk 大小(字符數)", "class": "所有類別", "Classifier API": "分類器 API", @@ -1608,7 +1608,7 @@ "Default / Fallback Expression": "默認/回退表情", "Delay": "延遲", "Delay until recursion (can only be activated on recursive checking)": "遞迴掃描延遲(僅在啟用遞迴掃描時可用)", - "Do not proceed if you do not agree to this!": "若不接受此條款,請勿繼續!", + "Do not proceed if you do not agree to this!": "若不同意此條款,請勿繼續!", "Edge Color": "邊緣顏色", "Edit captions before saving": "在保存前編輯字幕", "Enable for files": "為文件啟用", @@ -1625,18 +1625,18 @@ "Exclude Top Choices (XTC)": "排除頂部選項(XTC)", "Existing": "現有項目", "expression_label_pattern": "[情緒_標籤].[圖檔_格式]", - "ext_translate_auto_mode": "自動模式", + "ext_translate_auto_mode": "自動翻譯模式", "ext_translate_btn_chat": "翻譯聊天內容", "ext_translate_btn_input": "翻譯輸入內容", "ext_translate_clear": "清除翻譯", "ext_translate_mode_both": "翻譯輸入和回應", "ext_translate_mode_inputs": "僅翻譯輸入", "ext_translate_mode_none": "無翻譯", - "ext_translate_mode_provider": "提供者模式", + "ext_translate_mode_provider": "翻譯提供者", "ext_translate_mode_responses": "僅翻譯回應", "ext_translate_target_lang": "目標語言", "ext_translate_title": "聊天翻譯", - "Extensions Menu": "擴展選單", + "Extensions Menu": "擴充功能選單", "Extras": "擴充功能", "Extras API": "擴充功能 API", "Featherless Model Selection": "無羽模型選擇", @@ -1647,7 +1647,7 @@ "Group Scoring": "群組評分", "Groups and Past Personas": "群組與過去的使用者角色設定", "Hint:": "提示:", - "Hint: Set the URL in the API connection settings.": "提示:在 API 連線設置中設定URL。", + "Hint: Set the URL in the API connection settings.": "提示:在 API 連線設置中設定 URL。", "Horde": "Horde", "HuggingFace Token": "HuggingFace 符記", "Image Captioning": "圖片標註", @@ -1679,7 +1679,7 @@ "Max Entries": "最大條目數", "Max Recursion Steps": "最大遞迴步數", "Message attachments": "訊息附件", - "Message Template": "訊息範本", + "Message Template": "訊息模板", "Model ID": "模型 ID", "mui_reset": "重置", "Multimodal (OpenAI / Anthropic / llama / Google)": "多模態(OpenAI/Anthropic/llama/Google)", @@ -1700,7 +1700,7 @@ "prompt_manager_in_chat": "聊天中的提示詞管理", "prompt_post_processing_none": "無", "Purge Vectors": "清除向量", - "Put images with expressions there. File names should follow the pattern:": "將情緒圖片放置於此,檔案名稱應遵循以下格式:", + "Put images with expressions there. File names should follow the pattern:": "將表情圖片放置於此,檔案名稱應遵循以下格式:", "Quad": "四元數", "Query messages": "查詢訊息", "Quick Impersonate button": "快速模擬按鈕", @@ -1708,7 +1708,7 @@ "Remove all image overrides": "移除所有圖片覆蓋", "Restore default": "", "Retain#": "保留#", - "Retrieve chunks": "檢索 chunks", + "Retrieve chunks": "檢索 Chunks", "Sampler Order": "取樣順序", "Score threshold": "分數閾值", "sd_free_extend_small": "(互動/指令)", @@ -1716,14 +1716,14 @@ "sd_function_tool_txt": "使用功能工具", "sd_prompt_-1": "聊天訊息範本", "sd_prompt_-2": "功能工具提示描述", - "sd_prompt_0": "角色(您自己)", - "sd_prompt_1": "使用者(我)", + "sd_prompt_0": "角色(你,第二人稱)", + "sd_prompt_1": "使用者(我,第一人稱)", "sd_prompt_10": "肖像(多模態模式)", "sd_prompt_11": "自由模式(LLM 擴展)", "sd_prompt_2": "場景(完整故事)", "sd_prompt_3": "原始最後訊息", "sd_prompt_4": "最後訊息", - "sd_prompt_5": "肖像(您的臉)", + "sd_prompt_5": "肖像(你,第二人稱)", "sd_prompt_7": "背景", "sd_prompt_8": "角色(多模態模式)", "sd_prompt_9": "使用者(多模態模式)", @@ -1735,13 +1735,13 @@ "Separators as Stop Strings": "以分隔符作為停止字串", "Set the default and fallback expression being used when no matching expression is found.": "設定在無法配對到表情時所使用的預設和替代圖片。", "Set your API keys and endpoints in the API Connections tab first.": "請先在「API 連接」頁面中設定您的 API 密鑰和端點。", - "Show default images (emojis) if sprite missing": "無對應圖片時,顯示為預設表情符號 (emoji)", + "Show default images (emojis) if sprite missing": "無對應圖片時,顯示為預設表情符號(emoji)", "Show group chat queue": "顯示群組聊天隊列", - "Size threshold (KB)": "大小閾值 (KB)", + "Size threshold (KB)": "大小閾值(KB)", "Slash Command": "斜線命令", "space_ slash command.": "斜線命令。", "Sprite Folder Override": "表情立繪資料夾覆蓋", - "Sprite set:": "立繪集:", + "Sprite set:": "表情集:", "Show Gallery": "查看畫廊", "Sticky": "固定", "Style Preset": "預設樣式", @@ -1787,5 +1787,5 @@ "Will be used if the API doesnt support JSON schemas or function calling.": "若 API 不支持 JSON 模式或函數調用,將使用此設定。", "World Info settings": "世界資訊設定", "You are in offline mode. Click on the image below to set the expression.": "您目前為離線狀態,請點擊下方圖片進行表情設定。", - "You can find your API key in the Stability AI dashboard.": "API 密鑰可在 Stability AI 儀表板中查看。" + "You can find your API key in the Stability AI dashboard.": "API 金鑰可在 Stability AI 儀表板中查看。" } From 60caa6766720ec5e5dfebf99fb09f8c8b27ca896 Mon Sep 17 00:00:00 2001 From: Rivelle Date: Mon, 9 Dec 2024 14:29:10 +0800 Subject: [PATCH 021/222] Update zh-tw.json --- public/locales/zh-tw.json | 108 +++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index de1583607..c31b03f89 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -27,12 +27,12 @@ "Instruct": "指示", "Prose Augmenter": "散文增強器", "Text Adventure": "文字冒險", - "response legth(tokens)": "回應長度(符記數)", + "response legth(tokens)": "回應長度(符元數)", "Streaming": "串流", "Streaming_desc": "生成時逐位顯示回應。當此功能關閉時,回應將在完成後一次顯示。", - "context size(tokens)": "上下文長度(符記數)", + "context size(tokens)": "上下文長度(符元數)", "unlocked": "解鎖", - "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符記的上下文長度時啟用此功能", + "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符元的上下文長度時啟用此功能", "Max prompt cost:": "最大提示詞費用", "Display the response bit by bit as it is generated.": "生成時逐位顯示回應。", "When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在完成後一次性顯示。", @@ -52,8 +52,8 @@ "Very aggressive": "非常積極", "Unlocked Context Size": "解鎖的上下文長度", "Unrestricted maximum value for the context slider": "上下文滑桿的無限制最大值", - "Context Size (tokens)": "上下文長度(符記數)", - "Max Response Length (tokens)": "最大回應長度(符記數)", + "Context Size (tokens)": "上下文長度(符元數)", + "Max Response Length (tokens)": "最大回應長度(符元數)", "Multiple swipes per generation": "每次生成多次滑動", "Enable OpenAI completion streaming": "啟用 OpenAI 補充串流", "Frequency Penalty": "頻率懲罰", @@ -95,14 +95,14 @@ "Send this text instead of nothing when the text box is empty.": "當文字方塊為空時,發送此字串而不是空白。", "Seed": "種子", "Set to get deterministic results. Use -1 for random seed.": "設定以獲取確定性結果。使用 -1 作為隨機種子", - "Temperature controls the randomness in token selection": "溫度控制符記選擇中的隨機性", - "Top_K_desc": "Top K 設定可以選擇的最高符記數量。\n例如,Top K 為 20,這意味著只保留排名前 20 的符記(無論它們的機率是多樣還是有限的)。\n設定為 0 以停用。", - "Top_P_desc": "Top P(又名核心取樣)會將所有頂級符記加總,直到達到目標百分比。\n例如,如果前兩個符記都是 25%,而 Top P 設為 0.5,那麼只有前兩個符記會被考慮。\n設定為 1.0 以停用。", + "Temperature controls the randomness in token selection": "溫度控制符元選擇中的隨機性", + "Top_K_desc": "Top K 設定可以選擇的最高符元數量。\n例如,Top K 為 20,這意味著只保留排名前 20 的符元(無論它們的機率是多樣還是有限的)。\n設定為 0 以停用。", + "Top_P_desc": "Top P(又名核心取樣)會將所有頂級符元加總,直到達到目標百分比。\n例如,如果前兩個符元都是 25%,而 Top P 設為 0.5,那麼只有前兩個符元會被考慮。\n設定為 1.0 以停用。", "Typical P": "Typical P", - "Typical_P_desc": "Typical P 取樣根據符記偏離集合平均熵的程度進行優先排序。\n它會保留累積機率接近預設閾值(例如0.5)的符記,強調那些具有平均信息量的符記。\n設定為 1.0 以停用。", - "Min_P_desc": "Min P 設定基本最小機率。\n這個值會根據最高符記的機率進行調整。例如,如果最高符記機率為 80%,而 Min P 設為 0.1 ,那麼只有機率高於 8% 的符記會被考慮。\n設定為 0 以停用。", - "Top_A_desc": "Top A 根據最高符記機率的平方設定符記選擇的門檻。\n例如,如果 Top A 值為 0.2,而最高符記機率為 50%,那麼低於 5%(0.2 * 0.5^2) 的符記機率就會被排除。\n設定為 0 以停用。", - "Tail_Free_Sampling_desc": "無尾取樣(Tail-Free Sampling, TFS)會透過分析符記機率的變化率(使用導數)來尋找分佈中的低機率符記尾部。\n它會根據標準化的二階導數,保留直到某個閾值(例如0.3)的符記。\n數值越接近 0 ,表示會棄去越多符記。設定為 1.0 以停用。", + "Typical_P_desc": "Typical P 取樣根據符元偏離集合平均熵的程度進行優先排序。\n它會保留累積機率接近預設閾值(例如0.5)的符元,強調那些具有平均信息量的符元。\n設定為 1.0 以停用。", + "Min_P_desc": "Min P 設定基本最小機率。\n這個值會根據最高符元的機率進行調整。例如,如果最高符元機率為 80%,而 Min P 設為 0.1 ,那麼只有機率高於 8% 的符元會被考慮。\n設定為 0 以停用。", + "Top_A_desc": "Top A 根據最高符元機率的平方設定符元選擇的門檻。\n例如,如果 Top A 值為 0.2,而最高符元機率為 50%,那麼低於 5%(0.2 * 0.5^2) 的符元機率就會被排除。\n設定為 0 以停用。", + "Tail_Free_Sampling_desc": "無尾取樣(Tail-Free Sampling, TFS)會透過分析符元機率的變化率(使用導數)來尋找分佈中的低機率符元尾部。\n它會根據標準化的二階導數,保留直到某個閾值(例如0.3)的符元。\n數值越接近 0 ,表示會棄去越多符元。設定為 1.0 以停用。", "rep.pen range": "重複懲罰範圍", "Mirostat": "Mirostat", "Mode": "模式", @@ -111,8 +111,8 @@ "Mirostat_Tau_desc": "這個設定控制了 Mirostat 輸出的可變性。", "Eta": "Eta", "Mirostat_Eta_desc": "這個設定控制 Mirostat 的學習率。", - "Ban EOS Token": "禁止 EOS 符記", - "Ban_EOS_Token_desc": "對於 KoboldCpp(以及可能也適用於 KoboldAI 的其他符記),禁止使用序列結束 (End-of-Sequence, EOS) 符記。這個設定對於故事創作很有幫助,但不應該在聊天和指令模式中使用。", + "Ban EOS Token": "禁止 EOS 符元", + "Ban_EOS_Token_desc": "對於 KoboldCpp(以及可能也適用於 KoboldAI 的其他符元),禁止使用序列結束 (End-of-Sequence, EOS) 符元。這個設定對於故事創作很有幫助,但不應該在聊天和指令模式中使用。", "GBNF Grammar": "GBNF 語法", "Type in the desired custom grammar": "輸入所需的自定義語法", "Samplers Order": "取樣器順序", @@ -121,11 +121,11 @@ "Load koboldcpp order": "載入 koboldcpp 順序", "Preamble": "前言", "Use style tags to modify the writing style of the output.": "使用樣式標籤修改輸出的寫作樣式。", - "Banned Tokens": "禁止的符記", - "Sequences you don't want to appear in the output. One per line.": "您不希望在輸出中出現的序列。每行一個,使用文字或符記 ID。", + "Banned Tokens": "禁止的符元", + "Sequences you don't want to appear in the output. One per line.": "您不希望在輸出中出現的序列。每行一個,使用文字或符元 ID。", "Logit Bias": "Logit 偏差", "Add": "新增", - "Helps to ban or reenforce the usage of certain words": "有助於禁止或強化某些符記的使用", + "Helps to ban or reenforce the usage of certain words": "有助於禁止或強化某些符元的使用", "CFG Scale": "CFG 比例", "Negative Prompt": "負面提示詞", "Add text here that would make the AI generate things you don't want in your outputs.": "在這裡新增文字,使 AI 生成您不希望在輸出中出現的內容。", @@ -142,19 +142,19 @@ "Sampler Select": "選擇取樣器", "Customize displayed samplers or add custom samplers.": "自訂顯示的取樣器或新增自訂取樣器。", "Epsilon Cutoff": "Epsilon 截斷", - "Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled": "Epsilon 截斷設定排除符記的機率下限", + "Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled": "Epsilon 截斷設定排除符元的機率下限", "Eta Cutoff": "Eta 截斷", "Eta_Cutoff_desc": "Eta 截斷是特殊 Eta 取樣技術的主要參數。\n單位為 1e-4;合理值為 3。\n設為 0 以停用。\n詳情請參見 Hewitt 等人於 2022 年撰寫的論文《Truncation Sampling as Language Model Desmoothing》。", "rep.pen decay": "重複懲罰衰減", "Encoder Rep. Pen.": "編碼器重複懲罰", "No Repeat Ngram Size": "無重複 Ngram 大小", "Skew": "Skew", - "Max Tokens Second": "最大符記/秒", + "Max Tokens Second": "最大符元/秒", "Smooth Sampling": "平滑取樣", "Smooth_Sampling_desc": "允許您使用二次/三次變換來調整分佈。較低的平滑因子值將更具創造性,通常在 0.2-0.3 之間是最佳點(假設曲線=1)。較高的平滑曲線值會使曲線更陡峭,這將更加激烈地懲罰低概率選擇。1.0 的曲線值相當於僅使用平滑因子。", "Smoothing Factor": "平滑因子", "Smoothing Curve": "平滑曲線", - "DRY_Repetition_Penalty_desc": "DRY 會懲罰那些將輸入的結尾擴充為已在先前輸入中出現過序列的符記。將乘法器設為 0 以停用。", + "DRY_Repetition_Penalty_desc": "DRY 會懲罰那些將輸入的結尾擴充為已在先前輸入中出現過序列的符元。將乘法器設為 0 以停用。", "DRY Repetition Penalty": "DRY 重複懲罰", "DRY_Multiplier_desc": "將值設為大於 0 來啟用 DRY。控制對最短受懲罰序列的懲罰幅度。", "Multiplier": "乘法器", @@ -163,11 +163,11 @@ "DRY_Allowed_Length_desc": "可以重複而不受懲罰的最長序列長度。", "Allowed Length": "允許長度", "Penalty Range": "懲罰範圍", - "DRY_Sequence_Breakers_desc": "序列停止繼續配對的符記,使用引號包裹字串並以逗號分隔清單。", + "DRY_Sequence_Breakers_desc": "序列停止繼續配對的符元,使用引號包裹字串並以逗號分隔清單。", "Sequence Breakers": "序列中斷器", "JSON-serialized array of strings.": "序列化 JSON 的字串陣列。", "Dynamic Temperature": "動態溫度", - "Scale Temperature dynamically per token, based on the variation of probabilities": "根據機率變化,動態調整每個符記的溫度", + "Scale Temperature dynamically per token, based on the variation of probabilities": "根據機率變化,動態調整每個符元的溫度", "Minimum Temp": "最低溫度", "Maximum Temp": "最高溫度", "Exponent": "指數", @@ -187,17 +187,17 @@ "Penalty Alpha": "懲罰 Alpha", "Strength of the Contrastive Search regularization term. Set to 0 to disable CS": "對比搜尋正則化項的強度。設定為 0 以停用 CS", "Do Sample": "進行取樣", - "Add BOS Token": "新增 BOS 符記", + "Add BOS Token": "新增 BOS 符元", "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "在提示詞的開頭新增 bos_token。停用此功能可以使回應更具創造性", "Ban the eos_token. This forces the model to never end the generation prematurely": "禁止 eos_token。這迫使模型不會提前結束生成", - "Ignore EOS Token": "忽略 EOS 符記", - "Ignore the EOS Token even if it generates.": "即使生成也忽略 EOS 符記", - "Skip Special Tokens": "跳過特殊符記", + "Ignore EOS Token": "忽略 EOS 符元", + "Ignore the EOS Token even if it generates.": "即使生成也忽略 EOS 符元", + "Skip Special Tokens": "跳過特殊符元", "Temperature Last": "最後的溫度", - "Temperature_Last_desc": "使用最後應用溫度取樣器。這幾乎總是明智的做法。\n啟用時:首先取樣一組合理的符記,然後應用溫度來調整它們的相對機率(技術上講,是 logits)。\n停用時:首先應用溫度調整所有符記的相對機率,然後從中取樣合理的符記。\n停用「最後應用溫度取樣」會增加分佈尾部的概率,這傾向於放大獲得不連貫回應的機會。", + "Temperature_Last_desc": "使用最後應用溫度取樣器。這幾乎總是明智的做法。\n啟用時:首先取樣一組合理的符元,然後應用溫度來調整它們的相對機率(技術上講,是 logits)。\n停用時:首先應用溫度調整所有符元的相對機率,然後從中取樣合理的符元。\n停用「最後應用溫度取樣」會增加分佈尾部的概率,這傾向於放大獲得不連貫回應的機會。", "Speculative Ngram": "推測性 Ngram", "Use a different speculative decoding method without a draft model": "使用不含草稿模型的不同推測性解碼方法。", - "Spaces Between Special Tokens": "特殊符記之間的空格", + "Spaces Between Special Tokens": "特殊符元之間的空格", "LLaMA / Mistral / Yi models only": "僅限 LLaMA/Mistral/Yi 模型", "Example: some text [42, 69, 1337]": "範例:\n一些文字 [42, 69, 1337]", "Classifier Free Guidance. More helpful tip coming soon": "無分類器導引。更多有用提示訊息即將推出", @@ -246,7 +246,7 @@ "Use AI21 Tokenizer": "使用 AI21 分詞器", "Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.": "對於 Jurassic 模型使用適當的分詞器,比 GPT 的更高效", "Use Google Tokenizer": "使用 Google 分詞器", - "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "通過 Google 模型的 API 使用適當的分詞器。提示詞處理速度較慢,但提供更準確的符記計數。", + "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "通過 Google 模型的 API 使用適當的分詞器。提示詞處理速度較慢,但提供更準確的符元計數。", "Use system prompt": "使用系統提示詞", "(Gemini 1.5 Pro/Flash only)": "(僅限於 Gemini 1.5 Pro/Flash)", "Merges_all_system_messages_desc_1": "合併所有系統訊息,直到第一則非系統角色的訊息,並通過 google 的", @@ -263,7 +263,7 @@ "Delete preset": "刪除預設", "View / Edit bias preset": "查看/編輯 Bias 預設", "Add bias entry": "新增 Bias 條目", - "Most tokens have a leading space.": "大多數符記有前導空格", + "Most tokens have a leading space.": "大多數符元有前導空格", "API Connections": "API 連線", "Text Completion": "文字補充", "Chat Completion": "聊天補充", @@ -337,7 +337,7 @@ "koboldcpp API key (optional)": "KoboldCpp API 金鑰(可選)", "Example: 127.0.0.1:5001": "範例:127.0.0.1:5001", "Authorize": "授權", - "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用 OAuth 流程取得您的 OpenRouter API 符記。您將被重新導向到 openrouter.ai", + "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "使用 OAuth 流程取得您的 OpenRouter API 符元。您將被重新導向到 openrouter.ai", "Bypass status check": "繞過狀態檢查", "Chat Completion Source": "聊天補充來源", "Reverse Proxy": "反向代理伺服器", @@ -475,7 +475,7 @@ "Collapse Consecutive Newlines": "折疊連續的換行符號", "Trim spaces": "修剪空格", "Tokenizer": "分詞器 Tokenizer", - "Token Padding": "符記填充", + "Token Padding": "符元填充", "Start Reply With": "開始回覆", "AI reply prefix": "AI 回覆前綴", "Show reply prefix in chat": "在聊天中顯示回覆前綴", @@ -486,7 +486,7 @@ "Replace Macro in Custom Stopping Strings": "取代自訂停止字串中的巨集", "Auto-Continue": "自動繼續", "Allow for Chat Completion APIs": "允許聊天補充 API", - "Target length (tokens)": "目標長度(符記)", + "Target length (tokens)": "目標長度(符元)", "World Info": "世界資訊", "Locked = World Editor will stay open": "上鎖 = 世界編輯器將保持開啟", "Worlds/Lorebooks": "世界/知識書", @@ -498,7 +498,7 @@ "Context %": "上下文百分比", "Budget Cap": "預算上限", "(0 = disabled)": "(0 = 停用)", - "Scan chronologically until reached min entries or token budget.": "按時間順序掃描直到達到最小條目或符記預算", + "Scan chronologically until reached min entries or token budget.": "按時間順序掃描直到達到最小條目或符元預算", "Min Activations": "最小啟動次數", "Max Depth": "最大深度", "(0 = unlimited, use budget)": "(0 = 無限制,使用預算)", @@ -534,8 +534,8 @@ "Custom": "自訂", "Title A-Z": "標題 A-Z", "Title Z-A": "標題 Z-A", - "Tokens ↗": "符記 ↗", - "Tokens ↘": "符記 ↘", + "Tokens ↗": "符元 ↗", + "Tokens ↘": "符元 ↘", "Depth ↗": "深度 ↗", "Depth ↘": "深度 ↘", "Order ↗": "順序 ↗", @@ -611,8 +611,8 @@ "Message IDs": "訊息 ID", "Hide avatars in chat messages.": "在聊天訊息中隱藏頭像", "Hide Chat Avatars": "隱藏聊天頭像", - "Show the number of tokens in each message in the chat log": "在聊天記錄中顯示每條訊息中的符記數量", - "Show Message Token Count": "顯示訊息符記數量", + "Show the number of tokens in each message in the chat log": "在聊天記錄中顯示每條訊息中的符元數量", + "Show Message Token Count": "顯示訊息符元數量", "Single-row message input area. Mobile only, no effect on PC": "單行訊息輸入區域。僅適用於行動裝置版。", "Compact Input Area (Mobile)": "緊湊的輸入區域(行動版)", "In the Character Management panel, show quick selection buttons for favorited characters": "在角色管理面板中,顯示加入到最愛角色的快速選擇按鈕。", @@ -704,8 +704,8 @@ "Allow AI messages in groups to contain lines spoken by other group members": "允許群組中的 AI 訊息包含其他群組成員說的話", "Relax message trim in Groups": "放寬群組中的訊息修剪", "Log prompts to console": "將提示詞記錄到控制台", - "Requests logprobs from the API for the Token Probabilities feature": "從 API 請求 logprobs 用於符記機率功能。", - "Request token probabilities": "請求符記機率", + "Requests logprobs from the API for the Token Probabilities feature": "從 API 請求 logprobs 用於符元機率功能。", + "Request token probabilities": "請求符元機率", "Automatically reject and re-generate AI message based on configurable criteria": "根據可配置標準自動拒絕並重新生成 AI 訊息。", "Auto-swipe": "自動滑動", "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。", @@ -775,7 +775,7 @@ "Click to set user name for all messages": "點選以設定所有訊息的使用者名稱", "Persona Description": "使用者角色描述", "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "範例:[{{user}} 是一個 28 歲的羅馬尼亞貓娘。]", - "Tokens persona description": "符記角色描述", + "Tokens persona description": "符元角色描述", "Position:": "位置:", "In Story String / Prompt Manager": "在故事字串/提示詞管理器", "Top of Author's Note": "作者備註的頂部", @@ -791,16 +791,16 @@ "Locked = Character Management panel will stay open": "上鎖 = 角色管理面板將保持開啟", "Select/Create Characters": "選擇/建立角色", "Favorite characters to add them to HotSwaps": "將角色加入到最愛來新增到快速切換", - "Token counts may be inaccurate and provided just for reference.": "符記計數可能不準確,僅供參考。", - "Total tokens": "符記總計", + "Token counts may be inaccurate and provided just for reference.": "符元計數可能不準確,僅供參考。", + "Total tokens": "符元總計", "Calculating...": "計算中⋯", - "Tokens": "符記", - "Permanent tokens": "永久符記", + "Tokens": "符元", + "Permanent tokens": "永久符元", "Permanent": "永久", - "About Token 'Limits'": "關於符記數「限制」", + "About Token 'Limits'": "關於符元數「限制」", "Toggle character info panel": "切換角色資訊面板", "Name this character": "為此角色命名", - "extension_token_counter": "符記:", + "extension_token_counter": "符元:", "Click to select a new avatar for this character": "點選以選擇此角色的新頭像", "Add to Favorites": "新增至我的最愛", "Advanced Definition": "進階定義", @@ -868,8 +868,8 @@ "Recent": "最近", "Most chats": "最多聊天", "Least chats": "最少聊天", - "Most tokens": "最多符記", - "Least tokens": "最少符記", + "Most tokens": "最多符元", + "Least tokens": "最少符元", "Random": "隨機", "Toggle character grid view": "切換為角色網格視圖", "Bulk_edit_characters": "批量編輯角色\n\n點選以切換角色\nShift + 點選以選擇/取消選擇一範圍的角色\n右鍵以查看動作", @@ -1004,7 +1004,7 @@ "Selective": "選擇性", "Use Probability": "使用機率", "Add Memo": "新增備忘錄", - "Text or token ids": "文字或符記 ID", + "Text or token ids": "文字或符元 ID", "close": "關閉", "prompt_manager_edit": "編輯", "prompt_manager_name": "名稱", @@ -1115,8 +1115,8 @@ "Global Negatives": "全域負面提示詞", "Custom Separator:": "自訂分隔符:", "Insertion Depth:": "插入深度:", - "Token Probabilities": "符記機率", - "Select a token to see alternatives considered by the AI.": "選擇一個符記以檢視 AI 考慮的替代方案", + "Token Probabilities": "符元機率", + "Select a token to see alternatives considered by the AI.": "選擇一個符元以檢視 AI 考慮的替代方案", "Not connected to API!": "未連線到 API!", "Type a message, or /? for help": "輸入一則訊息,或輸入 /? 取得支援", "Continue script execution": "繼續腳本執行", @@ -1208,7 +1208,7 @@ "ext_sum_target_length_3": "字)", "ext_sum_api_response_length_1": "API 響應長度", "ext_sum_api_response_length_2": "(", - "ext_sum_api_response_length_3": "個符記)", + "ext_sum_api_response_length_3": "個符元)", "ext_sum_0_default": "0 = 預設", "ext_sum_raw_max_msg": "[原始] 每個請求的最大訊息數", "ext_sum_0_unlimited": "0 = 無限制", @@ -1649,7 +1649,7 @@ "Hint:": "提示:", "Hint: Set the URL in the API connection settings.": "提示:在 API 連線設置中設定 URL。", "Horde": "Horde", - "HuggingFace Token": "HuggingFace 符記", + "HuggingFace Token": "HuggingFace 符元", "Image Captioning": "圖片標註", "Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)", "Injection Position": "插入位置", From 3ed5d892f70448c209efe242d7f074968a0d8911 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:13:19 +0200 Subject: [PATCH 022/222] Fix bugs with the settings --- public/scripts/textgen-settings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index e5d018b74..ccbdc91c6 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -96,7 +96,7 @@ const APHRODITE_DEFAULT_ORDER = [ 'epsilon_cutoff', 'typical_p', 'quadratic', - 'xtc' + 'xtc', ]; const BIAS_KEY = '#textgenerationwebui_api-settings'; @@ -179,7 +179,7 @@ const settings = { banned_tokens: '', sampler_priority: OOBA_DEFAULT_ORDER, samplers: LLAMACPP_DEFAULT_ORDER, - samplers_priorties: APHRODITE_DEFAULT_ORDER, + samplers_priorities: APHRODITE_DEFAULT_ORDER, ignore_eos_token: false, spaces_between_special_tokens: true, speculative_ngram: false, @@ -879,7 +879,7 @@ function setSettingByName(setting, value, trigger) { return; } - if ('samplers_priority' === setting) { + if ('samplers_priorities' === setting) { value = Array.isArray(value) ? value : APHRODITE_DEFAULT_ORDER; insertMissingArrayItems(APHRODITE_DEFAULT_ORDER, value); sortAphroditeItemsByOrder(value); From 323b9407df6433f4251a780bad549cb6ed2d640b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:26:57 +0200 Subject: [PATCH 023/222] Small cosmetic fixes --- public/index.html | 2 +- public/scripts/textgen-settings.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index d8a679675..233ab710f 100644 --- a/public/index.html +++ b/public/index.html @@ -1780,7 +1780,7 @@ Sampler Order
-
+
Aphrodite only. Determines the order of samplers.
diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index ccbdc91c6..1a2306a72 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -571,7 +571,12 @@ function sortOobaItemsByOrder(orderArray) { }); } +/** + * Sorts the Aphrodite sampler items by the given order. + * @param {string[]} orderArray Sampler order array. + */ function sortAphroditeItemsByOrder(orderArray) { + console.debug('Preset samplers order: ', orderArray); const $container = $('#sampler_priority_container_aphrodite'); orderArray.forEach((name) => { From bcbfcb87b52c03f4d2aa50e2efe95ffafeda515d Mon Sep 17 00:00:00 2001 From: AlpinDale Date: Mon, 9 Dec 2024 14:05:33 +0000 Subject: [PATCH 024/222] aphrodite: send an empty sampler priority list if using the default order --- public/scripts/textgen-settings.js | 8 ++++++-- public/scripts/utils.js | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 1a2306a72..d93b787f8 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -16,7 +16,7 @@ import { power_user, registerDebugFunction } from './power-user.js'; import { getEventSourceStream } from './sse-stream.js'; import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer } from './textgen-models.js'; import { ENCODE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js'; -import { getSortableDelay, onlyUnique } from './utils.js'; +import { getSortableDelay, onlyUnique, arraysEqual } from './utils.js'; export const textgen_types = { OOBA: 'ooba', @@ -1316,7 +1316,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'nsigma': settings.nsigma, 'custom_token_bans': toIntArray(banned_tokens), 'no_repeat_ngram_size': settings.no_repeat_ngram_size, - 'sampler_priority': settings.type === APHRODITE ? settings.samplers_priorities : undefined, + 'sampler_priority': settings.type === APHRODITE && !arraysEqual( + settings.samplers_priorities, + APHRODITE_DEFAULT_ORDER) + ? settings.samplers_priorities + : undefined, }; if (settings.type === OPENROUTER) { diff --git a/public/scripts/utils.js b/public/scripts/utils.js index a198cb7db..f49333c48 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -2173,3 +2173,20 @@ export function getCharIndex(char) { if (index === -1) throw new Error(`Character not found: ${char.avatar}`); return index; } + +/** + * Compares two arrays for equality + * @param {any[]} a - The first array + * @param {any[]} b - The second array + * @returns {boolean} True if the arrays are equal, false otherwise + */ +export function arraysEqual(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} From cc73a45d1f0f59d31f8f5df8545928d9ddf107d5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:32:23 +0200 Subject: [PATCH 025/222] Make lint happy --- public/scripts/textgen-settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index d93b787f8..cc6aa0e78 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -1317,9 +1317,9 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'custom_token_bans': toIntArray(banned_tokens), 'no_repeat_ngram_size': settings.no_repeat_ngram_size, 'sampler_priority': settings.type === APHRODITE && !arraysEqual( - settings.samplers_priorities, + settings.samplers_priorities, APHRODITE_DEFAULT_ORDER) - ? settings.samplers_priorities + ? settings.samplers_priorities : undefined, }; From 3c82d961bdf364a9591366e6eddc4746ae8f275b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 9 Dec 2024 22:24:02 +0200 Subject: [PATCH 026/222] Batch extension version checks --- public/scripts/extensions.js | 37 +++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 9416c38e2..0229146b1 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -586,7 +586,6 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, const isUserAdmin = isAdmin(); const extensionIcon = getExtensionIcon(); const displayName = manifest.display_name; - let displayVersion = manifest.version ? ` v${manifest.version}` : ''; const externalId = name.replace('third-party', ''); let originHtml = ''; if (isExternal) { @@ -632,7 +631,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, ${originHtml} ${DOMPurify.sanitize(displayName)} - ${displayVersion} + ${modulesInfo} ${isExternal ? '' : ''} @@ -1032,6 +1031,29 @@ export function doDailyExtensionUpdatesCheck() { }, 1); } +const concurrencyLimit = 5; +let activeRequestsCount = 0; +const versionCheckQueue = []; + +function enqueueVersionCheck(fn) { + return new Promise((resolve, reject) => { + versionCheckQueue.push(() => fn().then(resolve).catch(reject)); + processVersionCheckQueue(); + }); +} + +function processVersionCheckQueue() { + if (activeRequestsCount >= concurrencyLimit || versionCheckQueue.length === 0) { + return; + } + activeRequestsCount++; + const fn = versionCheckQueue.shift(); + fn().finally(() => { + activeRequestsCount--; + processVersionCheckQueue(); + }); +} + /** * Performs a manual check for updates on all 3rd-party extensions. * @param {AbortSignal} abortSignal Signal to abort the operation @@ -1041,7 +1063,7 @@ async function checkForUpdatesManual(abortSignal) { const promises = []; for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party'))) { const externalId = id.replace('third-party', ''); - const promise = new Promise(async (resolve, reject) => { + const promise = enqueueVersionCheck(async () => { try { const data = await getExtensionVersion(externalId, abortSignal); const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`); @@ -1072,10 +1094,8 @@ async function checkForUpdatesManual(abortSignal) { versionElement.textContent += ` (${branch}-${commitHash.substring(0, 7)})`; } } - resolve(); } catch (error) { console.error('Error checking for extension updates', error); - reject(); } }); promises.push(promise); @@ -1111,17 +1131,16 @@ async function checkForExtensionUpdates(force) { console.debug(`Skipping global extension: ${manifest.display_name} (${id}) for non-admin user`); continue; } + if (manifest.auto_update && id.startsWith('third-party')) { - const promise = new Promise(async (resolve, reject) => { + const promise = enqueueVersionCheck(async () => { try { const data = await getExtensionVersion(id.replace('third-party', '')); - if (data.isUpToDate === false) { + if (!data.isUpToDate) { updatesAvailable.push(manifest.display_name); } - resolve(); } catch (error) { console.error('Error checking for extension updates', error); - reject(); } }); promises.push(promise); From f5088b398f0e192f7c4098eda75ff183affb8692 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 9 Dec 2024 22:37:43 +0200 Subject: [PATCH 027/222] Improve styles of extension blocks --- public/css/extensions-panel.css | 18 ++++++++++++++---- public/scripts/extensions.js | 10 +++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/public/css/extensions-panel.css b/public/css/extensions-panel.css index 5860b2eab..ad0bd0127 100644 --- a/public/css/extensions-panel.css +++ b/public/css/extensions-panel.css @@ -83,12 +83,12 @@ label[for="extensions_autoconnect"] { .extensions_info .extension_block { display: flex; - flex-wrap: wrap; - padding: 5px 10px; + flex-wrap: nowrap; + padding: 5px; margin-bottom: 5px; border: 1px solid var(--SmartThemeBorderColor); border-radius: 10px; - align-items: center; + align-items: baseline; justify-content: space-between; gap: 5px; } @@ -101,7 +101,7 @@ label[for="extensions_autoconnect"] { opacity: 0.8; font-size: 0.8em; font-weight: normal; - margin-left: 5px; + margin-left: 2px; } .extensions_info .extension_block a { @@ -136,3 +136,13 @@ input.extension_missing[type="checkbox"] { #extensionsMenu>div.extension_container:empty { display: none; } + +.extensions_info .extension_text_block { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.extensions_info .extension_actions { + flex-wrap: nowrap; +} diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 0229146b1..788292955 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -573,13 +573,13 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, const type = getExtensionType(name); switch (type) { case 'global': - return ''; + return ''; case 'local': - return ''; + return ''; case 'system': - return ''; + return ''; default: - return ''; + return ''; } } @@ -627,7 +627,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
${extensionIcon}
-
+
${originHtml} ${DOMPurify.sanitize(displayName)} From 5a01eb8eb1ec5645105823fa2222e4fa6d13fc3f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 9 Dec 2024 23:55:08 +0200 Subject: [PATCH 028/222] Ok, the manifest version can stay --- public/scripts/extensions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 788292955..845a7c70f 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -586,6 +586,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, const isUserAdmin = isAdmin(); const extensionIcon = getExtensionIcon(); const displayName = manifest.display_name; + const displayVersion = manifest.version || ''; const externalId = name.replace('third-party', ''); let originHtml = ''; if (isExternal) { @@ -631,7 +632,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, ${originHtml} ${DOMPurify.sanitize(displayName)} - + ${DOMPurify.sanitize(displayVersion)} ${modulesInfo} ${isExternal ? '' : ''} From df8e0ba9234715d5d274613cbef909c22a2632c5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 10 Dec 2024 00:01:54 +0200 Subject: [PATCH 029/222] Don't insert non-HTTP links to extension origin --- public/scripts/extensions.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 845a7c70f..c73a1fa27 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -1068,7 +1068,7 @@ async function checkForUpdatesManual(abortSignal) { try { const data = await getExtensionVersion(externalId, abortSignal); const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`); - if (extensionBlock) { + if (extensionBlock && data) { if (data.isUpToDate === false) { const buttonElement = extensionBlock.querySelector('.btn_update'); if (buttonElement) { @@ -1085,9 +1085,17 @@ async function checkForUpdatesManual(abortSignal) { const originLink = extensionBlock.querySelector('a'); if (originLink) { - originLink.href = origin; - originLink.target = '_blank'; - originLink.rel = 'noopener noreferrer'; + try { + const url = new URL(origin); + if (!['https:', 'http:'].includes(url.protocol)) { + throw new Error('Invalid protocol'); + } + originLink.href = url.href; + originLink.target = '_blank'; + originLink.rel = 'noopener noreferrer'; + } catch (error) { + console.log('Error setting origin link', originLink, error); + } } const versionElement = extensionBlock.querySelector('.extension_version'); From bcfb07de5ece5b370958d7f9b115ce160a7c59bb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:59:59 +0200 Subject: [PATCH 030/222] New llama-3.3 Groq model Closes #3168 --- public/index.html | 3 +++ public/scripts/openai.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 233ab710f..30c574365 100644 --- a/public/index.html +++ b/public/index.html @@ -3100,6 +3100,9 @@

Groq Model

Use alphabetical sorting -
-
+
+
-
\ No newline at end of file +
From a64c8ade9d53a610abc03d19d0585abc5462e500 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Thu, 12 Dec 2024 06:31:27 +0900 Subject: [PATCH 044/222] Support Gemini 2.0 Flash-exp --- public/index.html | 1 + public/scripts/extensions/caption/settings.html | 1 + public/scripts/openai.js | 2 +- src/endpoints/backends/chat-completions.js | 8 +++++++- src/prompt-converters.js | 1 + 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index f13a22092..7fbf896b7 100644 --- a/public/index.html +++ b/public/index.html @@ -3014,6 +3014,7 @@ + diff --git a/public/scripts/extensions/caption/settings.html b/public/scripts/extensions/caption/settings.html index 79692d54a..6fdbcb5d6 100644 --- a/public/scripts/extensions/caption/settings.html +++ b/public/scripts/extensions/caption/settings.html @@ -53,6 +53,7 @@ + diff --git a/public/scripts/openai.js b/public/scripts/openai.js index d819a6792..ca163ed8c 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -4081,7 +4081,7 @@ async function onModelChange() { $('#openai_max_context').attr('max', max_32k); } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) { $('#openai_max_context').attr('max', max_2mil); - } else if (value.includes('gemini-1.5-flash')) { + } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) { $('#openai_max_context').attr('max', max_1mil); } else if (value.includes('gemini-1.0-pro-vision') || value === 'gemini-pro-vision') { $('#openai_max_context').attr('max', max_16k); diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 248596404..f30a5163d 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -280,7 +280,13 @@ async function sendMakerSuiteRequest(request, response) { delete generationConfig.stopSequences; } - const should_use_system_prompt = (model.includes('gemini-1.5-flash') || model.includes('gemini-1.5-pro') || model.startsWith('gemini-exp')) && request.body.use_makersuite_sysprompt; + const should_use_system_prompt = ( + model.includes('gemini-2.0-flash-exp') || + model.includes('gemini-1.5-flash') || + model.includes('gemini-1.5-pro') || + model.startsWith('gemini-exp') + ) && request.body.use_makersuite_sysprompt; + const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, request.body.char_name, request.body.user_name); let body = { contents: prompt.contents, diff --git a/src/prompt-converters.js b/src/prompt-converters.js index 3f052c8c5..c23dd6a69 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -337,6 +337,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; const visionSupportedModels = [ + 'gemini-2.0-flash-exp', 'gemini-1.5-flash', 'gemini-1.5-flash-latest', 'gemini-1.5-flash-001', From 67cb89f634c19653a1ffcc52dbf47ac399b0e74c Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Thu, 12 Dec 2024 06:33:21 +0900 Subject: [PATCH 045/222] Sort Gemini-Subversion: gemini-exp-series by date --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 7fbf896b7..7f04f6ee6 100644 --- a/public/index.html +++ b/public/index.html @@ -3015,8 +3015,8 @@ - + From cf1b98e25d4858dd8e379ebe484eb9a1ef4b2d43 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:40:45 +0200 Subject: [PATCH 046/222] Add 'gemini-2.0-flash-exp' to supported vision models --- public/scripts/openai.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index ca163ed8c..de46a285e 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -4753,6 +4753,7 @@ export function isImageInliningSupported() { // gultra just isn't being offered as multimodal, thanks google. const visionSupportedModels = [ 'gpt-4-vision', + 'gemini-2.0-flash-exp', 'gemini-1.5-flash', 'gemini-1.5-flash-latest', 'gemini-1.5-flash-001', From 5c82ccf4352c3be6465036250823ffa04ce9a5b8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:07:37 +0200 Subject: [PATCH 047/222] Refactor endpoints/chats.js 1. Move formatBytes utility to util.js 2. Fix type error related to dates sorting 3. Add type to backupFunctions map --- src/endpoints/chats.js | 29 +++++++++++++---------------- src/util.js | 13 +++++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index 1e7a00d08..01fd12821 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -9,7 +9,14 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; import _ from 'lodash'; import { jsonParser, urlencodedParser } from '../express-common.js'; -import { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } from '../util.js'; +import { + getConfigValue, + humanizedISO8601DateTime, + tryParse, + generateTimestamp, + removeOldBackups, + formatBytes, +} from '../util.js'; const isBackupDisabled = getConfigValue('disableChatBackup', false); const maxTotalChatBackups = Number(getConfigValue('maxTotalChatBackups', -1)); @@ -46,6 +53,9 @@ function backupChat(directory, name, chat) { } } +/** + * @type {Map>} + */ const backupFunctions = new Map(); /** @@ -57,20 +67,7 @@ function getBackupFunction(handle) { if (!backupFunctions.has(handle)) { backupFunctions.set(handle, _.throttle(backupChat, throttleInterval, { leading: true, trailing: true })); } - return backupFunctions.get(handle); -} - -/** - * Formats a byte size into a human-readable string with units - * @param {number} bytes - The size in bytes to format - * @returns {string} The formatted string (e.g., "1.5 MB") - */ -function formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + return backupFunctions.get(handle) || (() => {}); } /** @@ -710,7 +707,7 @@ router.post('/search', jsonParser, function (request, response) { } // Sort by last message date descending - results.sort((a, b) => new Date(b.last_mes) - new Date(a.last_mes)); + results.sort((a, b) => new Date(b.last_mes).getTime() - new Date(a.last_mes).getTime()); return response.send(results); } catch (error) { diff --git a/src/util.js b/src/util.js index b07142e6b..13176f46e 100644 --- a/src/util.js +++ b/src/util.js @@ -142,6 +142,19 @@ export function getHexString(length) { return result; } +/** + * Formats a byte size into a human-readable string with units + * @param {number} bytes - The size in bytes to format + * @returns {string} The formatted string (e.g., "1.5 MB") + */ +export function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + /** * Extracts a file with given extension from an ArrayBuffer containing a ZIP archive. * @param {ArrayBuffer} archiveBuffer Buffer containing a ZIP archive From 205f1d7adb0d86b4510e0be8fe6163b70296ebfa Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:02:24 +0200 Subject: [PATCH 048/222] Add filter arg for inject command --- public/scripts/slash-commands.js | 61 ++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 23a36828f..7fa9e1582 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -84,6 +84,20 @@ export const parser = new SlashCommandParser(); const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser); const getSlashCommandsHelp = parser.getHelpString.bind(parser); +/** + * Converts a SlashCommandClosure to a filter function that returns a boolean. + * @param {SlashCommandClosure} closure + * @returns {() => Promise} + */ +function closureToFilter(closure) { + return async () => { + const localClosure = closure.getCopy(); + localClosure.onProgress = () => { }; + const result = await localClosure.execute(); + return isTrueBoolean(result.pipe); + }; +} + export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: '?', @@ -1611,6 +1625,13 @@ export function initDefaultSlashCommands() { new SlashCommandNamedArgument( 'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), + SlashCommandNamedArgument.fromProps({ + name: 'filter', + description: 'if a filter is defined, an injection will only be performed if the closure returns true', + typeList: [ARGUMENT_TYPE.CLOSURE], + isRequired: false, + acceptsMultiple: false, + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1901,6 +1922,11 @@ const NARRATOR_NAME_DEFAULT = 'System'; export const COMMENT_NAME_DEFAULT = 'Note'; const SCRIPT_PROMPT_KEY = 'script_inject_'; +/** + * Adds a new script injection to the chat. + * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments + * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value Unnamed argument + */ function injectCallback(args, value) { const positions = { 'before': extension_prompt_types.BEFORE_PROMPT, @@ -1914,8 +1940,8 @@ function injectCallback(args, value) { 'assistant': extension_prompt_roles.ASSISTANT, }; - const id = args?.id; - const ephemeral = isTrueBoolean(args?.ephemeral); + const id = String(args?.id); + const ephemeral = isTrueBoolean(String(args?.ephemeral)); if (!id) { console.warn('WARN: No ID provided for /inject command'); @@ -1931,7 +1957,9 @@ function injectCallback(args, value) { const depth = isNaN(depthValue) ? defaultDepth : depthValue; const roleValue = typeof args?.role === 'string' ? args.role.toLowerCase().trim() : Number(args?.role ?? extension_prompt_roles.SYSTEM); const role = roles[roleValue] ?? roles[extension_prompt_roles.SYSTEM]; - const scan = isTrueBoolean(args?.scan); + const scan = isTrueBoolean(String(args?.scan)); + const filter = args?.filter instanceof SlashCommandClosure ? args.filter.rawText : null; + const filterFunction = args?.filter instanceof SlashCommandClosure ? closureToFilter(args.filter) : null; value = value || ''; const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`; @@ -1941,13 +1969,13 @@ function injectCallback(args, value) { } if (value) { - const inject = { value, position, depth, scan, role }; + const inject = { value, position, depth, scan, role, filter }; chat_metadata.script_injects[id] = inject; } else { delete chat_metadata.script_injects[id]; } - setExtensionPrompt(prefixedId, value, position, depth, scan, role); + setExtensionPrompt(prefixedId, String(value), position, depth, scan, role, filterFunction); saveMetadataDebounced(); if (ephemeral) { @@ -1958,7 +1986,7 @@ function injectCallback(args, value) { } console.log('Removing ephemeral script injection', id); delete chat_metadata.script_injects[id]; - setExtensionPrompt(prefixedId, '', position, depth, scan, role); + setExtensionPrompt(prefixedId, '', position, depth, scan, role, filterFunction); saveMetadataDebounced(); deleted = true; }; @@ -2053,9 +2081,28 @@ export function processChatSlashCommands() { } for (const [id, inject] of Object.entries(context.chatMetadata.script_injects)) { + /** + * Rehydrates a filter closure from a string. + * @returns {SlashCommandClosure | null} + */ + function reviveFilterClosure() { + if (!inject.filter) { + return null; + } + + try { + return new SlashCommandParser().parse(inject.filter, true); + } catch (error) { + console.warn('Failed to revive filter closure for script injection', id, error); + return null; + } + } + const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`; + const filterClosure = reviveFilterClosure(); + const filter = filterClosure ? closureToFilter(filterClosure) : null; console.log('Adding script injection', id); - setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role); + setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role, filter); } } From 3167019faf9d8626bf5acf653ad28036fd2d78fc Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 13 Dec 2024 01:12:10 +0200 Subject: [PATCH 049/222] Add generic text completion API type (100% OAI compatible) --- public/index.html | 21 ++++++++++++++++++++- public/script.js | 6 +++++- public/scripts/preset-manager.js | 1 + public/scripts/secrets.js | 2 ++ public/scripts/slash-commands.js | 1 + public/scripts/textgen-models.js | 18 ++++++++++++++++++ public/scripts/textgen-settings.js | 10 ++++++++++ src/additional-headers.js | 14 ++++++++++++++ src/constants.js | 19 +++++++++++++++++++ src/endpoints/backends/text-completions.js | 10 +++++++++- src/endpoints/secrets.js | 1 + 11 files changed, 100 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 7f04f6ee6..310b60fd3 100644 --- a/public/index.html +++ b/public/index.html @@ -2187,10 +2187,10 @@

API Type

@@ -2321,6 +2322,24 @@
+
+

API key (optional)

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+
+

Server URL

+ Example: http://127.0.0.1:5000 + +
+ + +
diff --git a/public/script.js b/public/script.js index 99f673f21..4631647cf 100644 --- a/public/script.js +++ b/public/script.js @@ -234,7 +234,7 @@ import { import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; -import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels } from './scripts/textgen-models.js'; +import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js'; import { getPresetManager, initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js'; @@ -1221,6 +1221,9 @@ async function getStatusTextgen() { } else if (textgen_settings.type === textgen_types.TABBY) { loadTabbyModels(data?.data); setOnlineStatus(textgen_settings.tabby_model || data?.result); + } else if (textgen_settings.type === textgen_types.GENERIC) { + loadGenericModels(data?.data); + setOnlineStatus(textgen_settings.generic_model || 'Connected'); } else { setOnlineStatus(data?.result); } @@ -10000,6 +10003,7 @@ jQuery(async function () { { id: 'api_key_llamacpp', secret: SECRET_KEYS.LLAMACPP }, { id: 'api_key_featherless', secret: SECRET_KEYS.FEATHERLESS }, { id: 'api_key_huggingface', secret: SECRET_KEYS.HUGGINGFACE }, + { id: 'api_key_generic', secret: SECRET_KEYS.GENERIC }, ]; for (const key of keys) { diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js index 4c14b7519..0b6f3dcec 100644 --- a/public/scripts/preset-manager.js +++ b/public/scripts/preset-manager.js @@ -585,6 +585,7 @@ class PresetManager { 'openrouter_allow_fallbacks', 'tabby_model', 'derived', + 'generic_model', ]; const settings = Object.assign({}, getSettingsByApiId(this.apiId)); diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 4fce1e36c..816973232 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -38,6 +38,7 @@ export const SECRET_KEYS = { NANOGPT: 'api_key_nanogpt', TAVILY: 'api_key_tavily', BFL: 'api_key_bfl', + GENERIC: 'api_key_generic', }; const INPUT_MAP = { @@ -71,6 +72,7 @@ const INPUT_MAP = { [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface', [SECRET_KEYS.BLOCKENTROPY]: '#api_key_blockentropy', [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt', + [SECRET_KEYS.GENERIC]: '#api_key_generic', }; async function clearSecret() { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 23a36828f..7f26127e3 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -3687,6 +3687,7 @@ function setBackgroundCallback(_, bg) { function getModelOptions(quiet) { const nullResult = { control: null, options: null }; const modelSelectMap = [ + { id: 'generic_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.GENERIC }, { id: 'custom_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.OOBA }, { id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI }, { id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER }, diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index 53efe597d..6323505f5 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -160,6 +160,24 @@ export async function loadInfermaticAIModels(data) { } } +export function loadGenericModels(data) { + if (!Array.isArray(data)) { + console.error('Invalid Generic models data', data); + return; + } + + data.sort((a, b) => a.id.localeCompare(b.id)); + const dataList = $('#generic_model_fill'); + dataList.empty(); + + for (const model of data) { + const option = document.createElement('option'); + option.value = model.id; + option.text = model.id; + dataList.append(option); + } +} + export async function loadDreamGenModels(data) { if (!Array.isArray(data)) { console.error('Invalid DreamGen models data', data); diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index cc6aa0e78..f8d774224 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -33,9 +33,11 @@ export const textgen_types = { OPENROUTER: 'openrouter', FEATHERLESS: 'featherless', HUGGINGFACE: 'huggingface', + GENERIC: 'generic', }; const { + GENERIC, MANCER, VLLM, APHRODITE, @@ -120,6 +122,7 @@ export const SERVER_INPUTS = { [textgen_types.LLAMACPP]: '#llamacpp_api_url_text', [textgen_types.OLLAMA]: '#ollama_api_url_text', [textgen_types.HUGGINGFACE]: '#huggingface_api_url_text', + [textgen_types.GENERIC]: '#generic_api_url_text', }; const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5]; @@ -205,6 +208,7 @@ const settings = { xtc_probability: 0, nsigma: 0.0, featherless_model: '', + generic_model: '', }; export { @@ -282,6 +286,7 @@ export const setting_names = [ 'xtc_threshold', 'xtc_probability', 'nsigma', + 'generic_model', ]; const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba'); @@ -1100,6 +1105,11 @@ export function getTextGenModel() { return settings.custom_model; } break; + case GENERIC: + if (settings.generic_model) { + return settings.generic_model; + } + break; case MANCER: return settings.mancer_model; case TOGETHERAI: diff --git a/src/additional-headers.js b/src/additional-headers.js index e35456ae4..1ea5d035f 100644 --- a/src/additional-headers.js +++ b/src/additional-headers.js @@ -172,6 +172,19 @@ function getHuggingFaceHeaders(directories) { }) : {}; } +/** + * Gets the headers for the Generic text completion API. + * @param {import('./users.js').UserDirectoryList} directories + * @returns {object} Headers for the request + */ +function getGenericHeaders(directories) { + const apiKey = readSecret(directories, SECRET_KEYS.GENERIC); + + return apiKey ? ({ + 'Authorization': `Bearer ${apiKey}`, + }) : {}; +} + export function getOverrideHeaders(urlHost) { const requestOverrides = getConfigValue('requestOverrides', []); const overrideHeaders = requestOverrides?.find((e) => e.hosts?.includes(urlHost))?.headers; @@ -214,6 +227,7 @@ export function setAdditionalHeadersByType(requestHeaders, type, server, directo [TEXTGEN_TYPES.LLAMACPP]: getLlamaCppHeaders, [TEXTGEN_TYPES.FEATHERLESS]: getFeatherlessHeaders, [TEXTGEN_TYPES.HUGGINGFACE]: getHuggingFaceHeaders, + [TEXTGEN_TYPES.GENERIC]: getGenericHeaders, }; const getHeaders = headerGetters[type]; diff --git a/src/constants.js b/src/constants.js index 673bd95f0..d2c40daeb 100644 --- a/src/constants.js +++ b/src/constants.js @@ -225,6 +225,7 @@ export const TEXTGEN_TYPES = { OPENROUTER: 'openrouter', FEATHERLESS: 'featherless', HUGGINGFACE: 'huggingface', + GENERIC: 'generic', }; export const INFERMATICAI_KEYS = [ @@ -346,6 +347,24 @@ export const OLLAMA_KEYS = [ 'min_p', ]; +// https://platform.openai.com/docs/api-reference/completions +export const OPENAI_KEYS = [ + 'model', + 'prompt', + 'stream', + 'temperature', + 'top_p', + 'frequency_penalty', + 'presence_penalty', + 'stop', + 'seed', + 'logit_bias', + 'logprobs', + 'max_tokens', + 'n', + 'best_of', +]; + export const AVATAR_WIDTH = 512; export const AVATAR_HEIGHT = 768; diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index 80955696e..28651e93a 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -13,6 +13,7 @@ import { VLLM_KEYS, DREAMGEN_KEYS, FEATHERLESS_KEYS, + OPENAI_KEYS, } from '../../constants.js'; import { forwardFetchResponse, trimV1, getConfigValue } from '../../util.js'; import { setAdditionalHeaders } from '../../additional-headers.js'; @@ -113,8 +114,8 @@ router.post('/status', jsonParser, async function (request, response) { let url = baseUrl; let result = ''; - switch (apiType) { + case TEXTGEN_TYPES.GENERIC: case TEXTGEN_TYPES.OOBA: case TEXTGEN_TYPES.VLLM: case TEXTGEN_TYPES.APHRODITE: @@ -287,6 +288,7 @@ router.post('/generate', jsonParser, async function (request, response) { let url = trimV1(baseUrl); switch (request.body.api_type) { + case TEXTGEN_TYPES.GENERIC: case TEXTGEN_TYPES.VLLM: case TEXTGEN_TYPES.FEATHERLESS: case TEXTGEN_TYPES.APHRODITE: @@ -347,6 +349,12 @@ router.post('/generate', jsonParser, async function (request, response) { args.body = JSON.stringify(request.body); } + if (request.body.api_type === TEXTGEN_TYPES.GENERIC) { + request.body = _.pickBy(request.body, (_, key) => OPENAI_KEYS.includes(key)); + if (Array.isArray(request.body.stop)) { request.body.stop = request.body.stop.slice(0, 4); } + args.body = JSON.stringify(request.body); + } + if (request.body.api_type === TEXTGEN_TYPES.OPENROUTER) { if (Array.isArray(request.body.provider) && request.body.provider.length > 0) { request.body.provider = { diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index b30a51f48..ed2a0e4fc 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -50,6 +50,7 @@ export const SECRET_KEYS = { TAVILY: 'api_key_tavily', NANOGPT: 'api_key_nanogpt', BFL: 'api_key_bfl', + GENERIC: 'api_key_generic', }; // These are the keys that are safe to expose, even if allowKeysExposure is false From 294b15976cf0b855e8b441457ca1e6dea3638630 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 13 Dec 2024 01:20:43 +0200 Subject: [PATCH 050/222] Add validation for filter inject argument --- public/scripts/slash-commands.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 7fa9e1582..004c522d3 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1962,6 +1962,10 @@ function injectCallback(args, value) { const filterFunction = args?.filter instanceof SlashCommandClosure ? closureToFilter(args.filter) : null; value = value || ''; + if (args?.filter && !String(filter ?? '').trim()) { + throw new Error('Failed to parse the filter argument. Make sure it is a valid non-empty closure.'); + } + const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`; if (!chat_metadata.script_injects) { From 6f4350b3a7f074e57e1da5c150ec71415e43c712 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 13 Dec 2024 01:23:13 +0200 Subject: [PATCH 051/222] Add error handler to filter closure executor --- public/scripts/slash-commands.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 004c522d3..1ad0363fc 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -91,10 +91,15 @@ const getSlashCommandsHelp = parser.getHelpString.bind(parser); */ function closureToFilter(closure) { return async () => { - const localClosure = closure.getCopy(); - localClosure.onProgress = () => { }; - const result = await localClosure.execute(); - return isTrueBoolean(result.pipe); + try { + const localClosure = closure.getCopy(); + localClosure.onProgress = () => { }; + const result = await localClosure.execute(); + return isTrueBoolean(result.pipe); + } catch (e) { + console.error('Error executing filter closure', e); + return false; + } }; } From cd0b83429187a94715419180befe9c8d5446d045 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:58:16 +0200 Subject: [PATCH 052/222] Implement "except" mode for type-specific controls in settings --- public/index.html | 19 ++++++++++--------- public/scripts/textgen-settings.js | 7 +++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/public/index.html b/public/index.html index 310b60fd3..6af41883c 100644 --- a/public/index.html +++ b/public/index.html @@ -1235,7 +1235,8 @@
-
+ +
Top K
@@ -1251,7 +1252,7 @@
-
+
Typical P
@@ -1259,7 +1260,7 @@
-
+
Min P
@@ -1267,7 +1268,7 @@
-
+
Top A
@@ -1275,7 +1276,7 @@
-
+
TFS
@@ -1307,7 +1308,7 @@
-
+
Repetition Penalty @@ -1569,7 +1570,7 @@ Ignore EOS Token
-
-
+
-
+

Banned Tokens/Strings diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index f8d774224..9b7db4444 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -838,7 +838,14 @@ jQuery(function () { function showTypeSpecificControls(type) { $('[data-tg-type]').each(function () { + const mode = String($(this).attr('data-tg-type-mode') ?? '').toLowerCase().trim(); const tgTypes = $(this).attr('data-tg-type').split(',').map(x => x.trim()); + + if (mode === 'except') { + $(this)[tgTypes.includes(type) ? 'hide' : 'show'](); + return; + } + for (const tgType of tgTypes) { if (tgType === type || tgType == 'all') { $(this).show(); From 0e5100180b38c47049cb51ec690e9de87f2ece0d Mon Sep 17 00:00:00 2001 From: Succubyss <87207237+Succubyss@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:29:34 -0600 Subject: [PATCH 053/222] expose tokenizers & getTextTokens in getContext --- public/scripts/st-context.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/scripts/st-context.js b/public/scripts/st-context.js index 4f0abdd8d..9cbf18210 100644 --- a/public/scripts/st-context.js +++ b/public/scripts/st-context.js @@ -63,7 +63,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { tag_map, tags } from './tags.js'; import { textgenerationwebui_settings } from './textgen-settings.js'; -import { getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js'; +import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js'; import { ToolManager } from './tool-calling.js'; import { timestampToMoment } from './utils.js'; @@ -95,6 +95,8 @@ export function getContext() { sendStreamingRequest, sendGenerationRequest, stopGeneration, + tokenizers, + getTextTokens, /** @deprecated Use getTokenCountAsync instead */ getTokenCount, getTokenCountAsync, From 1ef25d617653a76197244d43f3cba465174c3cd5 Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:56:06 -0700 Subject: [PATCH 054/222] fix double-prefixing on example messages --- public/scripts/instruct-mode.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 8d8556041..6875bb228 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -431,7 +431,8 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { return mesExamplesArray.map(x => x.replace(/\n/i, blockHeading)); } - const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE); + const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS; + const includeGroupNames = selected_group && (includeNames || power_user.instruct.names_behavior === names_behavior_types.FORCE); let inputPrefix = power_user.instruct.input_sequence || ''; let outputPrefix = power_user.instruct.output_sequence || ''; @@ -463,7 +464,7 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { for (const item of mesExamplesArray) { const cleanedItem = item.replace(//i, '{Example Dialogue:}').replace(/\r/gm, ''); - const blockExamples = parseExampleIntoIndividual(cleanedItem); + const blockExamples = parseExampleIntoIndividual(cleanedItem, includeGroupNames); if (blockExamples.length === 0) { continue; @@ -474,8 +475,9 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { } for (const example of blockExamples) { + // If group names were already included, we don't want to add an additional prefix // If force group/persona names is set, we should override the include names for the user placeholder - const includeThisName = includeNames || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user'); + const includeThisName = (includeNames && !includeGroupNames) || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user'); const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix; const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix; @@ -489,7 +491,6 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { if (formattedExamples.length === 0) { return mesExamplesArray.map(x => x.replace(/\n/i, blockHeading)); } - return formattedExamples; } From a7e8d00145dfecd63fb055c7990c7bea443e32f5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 14 Dec 2024 15:16:43 +0200 Subject: [PATCH 055/222] Use Array.includes --- public/scripts/instruct-mode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 6875bb228..73660ad65 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -432,7 +432,7 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { } const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS; - const includeGroupNames = selected_group && (includeNames || power_user.instruct.names_behavior === names_behavior_types.FORCE); + const includeGroupNames = selected_group && [names_behavior_types.ALWAYS, names_behavior_types.FORCE].includes(power_user.instruct.names_behavior); let inputPrefix = power_user.instruct.input_sequence || ''; let outputPrefix = power_user.instruct.output_sequence || ''; From e15e6dc3bd19ec35e1fd59a089dfc7f853fbf21d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:04:01 +0200 Subject: [PATCH 056/222] One more example of generic endpoint --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 6af41883c..076c7553b 100644 --- a/public/index.html +++ b/public/index.html @@ -2191,7 +2191,7 @@ - + From e960ae64c541f4aeffab2033daf5dc0afa444c37 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:20:13 +0200 Subject: [PATCH 057/222] Add a name argument for /getchatbook Closes #2599 --- public/scripts/world-info.js | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index a105ea46b..458a25d73 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -930,7 +930,12 @@ function registerWorldInfoSlashCommands() { return entries; } - async function getChatBookCallback() { + /** + * Gets the name of the chat-bound lorebook. Creates a new one if it doesn't exist. + * @param {import('./slash-commands/SlashCommandParser.js').NamedArguments} args Named arguments + * @returns + */ + async function getChatBookCallback(args) { const chatId = getCurrentChatId(); if (!chatId) { @@ -942,8 +947,19 @@ function registerWorldInfoSlashCommands() { return chat_metadata[METADATA_KEY]; } - // Replace non-alphanumeric characters with underscores, cut to 64 characters - const name = `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64); + const name = (() => { + // Use the provided name if it's not in use + if (typeof args.name === 'string') { + const name = String(args.name); + if (world_names.includes(name)) { + throw new Error('This World Info file name is already in use'); + } + return name; + } + + // Replace non-alphanumeric characters with underscores, cut to 64 characters + return `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64); + })(); await createNewWorldInfo(name); chat_metadata[METADATA_KEY] = name; @@ -1289,6 +1305,15 @@ function registerWorldInfoSlashCommands() { callback: getChatBookCallback, returns: 'lorebook name', helpString: 'Get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe.', + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'lorebook name if creating a new one, will be auto-generated otherwise', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + acceptsMultiple: false, + }), + ], aliases: ['getchatlore', 'getchatwi'], })); From 713c05f80898db9dee09b6c2e2f9e94967d2068d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:48:14 +0200 Subject: [PATCH 058/222] Add /getcharlore and /getpersonalore commands Closes #3183 --- public/scripts/utils.js | 2 +- public/scripts/world-info.js | 75 ++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/public/scripts/utils.js b/public/scripts/utils.js index f49333c48..58ba6f038 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -2112,7 +2112,7 @@ export async function showFontAwesomePicker(customList = null) { * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by * @param {boolean} [options.preferCurrentChar=true] - Whether to prefer the current character(s) * @param {boolean} [options.quiet=false] - Whether to suppress warnings - * @returns {any?} - The found character or null if not found + * @returns {import('./char-data.js').v1CharData?} - The found character or null if not found */ export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) { const matches = (char) => !name || (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name); diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 458a25d73..e4d106170 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -1,7 +1,7 @@ import { Fuse } from '../lib.js'; import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js'; -import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce } from './utils.js'; +import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce, findChar, onlyUnique } from './utils.js'; import { extension_settings, getContext } from './extensions.js'; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; import { isMobile } from './RossAscends-mods.js'; @@ -930,10 +930,48 @@ function registerWorldInfoSlashCommands() { return entries; } + /** + * Gets the name of the persona-bound lorebook. + * @returns {string} The name of the persona-bound lorebook + */ + function getPersonaBookCallback() { + return power_user.persona_description_lorebook || ''; + } + + /** + * Gets the name of the character-bound lorebook. + * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments + * @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} name Character name + * @returns {string} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string + */ + function getCharBookCallback({ type }, name) { + const context = getContext(); + if (context.groupId && !name) throw new Error('This command is not available in groups without providing a character name'); + type = String(type ?? '').trim().toLowerCase() || 'primary'; + name = String(name ?? '') || context.characters[context.characterId]?.avatar || null; + const character = findChar({ name }); + if (!character) { + toastr.error('Character not found.'); + return ''; + } + const books = []; + if (type === 'all' || type === 'primary') { + books.push(character.data?.extensions?.world); + } + if (type === 'all' || type === 'additional') { + const fileName = getCharaFilename(context.characters.indexOf(character)); + const extraCharLore = world_info.charLore?.find((e) => e.name === fileName); + if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) { + books.push(...extraCharLore.extraBooks); + } + } + return type === 'primary' ? (books[0] ?? '') : JSON.stringify(books.filter(onlyUnique).filter(Boolean)); + } + /** * Gets the name of the chat-bound lorebook. Creates a new one if it doesn't exist. - * @param {import('./slash-commands/SlashCommandParser.js').NamedArguments} args Named arguments - * @returns + * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments + * @returns {Promise} The name of the chat-bound lorebook */ async function getChatBookCallback(args) { const chatId = getCurrentChatId(); @@ -1316,6 +1354,37 @@ function registerWorldInfoSlashCommands() { ], aliases: ['getchatlore', 'getchatwi'], })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'getpersonabook', + callback: getPersonaBookCallback, + returns: 'lorebook name', + helpString: 'Get a name of the current persona-bound lorebook and pass it down the pipe. Returns empty string if persona lorebook is not set.', + aliases: ['getpersonalore', 'getpersonawi'], + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'getcharbook', + callback: getCharBookCallback, + returns: 'lorebook name or a list of lorebook names', + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'type', + description: 'type of the lorebook to get, returns a list for "all" and "additional"', + typeList: [ARGUMENT_TYPE.STRING], + enumList: ['primary', 'additional', 'all'], + defaultValue: 'primary', + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Character name - or unique character identifier (avatar key). If not provided, the current character is used.', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], + isRequired: false, + enumProvider: commonEnumProviders.characters('character'), + }), + ], + helpString: 'Get a name of the character-bound lorebook and pass it down the pipe. Returns empty string if character lorebook is not set. Does not work in group chats without providing a character avatar name.', + aliases: ['getcharlore', 'getcharwi'], + })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'findentry', From b8fc9f2194c5a0586110b15af4a6b44396c3c8bd Mon Sep 17 00:00:00 2001 From: InspectorCaracal <51038201+InspectorCaracal@users.noreply.github.com> Date: Sat, 14 Dec 2024 15:10:31 -0700 Subject: [PATCH 059/222] Fix name-inclusion logic check --- public/scripts/instruct-mode.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 73660ad65..746682859 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -475,9 +475,9 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { } for (const example of blockExamples) { - // If group names were already included, we don't want to add an additional prefix - // If force group/persona names is set, we should override the include names for the user placeholder - const includeThisName = (includeNames && !includeGroupNames) || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user'); + // If group names were included, we don't want to add any additional prefix as it already was applied. + // Otherwise, if force group/persona names is set, we should override the include names for the user placeholder + const includeThisName = !includeGroupNames && (includeNames || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user')); const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix; const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix; From 6fce056b8c71af0cdab2de172a79d2d123f12f2b Mon Sep 17 00:00:00 2001 From: Succubyss <87207237+Succubyss@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:25:09 -0600 Subject: [PATCH 060/222] Add /substr command --- public/scripts/slash-commands.js | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 45aea41a0..5cb76d06d 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1918,6 +1918,46 @@ export function initDefaultSlashCommands() { ], helpString: 'Converts the provided string to lowercase.', })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'substr', + aliases: ['substring'], + callback: (arg, text) => typeof text === 'string' ? text.slice(...[Number(arg.start), arg.end && Number(arg.end)]) : '', + returns: 'substring', + namedArgumentList: [ + new SlashCommandNamedArgument( + 'start', 'start index', [ARGUMENT_TYPE.NUMBER], false, false, + ), + new SlashCommandNamedArgument( + 'end', 'end index', [ARGUMENT_TYPE.NUMBER], false, false, + ), + ], + unnamedArgumentList: [ + new SlashCommandArgument( + 'string', [ARGUMENT_TYPE.STRING], true, false, + ), + ], + helpString: ` +
+ Extracts text from the provided string. +
+
+ If start is omitted, it's treated as 0.
+ If start < 0, the index is counted from the end of the string.
+ If start >= the string's length, an empty string is returned.
+ If end is omitted, or if end >= the string's length, extracts to the end of the string.
+ If end < 0, the index is counted from the end of the string.
+ If end <= start after normalizing negative values, an empty string is returned. +
+
+ Example: +
/let x The morning is upon us.     ||                                     
+
/substr start=-3 {{var::x}}         | /echo  |/# us.                    ||
+
/substr start=-3 end=-1 {{var::x}}  | /echo  |/# us                     ||
+
/substr end=-1 {{var::x}}           | /echo  |/# The morning is upon us ||
+
/substr start=4 end=-1 {{var::x}}   | /echo  |/# morning is upon us     ||
+
+ `, + })); registerVariableCommands(); } From e762405cba7d2ccb3d795ca13e60fab0f2b368a6 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:11:56 +0900 Subject: [PATCH 061/222] Update: [index.html] Remove GAI models deleted from the API The 1.0 Pro Vision model has already been removed. ``` Google AI Studio API returned error: 404 Not Found { "error": { "code": 404, "message": "Gemini 1.0 Pro Vision has been deprecated on July 12, 2024. Consider switching to a different model, for example gemini-1.5-flash.", "status": "NOT_FOUND" } } ``` --- public/index.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/index.html b/public/index.html index 076c7553b..e1640fffa 100644 --- a/public/index.html +++ b/public/index.html @@ -3027,7 +3027,6 @@ - @@ -3052,7 +3051,6 @@ -

From df5f69dc66a3e47ad69243a507be1fdc48d39c8b Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:18:05 +0900 Subject: [PATCH 062/222] Update: [index.html] Added notes for deprecated GAI models Based on https://ai.google.dev/gemini-api/docs/models/gemini, the 1.0 Pro model has been marked as deprecated. --- public/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/index.html b/public/index.html index e1640fffa..5b2e42c2b 100644 --- a/public/index.html +++ b/public/index.html @@ -3025,8 +3025,8 @@ - - + + @@ -3049,8 +3049,8 @@ - - + +
From 954ed6c2b2f560f685fad44c82fc2745ef0cbeb9 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:28:37 +0900 Subject: [PATCH 063/222] Update: [index.html] Remove PaLM from GAI models The PaLM family (PaLM 2, PaLM 2 Chat) is no longer available on AI Studio. ``` Google AI Studio API returned error: 404 Not Found { "error": { "code": 404, "message": "Requested entity was not found.", "status": "NOT_FOUND" } ``` --- public/index.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/index.html b/public/index.html index 5b2e42c2b..3013a50cc 100644 --- a/public/index.html +++ b/public/index.html @@ -3029,8 +3029,6 @@ - - From 58213d0ab012f1038d0b7a35640c6c0193c40d54 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:29:44 +0900 Subject: [PATCH 064/222] Update: [index.html] Fixed inconsistent notation of GAI models Corrected the text from "Experiment" to "Experimental" to align with the terminology used on https://aistudio.google.com/. --- public/index.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/index.html b/public/index.html index 3013a50cc..cc68a5c39 100644 --- a/public/index.html +++ b/public/index.html @@ -3031,19 +3031,19 @@ - + - - + + - - - + + + From 9cab0618b67341da05a4a681d53c5347da5bff87 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:43:16 +0900 Subject: [PATCH 065/222] Update: [caption/settings.html] Remove GAI version of gemini-pro-vision Retained the OpenRouter version, as it is still available via Vertex. --- public/scripts/extensions/caption/settings.html | 1 - 1 file changed, 1 deletion(-) diff --git a/public/scripts/extensions/caption/settings.html b/public/scripts/extensions/caption/settings.html index 6fdbcb5d6..5a92ba7d8 100644 --- a/public/scripts/extensions/caption/settings.html +++ b/public/scripts/extensions/caption/settings.html @@ -70,7 +70,6 @@ - From 9ea8fc92e464d771bdb058c1bdd35c7d41e6c028 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:06:30 +0900 Subject: [PATCH 066/222] Update: [openai.js] Remove deleted GAI models from context length checks Removed gemini-pro-vision and text-bison-001 (PaLM) from context length checks. --- public/scripts/openai.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 6f695ae3b..1f2fe73d5 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -4087,13 +4087,8 @@ async function onModelChange() { $('#openai_max_context').attr('max', max_2mil); } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) { $('#openai_max_context').attr('max', max_1mil); - } else if (value.includes('gemini-1.0-pro-vision') || value === 'gemini-pro-vision') { - $('#openai_max_context').attr('max', max_16k); } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') { $('#openai_max_context').attr('max', max_32k); - } else if (value === 'text-bison-001') { - $('#openai_max_context').attr('max', max_8k); - // The ultra endpoints are possibly dead: } else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') { $('#openai_max_context').attr('max', max_32k); } else { @@ -4776,7 +4771,6 @@ export function isImageInliningSupported() { 'gemini-1.5-pro-002', 'gemini-1.5-pro-exp-0801', 'gemini-1.5-pro-exp-0827', - 'gemini-pro-vision', 'claude-3', 'claude-3-5', 'gpt-4-turbo', From 43feffdfae4ecbf54e12d569cd88a35a9dea9170 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:06:48 +0900 Subject: [PATCH 067/222] Update: [chat-completions.js] Update sendMakerSuiteRequest function Removed branching logic for differences in JSON request body between PaLM and Gemini, following the removal of PaLM from Google AI Studio. --- src/endpoints/backends/chat-completions.js | 44 ++-------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index f30a5163d..53f86a4f9 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -262,9 +262,7 @@ async function sendMakerSuiteRequest(request, response) { } const model = String(request.body.model); - const isGemini = model.includes('gemini'); - const isText = model.includes('text'); - const stream = Boolean(request.body.stream) && isGemini; + const stream = Boolean(request.body.stream); const generationConfig = { stopSequences: request.body.stop, @@ -301,39 +299,7 @@ async function sendMakerSuiteRequest(request, response) { return body; } - function getBisonBody() { - const prompt = isText - ? ({ text: convertTextCompletionPrompt(request.body.messages) }) - : ({ messages: convertGooglePrompt(request.body.messages, model).contents }); - - /** @type {any} Shut the lint up */ - const bisonBody = { - ...generationConfig, - safetySettings: BISON_SAFETY, - candidate_count: 1, // lewgacy spelling - prompt: prompt, - }; - - if (!isText) { - delete bisonBody.stopSequences; - delete bisonBody.maxOutputTokens; - delete bisonBody.safetySettings; - - if (Array.isArray(prompt.messages)) { - for (const msg of prompt.messages) { - msg.author = msg.role; - msg.content = msg.parts[0].text; - delete msg.parts; - delete msg.role; - } - } - } - - delete bisonBody.candidateCount; - return bisonBody; - } - - const body = isGemini ? getGeminiBody() : getBisonBody(); + const body = getGeminiBody(); console.log('Google AI Studio request:', body); try { @@ -343,10 +309,8 @@ async function sendMakerSuiteRequest(request, response) { controller.abort(); }); - const apiVersion = isGemini ? 'v1beta' : 'v1beta2'; - const responseType = isGemini - ? (stream ? 'streamGenerateContent' : 'generateContent') - : (isText ? 'generateText' : 'generateMessage'); + const apiVersion = 'v1beta'; + const responseType = (stream ? 'streamGenerateContent' : 'generateContent'); const generateResponse = await fetch(`${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, { body: JSON.stringify(body), From d16f5a24f4110ebb0b2c29c9f73ebf32ffdd689c Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:07:02 +0900 Subject: [PATCH 068/222] Update: [prompt-converters.js] Remove gemini-pro-vision from constants Removed gemini-pro-vision from visionSupportedModels. Also removed dummyRequiredModels as Gemini Pro, its cause, has been deleted. --- src/prompt-converters.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/prompt-converters.js b/src/prompt-converters.js index c23dd6a69..0d4c5df7b 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -355,13 +355,6 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN 'gemini-1.5-pro-002', 'gemini-1.5-pro-exp-0801', 'gemini-1.5-pro-exp-0827', - 'gemini-1.0-pro-vision-latest', - 'gemini-pro-vision', - ]; - - const dummyRequiredModels = [ - 'gemini-1.0-pro-vision-latest', - 'gemini-pro-vision', ]; const isMultimodal = visionSupportedModels.includes(model); @@ -452,8 +445,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN } }); - // pro 1.5 doesn't require a dummy image to be attached, other vision models do - if (isMultimodal && dummyRequiredModels.includes(model) && !hasImage) { + if (isMultimodal && !hasImage) { contents[0].parts.push({ inlineData: { mimeType: 'image/png', From 2d66b7204ad0352ceadde05a75d98fc37c068cd3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:09:17 +0200 Subject: [PATCH 069/222] Fixes to group join examples parsing --- public/scripts/group-chats.js | 10 ++++++++-- public/scripts/openai.js | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 9784218ee..1152b2471 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -423,14 +423,20 @@ export function getGroupCharacterCards(groupId, characterId) { * @param {string} value Value to replace * @param {string} characterName Name of the character * @param {string} fieldName Name of the field + * @param {function(string): string} [preprocess] Preprocess function * @returns {string} Prepared text * */ - function replaceAndPrepareForJoin(value, characterName, fieldName) { + function replaceAndPrepareForJoin(value, characterName, fieldName, preprocess = null) { value = value.trim(); if (!value) { return ''; } + // Run preprocess function + if (typeof preprocess === 'function') { + value = preprocess(value); + } + // Prepare and replace prefixes const prefix = customBaseChatReplace(group.generation_mode_join_prefix, fieldName, characterName); const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName); @@ -465,7 +471,7 @@ export function getGroupCharacterCards(groupId, characterId) { descriptions.push(replaceAndPrepareForJoin(character.description, character.name, 'Description')); personalities.push(replaceAndPrepareForJoin(character.personality, character.name, 'Personality')); scenarios.push(replaceAndPrepareForJoin(character.scenario, character.name, 'Scenario')); - mesExamplesArray.push(replaceAndPrepareForJoin(character.mes_example, character.name, 'Example Messages')); + mesExamplesArray.push(replaceAndPrepareForJoin(character.mes_example, character.name, 'Example Messages', (x) => !x.startsWith('') ? `\n${x}` : x)); } const description = descriptions.filter(x => x.length).join('\n'); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 6f695ae3b..f50126abc 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -33,7 +33,7 @@ import { system_message_types, this_chid, } from '../script.js'; -import { selected_group } from './group-chats.js'; +import { groups, selected_group } from './group-chats.js'; import { chatCompletionDefaultPrompts, @@ -543,11 +543,18 @@ function setupChatCompletionPromptManager(openAiSettings) { * @returns {Message[]} Array of message objects */ export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) { + const groupMembers = selected_group ? groups.find(x => x.id == selected_group)?.members : null; + const groupBotNames = Array.isArray(groupMembers) + ? groupMembers.map(x => characters.find(y => y.avatar === x)?.name).filter(x => x).map(x => `${x}:`) + : []; + let result = []; // array of msgs let tmp = messageExampleString.split('\n'); let cur_msg_lines = []; let in_user = false; let in_bot = false; + let botName = name2; + // DRY my cock and balls :) function add_msg(name, role, system_name) { // join different newlines (we split them by \n and join by \n) @@ -571,10 +578,14 @@ export function parseExampleIntoIndividual(messageExampleString, appendNamesForG in_user = true; // we were in the bot mode previously, add the message if (in_bot) { - add_msg(name2, 'system', 'example_assistant'); + add_msg(botName, 'system', 'example_assistant'); } in_bot = false; - } else if (cur_str.startsWith(name2 + ':')) { + } else if (cur_str.startsWith(name2 + ':') || groupBotNames.some(n => cur_str.startsWith(n))) { + if (!cur_str.startsWith(name2 + ':') && groupBotNames.length) { + botName = cur_str.split(':')[0]; + } + in_bot = true; // we were in the user mode previously, add the message if (in_user) { @@ -589,7 +600,7 @@ export function parseExampleIntoIndividual(messageExampleString, appendNamesForG if (in_user) { add_msg(name1, 'system', 'example_user'); } else if (in_bot) { - add_msg(name2, 'system', 'example_assistant'); + add_msg(botName, 'system', 'example_assistant'); } return result; } From 266b181d49862ae9f8259ae1e7a0b4f8241a554f Mon Sep 17 00:00:00 2001 From: cloak1505 Date: Sun, 15 Dec 2024 14:47:22 -0600 Subject: [PATCH 070/222] Add Cohere command-r7b-12-2024 Also slightly reorder model list --- public/index.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 076c7553b..3a463b5ec 100644 --- a/public/index.html +++ b/public/index.html @@ -3220,16 +3220,17 @@

Cohere Model

- + From ce536201e6576e2c6793d3735c184c833fb6b914 Mon Sep 17 00:00:00 2001 From: cloak1505 Date: Sun, 15 Dec 2024 17:12:58 -0600 Subject: [PATCH 072/222] Fix Cohere context sizes --- public/scripts/openai.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 6f695ae3b..667f518bf 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -4215,16 +4215,13 @@ async function onModelChange() { if (oai_settings.max_context_unlocked) { $('#openai_max_context').attr('max', unlocked_max); } - else if (['command-light', 'command'].includes(oai_settings.cohere_model)) { + else if (['command-light-nightly', 'command-light', 'command'].includes(oai_settings.cohere_model)) { $('#openai_max_context').attr('max', max_4k); } - else if (['command-light-nightly', 'command-nightly'].includes(oai_settings.cohere_model)) { - $('#openai_max_context').attr('max', max_8k); - } - else if (oai_settings.cohere_model.includes('command-r') || ['c4ai-aya-expanse-32b'].includes(oai_settings.cohere_model)) { + else if (oai_settings.cohere_model.includes('command-r') || ['c4ai-aya-23', 'c4ai-aya-expanse-32b', 'command-nightly'].includes(oai_settings.cohere_model)) { $('#openai_max_context').attr('max', max_128k); } - else if (['c4ai-aya-23', 'c4ai-aya-expanse-8b'].includes(oai_settings.cohere_model)) { + else if (['c4ai-aya-23-8b', 'c4ai-aya-expanse-8b'].includes(oai_settings.cohere_model)) { $('#openai_max_context').attr('max', max_8k); } else { From 0e3b4335ebdb9e5b613a4c319d7bbfd5dd91de35 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:44:02 +0900 Subject: [PATCH 073/222] Update: [prompt-converters.js] Remove entire dummy img codes --- src/prompt-converters.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/prompt-converters.js b/src/prompt-converters.js index 0d4c5df7b..47f5989cc 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -333,8 +333,6 @@ export function convertCohereMessages(messages, charName = '', userName = '') { * @returns {{contents: *[], system_instruction: {parts: {text: string}}}} Prompt for Google MakerSuite models */ export function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '', userName = '') { - // This is a 1x1 transparent PNG - const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; const visionSupportedModels = [ 'gemini-2.0-flash-exp', @@ -445,15 +443,6 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN } }); - if (isMultimodal && !hasImage) { - contents[0].parts.push({ - inlineData: { - mimeType: 'image/png', - data: PNG_PIXEL, - }, - }); - } - return { contents: contents, system_instruction: system_instruction }; } From 3f253f42f2c6dde92ae484ad59ddc12719fc9c45 Mon Sep 17 00:00:00 2001 From: M0cho <77959408+M0ch0@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:45:30 +0900 Subject: [PATCH 074/222] Update: [constants.js] Remove BISON_SAFETY --- src/constants.js | 27 ---------------------- src/endpoints/backends/chat-completions.js | 1 - 2 files changed, 28 deletions(-) diff --git a/src/constants.js b/src/constants.js index d2c40daeb..7fb98eef9 100644 --- a/src/constants.js +++ b/src/constants.js @@ -159,33 +159,6 @@ export const GEMINI_SAFETY = [ }, ]; -export const BISON_SAFETY = [ - { - category: 'HARM_CATEGORY_DEROGATORY', - threshold: 'BLOCK_NONE', - }, - { - category: 'HARM_CATEGORY_TOXICITY', - threshold: 'BLOCK_NONE', - }, - { - category: 'HARM_CATEGORY_VIOLENCE', - threshold: 'BLOCK_NONE', - }, - { - category: 'HARM_CATEGORY_SEXUAL', - threshold: 'BLOCK_NONE', - }, - { - category: 'HARM_CATEGORY_MEDICAL', - threshold: 'BLOCK_NONE', - }, - { - category: 'HARM_CATEGORY_DANGEROUS', - threshold: 'BLOCK_NONE', - }, -]; - export const CHAT_COMPLETION_SOURCES = { OPENAI: 'openai', WINDOWAI: 'windowai', diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 53f86a4f9..78e3f1f63 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -6,7 +6,6 @@ import { jsonParser } from '../../express-common.js'; import { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, - BISON_SAFETY, OPENROUTER_HEADERS, } from '../../constants.js'; import { From d8b1fd3b0b94ccf9f45835a7c670435294277fd8 Mon Sep 17 00:00:00 2001 From: Rivelle Date: Mon, 16 Dec 2024 15:41:01 +0800 Subject: [PATCH 075/222] Update zh-tw.json Adjust translation terms to align with actual functionality. --- public/locales/zh-tw.json | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index c31b03f89..9d9135697 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -65,20 +65,20 @@ "Min P": "Min P", "Top A": "Top A", "Quick Prompts Edit": "快速提示詞編輯", - "Main": "主要", + "Main": "主要提示詞", "NSFW": "NSFW", "Jailbreak": "越獄", "Utility Prompts": "實用提示詞", "Impersonation prompt": "AI 扮演提示詞", "Restore default prompt": "還原預設提示詞", "Prompt that is used for Impersonation function": "用於 AI 模仿功能的提示詞", - "World Info Format Template": "世界資訊格式範本", + "World Info Format Template": "世界資訊格式", "Restore default format": "還原預設格式", "Wraps activated World Info entries before inserting into the prompt.": "在插入提示詞前包裝已啟用的世界資訊條目。", "scenario_format_template_part_1": "使用", "scenario_format_template_part_2": "來標示要插入內容的位置。", - "Scenario Format Template": "情境格式範本", - "Personality Format Template": "個性格式範本", + "Scenario Format Template": "情境格式", + "Personality Format Template": "個性格式", "Group Nudge Prompt Template": "群組推動提示詞範本", "Sent at the end of the group chat history to force reply from a specific character.": "在群組聊天歷史結束時發送以強制特定角色回覆", "New Chat": "新聊天", @@ -412,7 +412,7 @@ "Chat Start": "聊天開始符號", "Add Chat Start and Example Separator to a list of stopping strings.": "將聊天開始和範例分隔符號加入終止字串中。", "Use as Stop Strings": "用作停止字串", - "context_allow_jailbreak": "如果在角色卡中定義了越獄,且啟用了「偏好角色卡越獄」,則會在提示詞的結尾加入越獄內容。\n這不建議用於文字完成模型,因為可能導致不良的輸出結果。", + "context_allow_jailbreak": "如果在角色卡中定義了越獄,且啟用了「角色卡越獄優先」,則會在提示詞的結尾加入越獄內容。\n這不建議用於文字完成模型,因為可能導致不良的輸出結果。", "Allow Jailbreak": "允許越獄", "Context Order": "上下文順序", "Summary": "摘要", @@ -631,9 +631,9 @@ "Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊配對,並通過所有資料欄位在列表中搜尋角色,而不僅僅是通過名稱子字串。", "Advanced Character Search": "進階角色搜尋", "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果選中並且角色卡包含提示詞覆寫(系統提示詞),則使用該提示詞。", - "Prefer Character Card Prompt": "偏好角色卡主要提示詞", + "Prefer Character Card Prompt": "角色卡主要提示詞優先", "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "如果選中並且角色卡包含越獄覆寫(聊天歷史後指示),則使用該提示詞。", - "Prefer Character Card Jailbreak": "偏好角色卡越獄", + "Prefer Character Card Jailbreak": "角色卡越獄優先", "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和調整匯入的角色圖像大小。未勾選時將會裁剪/調整大小到 512x768。", "Never resize avatars": "永不調整頭像大小", "Show actual file names on the disk, in the characters list display only": "僅在角色列表顯示實際檔案名稱。", @@ -1018,7 +1018,7 @@ "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最後一則訊息之後,1 = 在最後一則訊息之前,以此類推。", "Prompt": "提示詞", "The prompt to be sent.": "要發送的提示詞。", - "This prompt cannot be overridden by character cards, even if overrides are preferred.": "即使啟用偏好覆寫,此提示詞也不能被角色卡片覆寫。", + "This prompt cannot be overridden by character cards, even if overrides are preferred.": "即使啟用優先覆寫,此提示詞也不能被角色卡片覆寫。", "prompt_manager_forbid_overrides": "禁止覆寫", "reset": "重設", "save": "節省", @@ -1181,7 +1181,7 @@ "Expression API": "表達 API", "Fallback Expression": "回退表達式", "ext_sum_with": "總結一下:", - "ext_sum_main_api": "主要API", + "ext_sum_main_api": "主要 API", "ext_sum_current_summary": "目前摘要:", "ext_sum_restore_previous": "還原上一個", "ext_sum_memory_placeholder": "將在此產生摘要⋯", @@ -1343,12 +1343,12 @@ "sd_prompt_prefix_placeholder": "使用 {prompt} 指定生成的提示詞將被插入的位置。", "Negative common prompt prefix": "負面通用提示詞前綴", "Character-specific prompt prefix": "角色特定提示詞前綴", - "Won't be used in groups.": "不會用在群組", + "Won't be used in groups.": "群聊中無效", "sd_character_prompt_placeholder": "描述該選擇角色的特徵。這些特徵將增加在通用提示詞前綴之後。\n例如:女性、綠色眼睛、棕色頭髮、粉紅色襯衫。", "Character-specific negative prompt prefix": "角色特定負面提示詞前綴", "sd_character_negative_prompt_placeholder": "不應出現在選定角色上的任何特徵。這些特徵將增加在負面通用提示詞前綴之後。例如:珠寶、鞋子、眼鏡。", "Shareable": "可分享", - "Image Prompt Templates": "圖片提示詞範本", + "Image Prompt Templates": "AI 算圖提示詞範本", "Vectors Model Warning": "向量模型警告", "Translate files into English before processing": "處理前將檔案翻譯成英文", "Manager Users": "管理使用者", @@ -1573,8 +1573,8 @@ "at the end of the URL!": "在 URL 末尾!", "Audio Playback Speed": "音檔播放速度", "Auto-select Input Text": "自動選擇輸入文本", - "Automatically caption images": "自動為圖片添加字幕", - "Auxiliary": "輔助", + "Automatically caption images": "自動為圖片添加註解", + "Auxiliary": "輔助提示詞", "Background Image": "背景圖片", "Block Entropy API Key": "Block Entropy API 金鑰", "Can be set manually or with an _space": "可以手動設置或使用 _space", @@ -1610,7 +1610,7 @@ "Delay until recursion (can only be activated on recursive checking)": "遞迴掃描延遲(僅在啟用遞迴掃描時可用)", "Do not proceed if you do not agree to this!": "若不同意此條款,請勿繼續!", "Edge Color": "邊緣顏色", - "Edit captions before saving": "在保存前編輯字幕", + "Edit captions before saving": "在保存前編輯註解", "Enable for files": "為文件啟用", "Enable for World Info": "為世界資訊啟用", "enable_functions_desc_1": "允許使用", @@ -1694,7 +1694,7 @@ "Only used when Main API or WebLLM Extension is selected.": "僅在選擇主要 API 或 WebLLM 擴展時使用。", "Open a chat to see the character expressions.": "開啟聊天以查看角色表情。", "Post-History Instructions": "聊天歷史後指示", - "Prefer Character Card Instructions": "偏好角色卡聊天歷史後指示", + "Prefer Character Card Instructions": "角色卡聊天歷史後指示優先", "Prioritize": "優先處理", "Prompt Content": "提示詞內容", "prompt_manager_in_chat": "聊天中的提示詞管理", @@ -1734,23 +1734,23 @@ "Select with Tab or Enter": "按 Tab 或 Enter 選擇", "Separators as Stop Strings": "以分隔符作為停止字串", "Set the default and fallback expression being used when no matching expression is found.": "設定在無法配對到表情時所使用的預設和替代圖片。", - "Set your API keys and endpoints in the API Connections tab first.": "請先在「API 連接」頁面中設定您的 API 密鑰和端點。", + "Set your API keys and endpoints in the API Connections tab first.": "請先在「API 連接」頁面中設定您的 API 金鑰和端點。", "Show default images (emojis) if sprite missing": "無對應圖片時,顯示為預設表情符號(emoji)", "Show group chat queue": "顯示群組聊天隊列", "Size threshold (KB)": "大小閾值(KB)", "Slash Command": "斜線命令", "space_ slash command.": "斜線命令。", "Sprite Folder Override": "表情立繪資料夾覆蓋", - "Sprite set:": "表情集:", + "Sprite set:": "立繪組:", "Show Gallery": "查看畫廊", "Sticky": "固定", "Style Preset": "預設樣式", "Summarize chat messages for vector generation": "摘要聊天訊息以進行向量化處理", "Summarize chat messages when sending": "傳送時摘要聊天內容", "Swipe # for All Messages": "為所有訊息分配滑動編號 #", - "System Message Sequences": "系統訊息順序", + "System Message Sequences": "系統訊息序列", "System Prefix": "系統訊息前綴", - "System Prompt Sequences": "系統提示詞順序", + "System Prompt Sequences": "系統提示詞序列", "System Suffix": "系統訊息後綴", "Tabby Model": "Tabby 模型", "tag_import_all": "全部匯入", @@ -1767,9 +1767,9 @@ "Upload sprite pack (ZIP)": "打包上傳立繪(ZIP 格式)", "Use a forward slash to specify a subfolder. Example: _space": "使用「/」來設置子目錄,例如:_space", "Use ADetailer (Face)": "使用 ADetailer 進行臉部處理。", - "Use an admin API key.": "使用管理員的 API 密鑰。", + "Use an admin API key.": "使用管理員的 API 金鑰。", "Use global": "啟用全域設定", - "User Message Sequences": "使用者訊息順序", + "User Message Sequences": "使用者訊息序列", "User Node Color": "使用者節點顏色", "User Prefix": "使用者訊息前綴", "User Suffix": "使用者訊息後綴", From 0ea4494deae79b63b70c1d73ffd9e0a75f617649 Mon Sep 17 00:00:00 2001 From: ceruleandeep Date: Mon, 16 Dec 2024 19:48:07 +1100 Subject: [PATCH 076/222] Fix i18n tags for Generate Image/Stop Image Generation, add translations --- public/locales/ja-jp.json | 1 + public/locales/zh-cn.json | 1 + public/locales/zh-tw.json | 1 + public/scripts/extensions/stable-diffusion/button.html | 4 ++-- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/locales/ja-jp.json b/public/locales/ja-jp.json index a993b3926..15f586b7c 100644 --- a/public/locales/ja-jp.json +++ b/public/locales/ja-jp.json @@ -1274,6 +1274,7 @@ "sd_Raw_Last_Message": "生の最後のメッセージ", "sd_Background": "背景", "Image Generation": "画像生成", + "Stop Image Generation": "画像生成を停止", "sd_refine_mode": "プロンプトを生成 API に送信する前に手動で編集できるようにする", "sd_refine_mode_txt": "生成前にプロンプ​​トを編集する", "sd_interactive_mode": "「猫の写真を送ってください」のようなメッセージを送信するときに、画像を自動的に生成します。", diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 0e33087bf..052f45208 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -1430,6 +1430,7 @@ "sd_Raw_Last_Message": "原始最后一条消息", "sd_Background": "背景", "Image Generation": "图像生成", + "Stop Image Generation": "中止图像生成", "sd_refine_mode": "允许在将提示词发送到生成 API 之前手动编辑提示词", "sd_refine_mode_txt": "生成之前编辑提示词", "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。", diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index c31b03f89..dba550189 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -1026,6 +1026,7 @@ "Message Actions": "訊息動作", "Translate message": "翻譯訊息", "Generate Image": "生成圖片", + "Stop Image Generation": "終止圖片生成", "Narrate": "敘述", "Exclude message from prompts": "從提示詞中排除訊息", "Include message in prompts": "在提示詞中包含訊息", diff --git a/public/scripts/extensions/stable-diffusion/button.html b/public/scripts/extensions/stable-diffusion/button.html index 578fa8276..2962ff4f4 100644 --- a/public/scripts/extensions/stable-diffusion/button.html +++ b/public/scripts/extensions/stable-diffusion/button.html @@ -1,8 +1,8 @@
- Generate Image + Generate Image
- Stop Image Generation + Stop Image Generation
From 247a23bda93a0848df68bda5728a7e6e1a9cbd1c Mon Sep 17 00:00:00 2001 From: ceruleandeep Date: Mon, 16 Dec 2024 20:49:31 +1100 Subject: [PATCH 077/222] Fix i18n for "Generate Caption" in wand menu --- public/locales/ja-jp.json | 1 + public/locales/zh-cn.json | 1 + public/locales/zh-tw.json | 1 + public/scripts/extensions/caption/index.js | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/locales/ja-jp.json b/public/locales/ja-jp.json index 15f586b7c..f5910f065 100644 --- a/public/locales/ja-jp.json +++ b/public/locales/ja-jp.json @@ -1275,6 +1275,7 @@ "sd_Background": "背景", "Image Generation": "画像生成", "Stop Image Generation": "画像生成を停止", + "Generate Caption": "画像説明を生成", "sd_refine_mode": "プロンプトを生成 API に送信する前に手動で編集できるようにする", "sd_refine_mode_txt": "生成前にプロンプ​​トを編集する", "sd_interactive_mode": "「猫の写真を送ってください」のようなメッセージを送信するときに、画像を自動的に生成します。", diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 052f45208..cf94c4933 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -1241,6 +1241,7 @@ "Enter web URLs to scrape (one per line):": "输入要抓取的网址(每行一个):", "Enter a video URL to download its transcript.": "输入视频 URL 或 ID 即可下载其文本。", "Image Captioning": "图像描述", + "Generate Caption": "生成图片说明", "Source": "来源", "Local": "本地", "Multimodal (OpenAI / Anthropic / llama / Google)": "多模式(OpenAI / Anthropic / llama / Google)", diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index dba550189..eb938945e 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -1652,6 +1652,7 @@ "Horde": "Horde", "HuggingFace Token": "HuggingFace 符元", "Image Captioning": "圖片標註", + "Generate Caption": "生成圖片說明", "Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)", "Injection Position": "插入位置", "Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。", diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index 19c35fe71..7b7421c8b 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -393,7 +393,7 @@ jQuery(async function () { const sendButton = $(`
- Generate Caption + Generate Caption
`); $('#caption_wand_container').append(sendButton); From 2ef9f5d74891bcf46afa0a3abfa4226997d70162 Mon Sep 17 00:00:00 2001 From: ceruleandeep Date: Mon, 16 Dec 2024 21:31:30 +1100 Subject: [PATCH 078/222] Fix i18n for Prompt Inspector in wand menu --- public/locales/ja-jp.json | 5 ++++- public/locales/zh-cn.json | 5 ++++- public/locales/zh-tw.json | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/public/locales/ja-jp.json b/public/locales/ja-jp.json index f5910f065..b75eb103f 100644 --- a/public/locales/ja-jp.json +++ b/public/locales/ja-jp.json @@ -1441,5 +1441,8 @@ "Still have questions?": "まだ質問がありますか?", "Join the SillyTavern Discord": "SillyTavernのDiscordに参加", "Post a GitHub issue": "GitHubの問題を投稿", - "Contact the developers": "開発者に連絡" + "Contact the developers": "開発者に連絡", + "Stop Inspecting": "検査を停止", + "Inspect Prompts": "プロンプトを検査", + "Toggle prompt inspection": "プロンプト検査の切り替え" } diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index cf94c4933..c7fb563fe 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -1863,5 +1863,8 @@ "Still have questions?": "仍有疑问?", "Join the SillyTavern Discord": "加入SillyTavern Discord", "Post a GitHub issue": "发布GitHub问题", - "Contact the developers": "联系开发人员" + "Contact the developers": "联系开发人员", + "Stop Inspecting": "停止检查", + "Inspect Prompts": "检查提示词", + "Toggle prompt inspection": "切换提示词检查" } diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index eb938945e..83676265c 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -1789,5 +1789,8 @@ "Will be used if the API doesnt support JSON schemas or function calling.": "若 API 不支持 JSON 模式或函數調用,將使用此設定。", "World Info settings": "世界資訊設定", "You are in offline mode. Click on the image below to set the expression.": "您目前為離線狀態,請點擊下方圖片進行表情設定。", - "You can find your API key in the Stability AI dashboard.": "API 金鑰可在 Stability AI 儀表板中查看。" + "You can find your API key in the Stability AI dashboard.": "API 金鑰可在 Stability AI 儀表板中查看。", + "Stop Inspecting": "停止檢查", + "Inspect Prompts": "檢查提示詞", + "Toggle prompt inspection": "切換提示詞檢查" } From b094a355c009821e693cecf1826644c137d04f60 Mon Sep 17 00:00:00 2001 From: ceruleandeep Date: Mon, 16 Dec 2024 22:43:14 +1100 Subject: [PATCH 079/222] Remove spurious attrib tags inserted by getMissingTranslations --- public/locales/zh-tw.json | 202 +++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index 83676265c..db30ec06b 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -1447,107 +1447,107 @@ "(-1 for random)": "(-1 表示隨機)", "(Optional)": "(可選)", "(use _space": "(使用", - "[no_connection_text]api_no_connection": "未連線⋯", - "[no_desc_text]No model description": "[無描述]", - "[no_items_text]openai_logit_bias_no_items": "無項目", - "[placeholder]Any contents here will replace the default Post-History Instructions used for this character. (v2 specpost_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 specpost_history_instructions)", - "[placeholder]comma delimited,no spaces between": "逗號分割,無需空格", - "[placeholder]e.g. black-forest-labs/FLUX.1-dev": "例如:black-forest-labs/FLUX.1-dev", - "[placeholder]Example: gpt-3.5-turbo": "例如:gpt-3.5-turbo", - "[placeholder]Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1", - "[popup-button-crop]popup-button-crop": "裁剪", - "[title](disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)", - "[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)", - "[title]A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。", - "[title]A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用區域擴展的倍數。", - "[title]Abort current image generation task": "終止目前的圖片生成任務", - "[title]Add Character and User names to a list of stopping strings.": "將角色和使用者角色名稱添加至停止字符列表。", - "[title]Alignment for rank nodes.": "對排名節點的對齊方式。", - "[title]Always show the node full info panel at the bottom left of the timeline view. When off, show it near the node.": "始終將節點的完整資訊面板顯示在時間軸視圖的左下角。關閉時,將顯示在節點附近。", - "[title]Always show the node tooltip at the bottom left of the timeline view. When off, show it near the node.": "始終將節點的工具提示欄顯示在時間軸視圖的左下角。關閉時,將顯示在節點附近。", - "[title]Apply current sorting as Order": "應用此排序為順序", - "[title]Cap the number of entry activation recursions": "限制入口啟動的遞歸次數", - "[title]Caption": "標題", - "[title]Close popup": "關閉彈出視窗", - "[title]Color configuration for Timelines when 'Use UI Theme' in Style Settings is off.": "關閉「使用介面主題」的時間線顏色。", - "[title]context_allow_post_history_instructions": "在文本完成模式中包含聊天歷史後指示(Post-History Instructions),但可能導致不良輸出。", - "[title]Create a new connection profile": "建立新的連線設定檔", - "[title]Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定義導入卡片時應採取的動作。選擇「詢問」將始終顯示對話框。", - "[title]delay_until_recursion_level": "定義遞迴掃描的延遲層級。\r最初僅匹配第一層(數字最小的層級)。\r未找到匹配時,下一層將成為可匹配的層級。\r此過程會重複,直到所有層級都被檢查完畢。\r與「延遲至遞歸」設定相關聯。", - "[title]Delete a connection profile": "刪除連線設定檔", - "[title]Delete template": "刪除模板", - "[title]Delete the template": "刪除此模板", - "[title]Disabling is not recommended.": "不建議禁用。", - "[title]Display swipe numbers for all messages, not just the last.": "顯示所有訊息的滑動編號,而不僅是最後一條訊息。", - "[title]Duplicate persona": "複製使用者角色", - "[title]Edit a connection profile": "編輯連線設定檔", - "[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "啟用自動選擇輸入框中的文本,適用於彈出輸入框及其他自定義輸入框。", - "[title]Entries with a cooldown can't be activated N messages after being triggered.": "設有冷卻時間的條目於觸發後的 N 條訊息內無法再次啟用。", - "[title]Entries with a delay can't be activated until there are N messages present in the chat.": "有延遲的條目需等待聊天中出現 N 條訊息後才能啟用。", - "[title]Expand swipe nodes when the timeline view first opens, and whenever the graph is refreshed. When off, you can expand them by long-pressing a node, or by pressing the Toggle Swipes button.": "時間線視圖首次打開或刷新時展開滑動節點。關閉時可通過長按節點或點選「切換滑動」按鈕展開。", - "[title]Export Advanced Formatting settings": "匯出高級格式設定", - "[title]Export template": "匯出模板", - "[title]Find similar characters": "尋找相似角色", - "[title]Height of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時的節點像素高度。", - "[title]How the automatic graph builder assigns a rank (layout depth) to graph nodes.": "自動圖表生成器分配圖節點等級(佈局深度)的方式。", - "[title]If checked and the character card contains a Post-History Instructions override, use that instead": "勾選後,將使用角色卡中的聊天歷史後指示(Post-History Instructions)覆蓋。", - "[title]Import Advanced Formatting settings": "匯入進階格式設定\n\n您也可以提供舊版檔案作為提示詞和上下文範本使用。", - "[title]Import template": "匯入模板", - "[title]In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "在群組聊天中,突出顯示該生成回應的角色及順序。", - "[title]Include names with each message into the context for scanning": "將每條訊息的名稱納入掃描上下文", - "[title]Inserted before the first User's message": "插入到第一條使用者訊息前。", - "[title]instruct_enabled": "啟用指令模式(Instruct Mode)", - "[title]instruct_last_input_sequence": "插入到最後一條使用者訊息之前。", - "[title]instruct_template_activation_regex_desc": "連接 API 或選擇模型時,若模型名稱符合所提供的正規表達式,則自動啟動該指令模板(Instruct Template)。", - "[title]Load Asset List": "載入資源列表", - "[title]load_asset_list_desc": "根據資源列表文件載入擴展及資源。\n\n該字段中的默認資源 URL 指向官方的擴展及資源的列表。\n可在此插入您的自定義資源列表。\n\n若需安裝單個第三方擴展,請使用右上角的「安裝擴展」按鈕。", - "[title]markdown_hotkeys_desc": "啟用熱鍵以在某些文本輸入框中插入 Markdown 格式字符。詳見「/help hotkeys」。", - "[title]Not all samplers supported.": "並非所有採樣器均受支援。", - "[title]Open the timeline view. Same as the slash command '/tl'.": "打開時間線視圖,與斜杠指令「/tl」相同。", - "[title]Penalize sequences based on their length.": "根據序列長度進行懲罰。", - "[title]Reload a connection profile": "重新載入連線設定檔", - "[title]Rename current preset": "重新命名此預設", - "[title]Rename current prompt": "重新命名此提示詞", - "[title]Rename current template": "重新密名此模板", - "[title]Reset all Timelines settings to their default values.": "將所有時間軸設定重置為預設值。", - "[title]Restore current prompt": "還原目前的提示詞", - "[title]Restore current template": "還原目前的模板", - "[title]Save prompt as": "另存提示詞為", - "[title]Save template as": "另存模板為", - "[title]sd_adetailer_face": "在生成過程中使用 ADetailer 臉部模型。需在後端安裝 ADetailer 擴展。", - "[title]sd_free_extend": "自動使用目前選定的 LLM 擴展自由模式主題提示詞(不包括肖像或背景)。", - "[title]sd_function_tool": "使用功能工具自動檢測意圖以生成圖片。", - "[title]Seed_desc": "用於生成確定性和可重現輸出的隨機種子。設定為 -1 時將使用隨機種子。", - "[title]Select your current Context Template": "選擇您目前的上下文模板", - "[title]Select your current Instruct Template": "選擇您目前的指令模板", - "[title]Select your current System Prompt": "選擇您目前的系統提示詞", - "[title]Separation between adjacent edges in the same rank.": "同一層級中相鄰邊之間的間距。", - "[title]Separation between adjacent nodes in the same rank.": "同一層級中相鄰節點之間的間距。", - "[title]Separation between each rank in the layout.": "佈局中各層級之間的間距。", - "[title]Settings for the visual appearance of the Timelines graph.": "時間線圖形的視覺外觀設置。", - "[title]Show a button in the input area to ask the AI to impersonate your character for a single message": "在輸入框中添加按鈕,讓 AI 模仿您的角色身份發送一則訊息。", - "[title]Show a legend for colors corresponding to different characters and chat checkpoints.": "顯示一個圖例,標註不同角色和對話檢查點對應的顏色。", - "[title]Show the AI character's avatar as the graph root node. When off, the root node is blank.": "將 AI 角色的頭像作為圖形的根節點;關閉時,根節點為空。", - "[title]Sticky entries will stay active for N messages after being triggered.": "觸發後,置頂條目將在接下來的 N 條訊息中保持活躍。", - "[title]stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} 和 {{getglobalvar::}} 巨集的字面巨集樣值被自動解析。\n例如,{{newline}} 將保持為字面字串 {{newline}}。\n\n(此功能通過內部將 {{getvar::}} 和 {{getglobalvar::}} 巨集替換為具範圍的變數來實現。)", - "[title]Style and routing of graph edges.": "圖形邊的樣式和路徑。", - "[title]Swap width and height": "交換寬度與高度", - "[title]Swipe left": "向左滑動", - "[title]Swipe right": "向右滑動", - "[title]Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry": "切換角色/標籤篩選,將列出的角色和標籤從此條目的匹配中排除", - "[title]sysprompt_enabled": "啟用系統提示詞", - "[title]The number of sequences generated at each step with Beam Search.": "在 Beam Search 中每一步生成的序列數量。", - "[title]The visual appearance of a node in the graph.": "圖形中節點的視覺外觀。", - "[title]Update a connection profile": "更新連線設定檔", - "[title]Update current prompt": "更新此提示詞", - "[title]Update current template": "更新此模板", - "[title]Use GPU acceleration for positioning the full info panel that appears when you click a node. If the tooltip arrow tends to disappear, turning this off may help.": "啟用 GPU 加速來定位點擊節點時出現的完整資訊面板。若發現工具提示箭頭經常消失,可考慮關閉此功能。", - "[title]Use the colors of the ST GUI theme, instead of the colors configured in Color Settings specifically for this extension.": "以 ST GUI 主題的顏色,取代「顏色設定」中針對此擴展特別配置的顏色。", - "[title]View connection profile details": "查看連線設定檔詳情", - "[title]When enabled, nodes that have swipes splitting off of them will appear subtly larger, in addition to having the double border.": "啟用後,具分支滑動的節點將顯示雙重邊框,還會略微放大。", - "[title]WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized": "世界資訊條目狀態:\\r🔵 恆定\\r🟢 正常\\r🔗 向量化", - "[title]Width of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時,節點的像素寬度。", - "[title]world_button_title": "角色背景設定\\n\\n點擊以載入\\nShift+ 點擊開啟「連結至世界資訊」彈窗", + "api_no_connection": "未連線⋯", + "No model description": "[無描述]", + "openai_logit_bias_no_items": "無項目", + "Any contents here will replace the default Post-History Instructions used for this character. (v2 specpost_history_instructions)": "此處填入的內容將取代該角色的默認聊天歷史後指示(Post-History Instructions)。\n(v2 specpost_history_instructions)", + "comma delimited,no spaces between": "逗號分割,無需空格", + "e.g. black-forest-labs/FLUX.1-dev": "例如:black-forest-labs/FLUX.1-dev", + "Example: gpt-3.5-turbo": "例如:gpt-3.5-turbo", + "Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1", + "popup-button-crop": "裁剪", + "(disabled when max recursion steps are used)": "(當最大遞歸步驟數使用時將停用)", + "0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)": "0 = 無限制,1 = 掃描一次且不遞歸,2 = 掃描一次並遞歸一次,以此類推\n(使用最小啟動設定時將停用)", + "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一種用於 LLM 抽樣的貪婪演算法,用於尋找最可能的單詞或標記序列。該方法會同時展開多個候選序列,並在每一步中保持固定數量的頂級序列(beam width)。", + "A multiplicative factor to expand the overall area that the nodes take up.": "節點佔用區域擴展的倍數。", + "Abort current image generation task": "終止目前的圖片生成任務", + "Add Character and User names to a list of stopping strings.": "將角色和使用者角色名稱添加至停止字符列表。", + "Alignment for rank nodes.": "對排名節點的對齊方式。", + "Always show the node full info panel at the bottom left of the timeline view. When off, show it near the node.": "始終將節點的完整資訊面板顯示在時間軸視圖的左下角。關閉時,將顯示在節點附近。", + "Always show the node tooltip at the bottom left of the timeline view. When off, show it near the node.": "始終將節點的工具提示欄顯示在時間軸視圖的左下角。關閉時,將顯示在節點附近。", + "Apply current sorting as Order": "應用此排序為順序", + "Cap the number of entry activation recursions": "限制入口啟動的遞歸次數", + "Caption": "標題", + "Close popup": "關閉彈出視窗", + "Color configuration for Timelines when 'Use UI Theme' in Style Settings is off.": "關閉「使用介面主題」的時間線顏色。", + "context_allow_post_history_instructions": "在文本完成模式中包含聊天歷史後指示(Post-History Instructions),但可能導致不良輸出。", + "Create a new connection profile": "建立新的連線設定檔", + "Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定義導入卡片時應採取的動作。選擇「詢問」將始終顯示對話框。", + "delay_until_recursion_level": "定義遞迴掃描的延遲層級。\r最初僅匹配第一層(數字最小的層級)。\r未找到匹配時,下一層將成為可匹配的層級。\r此過程會重複,直到所有層級都被檢查完畢。\r與「延遲至遞歸」設定相關聯。", + "Delete a connection profile": "刪除連線設定檔", + "Delete template": "刪除模板", + "Delete the template": "刪除此模板", + "Disabling is not recommended.": "不建議禁用。", + "Display swipe numbers for all messages, not just the last.": "顯示所有訊息的滑動編號,而不僅是最後一條訊息。", + "Duplicate persona": "複製使用者角色", + "Edit a connection profile": "編輯連線設定檔", + "Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.": "啟用自動選擇輸入框中的文本,適用於彈出輸入框及其他自定義輸入框。", + "Entries with a cooldown can't be activated N messages after being triggered.": "設有冷卻時間的條目於觸發後的 N 條訊息內無法再次啟用。", + "Entries with a delay can't be activated until there are N messages present in the chat.": "有延遲的條目需等待聊天中出現 N 條訊息後才能啟用。", + "Expand swipe nodes when the timeline view first opens, and whenever the graph is refreshed. When off, you can expand them by long-pressing a node, or by pressing the Toggle Swipes button.": "時間線視圖首次打開或刷新時展開滑動節點。關閉時可通過長按節點或點選「切換滑動」按鈕展開。", + "Export Advanced Formatting settings": "匯出高級格式設定", + "Export template": "匯出模板", + "Find similar characters": "尋找相似角色", + "Height of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時的節點像素高度。", + "How the automatic graph builder assigns a rank (layout depth) to graph nodes.": "自動圖表生成器分配圖節點等級(佈局深度)的方式。", + "If checked and the character card contains a Post-History Instructions override, use that instead": "勾選後,將使用角色卡中的聊天歷史後指示(Post-History Instructions)覆蓋。", + "Import Advanced Formatting settings": "匯入進階格式設定\n\n您也可以提供舊版檔案作為提示詞和上下文範本使用。", + "Import template": "匯入模板", + "In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "在群組聊天中,突出顯示該生成回應的角色及順序。", + "Include names with each message into the context for scanning": "將每條訊息的名稱納入掃描上下文", + "Inserted before the first User's message": "插入到第一條使用者訊息前。", + "instruct_enabled": "啟用指令模式(Instruct Mode)", + "instruct_last_input_sequence": "插入到最後一條使用者訊息之前。", + "instruct_template_activation_regex_desc": "連接 API 或選擇模型時,若模型名稱符合所提供的正規表達式,則自動啟動該指令模板(Instruct Template)。", + "Load Asset List": "載入資源列表", + "load_asset_list_desc": "根據資源列表文件載入擴展及資源。\n\n該字段中的默認資源 URL 指向官方的擴展及資源的列表。\n可在此插入您的自定義資源列表。\n\n若需安裝單個第三方擴展,請使用右上角的「安裝擴展」按鈕。", + "markdown_hotkeys_desc": "啟用熱鍵以在某些文本輸入框中插入 Markdown 格式字符。詳見「/help hotkeys」。", + "Not all samplers supported.": "並非所有採樣器均受支援。", + "Open the timeline view. Same as the slash command '/tl'.": "打開時間線視圖,與斜杠指令「/tl」相同。", + "Penalize sequences based on their length.": "根據序列長度進行懲罰。", + "Reload a connection profile": "重新載入連線設定檔", + "Rename current preset": "重新命名此預設", + "Rename current prompt": "重新命名此提示詞", + "Rename current template": "重新密名此模板", + "Reset all Timelines settings to their default values.": "將所有時間軸設定重置為預設值。", + "Restore current prompt": "還原目前的提示詞", + "Restore current template": "還原目前的模板", + "Save prompt as": "另存提示詞為", + "Save template as": "另存模板為", + "sd_adetailer_face": "在生成過程中使用 ADetailer 臉部模型。需在後端安裝 ADetailer 擴展。", + "sd_free_extend": "自動使用目前選定的 LLM 擴展自由模式主題提示詞(不包括肖像或背景)。", + "sd_function_tool": "使用功能工具自動檢測意圖以生成圖片。", + "Seed_desc": "用於生成確定性和可重現輸出的隨機種子。設定為 -1 時將使用隨機種子。", + "Select your current Context Template": "選擇您目前的上下文模板", + "Select your current Instruct Template": "選擇您目前的指令模板", + "Select your current System Prompt": "選擇您目前的系統提示詞", + "Separation between adjacent edges in the same rank.": "同一層級中相鄰邊之間的間距。", + "Separation between adjacent nodes in the same rank.": "同一層級中相鄰節點之間的間距。", + "Separation between each rank in the layout.": "佈局中各層級之間的間距。", + "Settings for the visual appearance of the Timelines graph.": "時間線圖形的視覺外觀設置。", + "Show a button in the input area to ask the AI to impersonate your character for a single message": "在輸入框中添加按鈕,讓 AI 模仿您的角色身份發送一則訊息。", + "Show a legend for colors corresponding to different characters and chat checkpoints.": "顯示一個圖例,標註不同角色和對話檢查點對應的顏色。", + "Show the AI character's avatar as the graph root node. When off, the root node is blank.": "將 AI 角色的頭像作為圖形的根節點;關閉時,根節點為空。", + "Sticky entries will stay active for N messages after being triggered.": "觸發後,置頂條目將在接下來的 N 條訊息中保持活躍。", + "stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} 和 {{getglobalvar::}} 巨集的字面巨集樣值被自動解析。\n例如,{{newline}} 將保持為字面字串 {{newline}}。\n\n(此功能通過內部將 {{getvar::}} 和 {{getglobalvar::}} 巨集替換為具範圍的變數來實現。)", + "Style and routing of graph edges.": "圖形邊的樣式和路徑。", + "Swap width and height": "交換寬度與高度", + "Swipe left": "向左滑動", + "Swipe right": "向右滑動", + "Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry": "切換角色/標籤篩選,將列出的角色和標籤從此條目的匹配中排除", + "sysprompt_enabled": "啟用系統提示詞", + "The number of sequences generated at each step with Beam Search.": "在 Beam Search 中每一步生成的序列數量。", + "The visual appearance of a node in the graph.": "圖形中節點的視覺外觀。", + "Update a connection profile": "更新連線設定檔", + "Update current prompt": "更新此提示詞", + "Update current template": "更新此模板", + "Use GPU acceleration for positioning the full info panel that appears when you click a node. If the tooltip arrow tends to disappear, turning this off may help.": "啟用 GPU 加速來定位點擊節點時出現的完整資訊面板。若發現工具提示箭頭經常消失,可考慮關閉此功能。", + "Use the colors of the ST GUI theme, instead of the colors configured in Color Settings specifically for this extension.": "以 ST GUI 主題的顏色,取代「顏色設定」中針對此擴展特別配置的顏色。", + "View connection profile details": "查看連線設定檔詳情", + "When enabled, nodes that have swipes splitting off of them will appear subtly larger, in addition to having the double border.": "啟用後,具分支滑動的節點將顯示雙重邊框,還會略微放大。", + "WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized": "世界資訊條目狀態:\\r🔵 恆定\\r🟢 正常\\r🔗 向量化", + "Width of a node, in pixels at zoom level 1.0.": "縮放等級為 1.0 時,節點的像素寬度。", + "world_button_title": "角色背景設定\\n\\n點擊以載入\\nShift+ 點擊開啟「連結至世界資訊」彈窗", "# of Beams": "# of Beams", "01.AI API Key": "01.AI API 金鑰", "01.AI Model": "01.AI 模型", From 7eacdac6658f5a151b31e6684a20a921b714c645 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:29:41 +0200 Subject: [PATCH 080/222] Add bypass status check for Generic TC API type --- public/index.html | 2 +- public/script.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index cc68a5c39..2a195373f 100644 --- a/public/index.html +++ b/public/index.html @@ -2616,7 +2616,7 @@
-
From 7e7b3e30c481e128d78c1e6391ecebc6e230ba6e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 19 Dec 2024 21:17:47 +0200 Subject: [PATCH 085/222] Tool Calling: Implement stealth tool defintions (#3192) * Tool Calling: Implement stealth tool defintions * Move isStealth check up * Always stop generation on stealth tool calls * Image Generation: use stealth flag for tool registration * Update stealth property description to clarify no follow-up generation will be performed * Revert "Image Generation: use stealth flag for tool registration" This reverts commit 8d13445c0b66e4c0ef1ddcfaf18ab185464de600. --- public/script.js | 14 +++++--- public/scripts/tool-calling.js | 63 +++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/public/script.js b/public/script.js index a717a0dc3..749a45343 100644 --- a/public/script.js +++ b/public/script.js @@ -4579,9 +4579,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor?.result); hasToolCalls && shouldDeleteMessage && await deleteLastMessage(); const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls); + const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length; if (hasToolCalls) { - if (!invocationResult.invocations.length && shouldDeleteMessage) { - ToolManager.showToolCallError(invocationResult.errors); + if (shouldStopGeneration) { + if (Array.isArray(invocationResult.errors) && invocationResult.errors.length) { + ToolManager.showToolCallError(invocationResult.errors); + } unblockGeneration(type); generatedPromptCache = ''; streamingProcessor = null; @@ -4681,9 +4684,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage); hasToolCalls && shouldDeleteMessage && await deleteLastMessage(); const invocationResult = await ToolManager.invokeFunctionTools(data); + const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length; if (hasToolCalls) { - if (!invocationResult.invocations.length && shouldDeleteMessage) { - ToolManager.showToolCallError(invocationResult.errors); + if (shouldStopGeneration) { + if (Array.isArray(invocationResult.errors) && invocationResult.errors.length) { + ToolManager.showToolCallError(invocationResult.errors); + } unblockGeneration(type); generatedPromptCache = ''; return; diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js index deda15289..b56e3027c 100644 --- a/public/scripts/tool-calling.js +++ b/public/scripts/tool-calling.js @@ -25,6 +25,7 @@ import { isTrueBoolean } from './utils.js'; * @typedef {object} ToolInvocationResult * @property {ToolInvocation[]} invocations Successful tool invocations * @property {Error[]} errors Errors that occurred during tool invocation + * @property {string[]} stealthCalls Names of stealth tools that were invoked */ /** @@ -36,6 +37,7 @@ import { isTrueBoolean } from './utils.js'; * @property {function} action - The action to perform when the tool is invoked. * @property {function} [formatMessage] - A function to format the tool call message. * @property {function} [shouldRegister] - A function to determine if the tool should be registered. + * @property {boolean} [stealth] - A tool call result will not be shown in the chat. No follow-up generation will be performed. */ /** @@ -147,6 +149,12 @@ class ToolDefinition { */ #shouldRegister; + /** + * A tool call result will not be shown in the chat. No follow-up generation will be performed. + * @type {boolean} + */ + #stealth; + /** * Creates a new ToolDefinition. * @param {string} name A unique name for the tool. @@ -156,8 +164,9 @@ class ToolDefinition { * @param {function} action A function that will be called when the tool is executed. * @param {function} formatMessage A function that will be called to format the tool call toast. * @param {function} shouldRegister A function that will be called to determine if the tool should be registered. + * @param {boolean} stealth A tool call result will not be shown in the chat. No follow-up generation will be performed. */ - constructor(name, displayName, description, parameters, action, formatMessage, shouldRegister) { + constructor(name, displayName, description, parameters, action, formatMessage, shouldRegister, stealth) { this.#name = name; this.#displayName = displayName; this.#description = description; @@ -165,6 +174,7 @@ class ToolDefinition { this.#action = action; this.#formatMessage = formatMessage; this.#shouldRegister = shouldRegister; + this.#stealth = stealth; } /** @@ -214,6 +224,10 @@ class ToolDefinition { get displayName() { return this.#displayName; } + + get stealth() { + return this.#stealth; + } } /** @@ -246,7 +260,7 @@ export class ToolManager { * Registers a new tool with the tool registry. * @param {ToolRegistration} tool The tool to register. */ - static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage, shouldRegister }) { + static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage, shouldRegister, stealth }) { // Convert WIP arguments if (typeof arguments[0] !== 'object') { [name, description, parameters, action] = arguments; @@ -256,7 +270,16 @@ export class ToolManager { console.warn(`[ToolManager] A tool with the name "${name}" has already been registered. The definition will be overwritten.`); } - const definition = new ToolDefinition(name, displayName, description, parameters, action, formatMessage, shouldRegister); + const definition = new ToolDefinition( + name, + displayName, + description, + parameters, + action, + formatMessage, + shouldRegister, + stealth, + ); this.#tools.set(name, definition); console.log('[ToolManager] Registered function tool:', definition); } @@ -302,6 +325,20 @@ export class ToolManager { } } + /** + * Checks if a tool is a stealth tool. + * @param {string} name The name of the tool to check. + * @returns {boolean} Whether the tool is a stealth tool. + */ + static isStealthTool(name) { + if (!this.#tools.has(name)) { + return false; + } + + const tool = this.#tools.get(name); + return !!tool.stealth; + } + /** * Formats a message for a tool call by name. * @param {string} name The name of the tool to format the message for. @@ -608,6 +645,7 @@ export class ToolManager { const result = { invocations: [], errors: [], + stealthCalls: [], }; const toolCalls = ToolManager.#getToolCallsFromData(data); @@ -625,7 +663,7 @@ export class ToolManager { const parameters = toolCall.function.arguments; const name = toolCall.function.name; const displayName = ToolManager.getDisplayName(name); - + const isStealth = ToolManager.isStealthTool(name); const message = await ToolManager.formatToolCallMessage(name, parameters); const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 }); const toolResult = await ToolManager.invokeFunctionTool(name, parameters); @@ -638,6 +676,12 @@ export class ToolManager { continue; } + // Don't save stealth tool invocations + if (isStealth) { + result.stealthCalls.push(name); + continue; + } + const invocation = { id, displayName, @@ -860,6 +904,14 @@ export class ToolManager { isRequired: false, acceptsMultiple: false, }), + SlashCommandNamedArgument.fromProps({ + name: 'stealth', + description: 'If true, a tool call result will not be shown in the chat and no follow-up generation will be performed.', + typeList: [ARGUMENT_TYPE.BOOLEAN], + isRequired: false, + acceptsMultiple: false, + defaultValue: String(false), + }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ @@ -891,7 +943,7 @@ export class ToolManager { }; } - const { name, displayName, description, parameters, formatMessage, shouldRegister } = args; + const { name, displayName, description, parameters, formatMessage, shouldRegister, stealth } = args; if (!(action instanceof SlashCommandClosure)) { throw new Error('The unnamed argument must be a closure.'); @@ -927,6 +979,7 @@ export class ToolManager { action: actionFunc, formatMessage: formatMessageFunc, shouldRegister: shouldRegisterFunc, + stealth: stealth && isTrueBoolean(String(stealth)), }); return ''; From 1dd97c139e87a1b1c513438aa25e08ea1512e28a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:59:36 +0200 Subject: [PATCH 086/222] Text Completion: Improve generic source model display --- public/img/generic.svg | 15 +++++++++++++++ public/script.js | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 public/img/generic.svg diff --git a/public/img/generic.svg b/public/img/generic.svg new file mode 100644 index 000000000..2411ed069 --- /dev/null +++ b/public/img/generic.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/public/script.js b/public/script.js index 749a45343..312418487 100644 --- a/public/script.js +++ b/public/script.js @@ -1223,7 +1223,7 @@ async function getStatusTextgen() { setOnlineStatus(textgen_settings.tabby_model || data?.result); } else if (textgen_settings.type === textgen_types.GENERIC) { loadGenericModels(data?.data); - setOnlineStatus(textgen_settings.generic_model || 'Connected'); + setOnlineStatus(textgen_settings.generic_model || data?.result || 'Connected'); } else { setOnlineStatus(data?.result); } From 4232f6c5f45ce82985781207b0493e781ef41bf8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:00:36 +0200 Subject: [PATCH 087/222] More null checks for llamacpp logprobs parser --- public/scripts/textgen-settings.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index c4f8d9a0a..44153d05e 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -1039,11 +1039,13 @@ export function parseTextgenLogprobs(token, logprobs) { return { token, topLogprobs: candidates }; } case LLAMACPP: { - /** @type {Record[]} */ if (!logprobs?.length) { return null; } - const candidates = logprobs[0].probs.map(x => [x.tok_str, x.prob]); + const candidates = logprobs?.[0]?.probs?.map(x => [x.tok_str, x.prob]); + if (!candidates) { + return null; + } return { token, topLogprobs: candidates }; } default: From 94de9411b69b02221e6bdec46d4b6af1a1b0b6d4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:20:46 +0200 Subject: [PATCH 088/222] UI performance fixes (#3207) * Optimize visibility checks for burger and wand menus * Optimize message actions visibility toggle * Run drawer toggle in animation frame * Replace jQuery slideToggle with a 3rd-party lib * Refactor export button functionality to manage popup state with a boolean flag * Do not close the pinned drawer on unpin * Revert "Do not close the pinned drawer on unpin" This reverts commit e3b34e9a586db853dd84809f4187d5b29cb9ac36. * Refactor slideToggle options * ease-in-out * Don't skip frame on drawer toggle --- package-lock.json | 7 + package.json | 1 + public/lib.js | 3 + public/script.js | 205 +++++++++++++++++------------ public/scripts/RossAscends-mods.js | 15 ++- public/scripts/extensions.js | 12 +- public/style.css | 3 +- 7 files changed, 150 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2419fe002..fa23bf5a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "showdown": "^2.1.0", "sillytavern-transformers": "2.14.6", "simple-git": "^3.19.1", + "slidetoggle": "^4.0.0", "tiktoken": "^1.0.16", "vectra": "^0.2.2", "wavefile": "^11.0.0", @@ -6531,6 +6532,12 @@ "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==", "license": "MIT" }, + "node_modules/slidetoggle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slidetoggle/-/slidetoggle-4.0.0.tgz", + "integrity": "sha512-6qvrOS1dnDFEr41UEEFFRQE8nswaAFIYZAHer6dVlznRIjHyCISjNJoxIn5U5QlAbZfBBxTELQk4jS7miHto1A==", + "license": "MIT" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", diff --git a/package.json b/package.json index 777d99076..26d979a7f 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "showdown": "^2.1.0", "sillytavern-transformers": "2.14.6", "simple-git": "^3.19.1", + "slidetoggle": "^4.0.0", "tiktoken": "^1.0.16", "vectra": "^0.2.2", "wavefile": "^11.0.0", diff --git a/public/lib.js b/public/lib.js index 2870d018d..4c6f318af 100644 --- a/public/lib.js +++ b/public/lib.js @@ -19,6 +19,7 @@ import seedrandom from 'seedrandom'; import * as Popper from '@popperjs/core'; import droll from 'droll'; import morphdom from 'morphdom'; +import { toggle as slideToggle } from 'slidetoggle'; /** * Expose the libraries to the 'window' object. @@ -94,6 +95,7 @@ export default { Popper, droll, morphdom, + slideToggle, }; export { @@ -115,4 +117,5 @@ export { Popper, droll, morphdom, + slideToggle, }; diff --git a/public/script.js b/public/script.js index 312418487..04eb8bc09 100644 --- a/public/script.js +++ b/public/script.js @@ -10,6 +10,7 @@ import { SVGInject, Popper, initLibraryShims, + slideToggle, default as libs, } from './lib.js'; @@ -549,6 +550,7 @@ let optionsPopper = Popper.createPopper(document.getElementById('options_button' let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), { placement: 'left', }); +let isExportPopupOpen = false; // Saved here for performance reasons const messageTemplate = $('#message_template .mes'); @@ -895,6 +897,13 @@ export function getRequestHeaders() { }; } +export function getSlideToggleOptions() { + return { + miliseconds: animation_duration * 1.5, + transitionFunction: animation_duration > 0 ? 'ease-in-out' : 'step-start', + }; +} + $.ajaxPrefilter((options, originalOptions, xhr) => { xhr.setRequestHeader('X-CSRF-Token', token); }); @@ -9209,40 +9218,48 @@ function doDrawerOpenClick() { * @returns {void} */ function doNavbarIconClick() { - var icon = $(this).find('.drawer-icon'); - var drawer = $(this).parent().find('.drawer-content'); + const icon = $(this).find('.drawer-icon'); + const drawer = $(this).parent().find('.drawer-content'); if (drawer.hasClass('resizing')) { return; } - var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer'); - let targetDrawerID = $(this).parent().find('.drawer-content').attr('id'); + const drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer'); + const targetDrawerID = $(this).parent().find('.drawer-content').attr('id'); const pinnedDrawerClicked = drawer.hasClass('pinnedOpen'); if (!drawerWasOpenAlready) { //to open the drawer - $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () { - await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); + $('.openDrawer').not('.pinnedOpen').addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: function (el) { + el.closest('.drawer-content').classList.remove('resizing'); + }, + }); }); - $('.openIcon').toggleClass('closedIcon openIcon'); + $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon'); $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer'); icon.toggleClass('openIcon closedIcon'); drawer.toggleClass('openDrawer closedDrawer'); //console.log(targetDrawerID); if (targetDrawerID === 'right-nav-panel') { - $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({ - duration: 200, - easing: 'swing', - start: function () { - jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll - }, - complete: async function () { - favsToHotswap(); - await delay(50); - $(this).closest('.drawer-content').removeClass('resizing'); - $('#rm_print_characters_block').trigger('scroll'); - }, + $(this).closest('.drawer').find('.drawer-content').addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + elementDisplayStyle: 'flex', + onAnimationEnd: function (el) { + el.closest('.drawer-content').classList.remove('resizing'); + favsToHotswap(); + $('#rm_print_characters_block').trigger('scroll'); + }, + }); }); } else { - $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, 'swing', async function () { - await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); + $(this).closest('.drawer').find('.drawer-content').addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: function (el) { + el.closest('.drawer-content').classList.remove('resizing'); + }, + }); }); } @@ -9258,13 +9275,23 @@ function doNavbarIconClick() { icon.toggleClass('closedIcon openIcon'); if (pinnedDrawerClicked) { - $(drawer).addClass('resizing').slideToggle(200, 'swing', async function () { - await delay(50); $(this).removeClass('resizing'); + $(drawer).addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: function (el) { + el.classList.remove('resizing'); + }, + }); }); } else { - $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () { - await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); + $('.openDrawer').not('.pinnedOpen').addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: function (el) { + el.closest('.drawer-content').classList.remove('resizing'); + }, + }); }); } @@ -10086,20 +10113,21 @@ jQuery(async function () { await getStatusNovel(); }); - var button = $('#options_button'); - var menu = $('#options'); + const button = $('#options_button'); + const menu = $('#options'); + let isOptionsMenuVisible = false; function showMenu() { showBookmarksButtons(); - // menu.stop() menu.fadeIn(animation_duration); optionsPopper.update(); + isOptionsMenuVisible = true; } function hideMenu() { - // menu.stop(); menu.fadeOut(animation_duration); optionsPopper.update(); + isOptionsMenuVisible = false; } function isMouseOverButtonOrMenu() { @@ -10107,26 +10135,15 @@ jQuery(async function () { } button.on('click', function () { - if (menu.is(':visible')) { + if (isOptionsMenuVisible) { hideMenu(); } else { showMenu(); } }); - button.on('blur', function () { - //delay to prevent menu hiding when mouse leaves button into menu - setTimeout(() => { - if (!isMouseOverButtonOrMenu()) { hideMenu(); } - }, 100); - }); - menu.on('blur', function () { - //delay to prevent menu hide when mouseleaves menu into button - setTimeout(() => { - if (!isMouseOverButtonOrMenu()) { hideMenu(); } - }, 100); - }); $(document).on('click', function () { - if (!isMouseOverButtonOrMenu() && menu.is(':visible')) { hideMenu(); } + if (!isOptionsMenuVisible) return; + if (!isMouseOverButtonOrMenu()) { hideMenu(); } }); /* $('#set_chat_scenario').on('click', setScenarioOverride); */ @@ -10522,22 +10539,28 @@ jQuery(async function () { }); $(document).on('click', '.extraMesButtonsHint', function (e) { - const elmnt = e.target; - $(elmnt).transition({ + const $hint = $(e.target); + const $buttons = $hint.siblings('.extraMesButtons'); + + $hint.transition({ opacity: 0, duration: animation_duration, - easing: 'ease-in-out', + easing: animation_easing, + complete: function () { + $hint.hide(); + $buttons + .addClass('visible') + .css({ + opacity: 0, + display: 'flex', + }) + .transition({ + opacity: 1, + duration: animation_duration, + easing: animation_easing, + }); + }, }); - setTimeout(function () { - $(elmnt).hide(); - $(elmnt).siblings('.extraMesButtons').css('opcacity', '0'); - $(elmnt).siblings('.extraMesButtons').css('display', 'flex'); - $(elmnt).siblings('.extraMesButtons').transition({ - opacity: 1, - duration: animation_duration, - easing: 'ease-in-out', - }); - }, animation_duration); }); $(document).on('click', function (e) { @@ -10548,23 +10571,36 @@ jQuery(async function () { // Check if the click was outside the relevant elements if (!$(e.target).closest('.extraMesButtons, .extraMesButtonsHint').length) { + const $visibleButtons = $('.extraMesButtons.visible'); + + if (!$visibleButtons.length) { + return; + } + + const $hiddenHints = $('.extraMesButtonsHint:hidden'); + // Transition out the .extraMesButtons first - $('.extraMesButtons:visible').transition({ + $visibleButtons.transition({ opacity: 0, duration: animation_duration, - easing: 'ease-in-out', + easing: animation_easing, complete: function () { - $(this).hide(); // Hide the .extraMesButtons after the transition + // Hide the .extraMesButtons after the transition + $(this) + .hide() + .removeClass('visible'); // Transition the .extraMesButtonsHint back in - $('.extraMesButtonsHint:not(:visible)').show().transition({ - opacity: .3, - duration: animation_duration, - easing: 'ease-in-out', - complete: function () { - $(this).css('opacity', ''); - }, - }); + $hiddenHints + .show() + .transition({ + opacity: 0.3, + duration: animation_duration, + easing: animation_easing, + complete: function () { + $(this).css('opacity', ''); + }, + }); }, }); } @@ -10748,8 +10784,9 @@ jQuery(async function () { } }); - $('#export_button').on('click', function (e) { - $('#export_format_popup').toggle(); + $('#export_button').on('click', function () { + isExportPopupOpen = !isExportPopupOpen; + $('#export_format_popup').toggle(isExportPopupOpen); exportPopper.update(); }); @@ -10760,6 +10797,10 @@ jQuery(async function () { return; } + $('#export_format_popup').hide(); + isExportPopupOpen = false; + exportPopper.update(); + // Save before exporting await createOrEditCharacter(); const body = { format, avatar_url: characters[this_chid].avatar }; @@ -10781,9 +10822,6 @@ jQuery(async function () { URL.revokeObjectURL(a.href); document.body.removeChild(a); } - - - $('#export_format_popup').hide(); }); //**************************CHAT IMPORT EXPORT*************************// $('#chat_import_button').click(function () { @@ -10855,15 +10893,18 @@ jQuery(async function () { }); $(document).on('click', '.drawer-opener', doDrawerOpenClick); + $('.drawer-toggle').on('click', doNavbarIconClick); $('html').on('touchstart mousedown', function (e) { var clickTarget = $(e.target); - if ($('#export_format_popup').is(':visible') + if (isExportPopupOpen && clickTarget.closest('#export_button').length == 0 && clickTarget.closest('#export_format_popup').length == 0) { $('#export_format_popup').hide(); + isExportPopupOpen = false; + exportPopper.update(); } const forbiddenTargets = [ @@ -10888,12 +10929,16 @@ jQuery(async function () { if ($('.openDrawer').length !== 0) { if (targetParentHasOpenDrawer === 0) { //console.log($('.openDrawer').not('.pinnedOpen').length); - $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', function () { - $(this).closest('.drawer-content').removeClass('resizing'); + $('.openDrawer').not('.pinnedOpen').addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: (el) => { + el.closest('.drawer-content').classList.remove('resizing'); + }, + }); }); $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon'); $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer'); - } } } @@ -11059,14 +11104,6 @@ jQuery(async function () { case 'renameCharButton': renameCharacter(); break; - /*case 'dupe_button': - DupeChar(); - break; - case 'export_button': - $('#export_format_popup').toggle(); - exportPopper.update(); - break; - */ case 'import_character_info': await importEmbeddedWorldInfo(); saveCharacterDebounced(); diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index fb55e42dd..750e9cdd2 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -1,4 +1,4 @@ -import { DOMPurify, Bowser } from '../lib.js'; +import { DOMPurify, Bowser, slideToggle } from '../lib.js'; import { characters, @@ -19,6 +19,7 @@ import { menu_type, substituteParams, sendTextareaMessage, + getSlideToggleOptions, } from '../script.js'; import { @@ -748,8 +749,8 @@ export function initRossMods() { $(RightNavDrawerIcon).removeClass('drawerPinnedOpen'); if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) { - $(RightNavPanel).slideToggle(200, 'swing'); - $(RightNavDrawerIcon).toggleClass('openIcon closedIcon'); + slideToggle(RightNavPanel, getSlideToggleOptions()); + $(RightNavDrawerIcon).toggleClass('closedIcon openIcon'); $(RightNavPanel).toggleClass('openDrawer closedDrawer'); } } @@ -766,8 +767,8 @@ export function initRossMods() { $(LeftNavDrawerIcon).removeClass('drawerPinnedOpen'); if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) { - $(LeftNavPanel).slideToggle(200, 'swing'); - $(LeftNavDrawerIcon).toggleClass('openIcon closedIcon'); + slideToggle(LeftNavPanel, getSlideToggleOptions()); + $(LeftNavDrawerIcon).toggleClass('closedIcon openIcon'); $(LeftNavPanel).toggleClass('openDrawer closedDrawer'); } } @@ -786,8 +787,8 @@ export function initRossMods() { if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) { console.debug('closing WI after lock removal'); - $(WorldInfo).slideToggle(200, 'swing'); - $(WIDrawerIcon).toggleClass('openIcon closedIcon'); + slideToggle(WorldInfo, getSlideToggleOptions()); + $(WIDrawerIcon).toggleClass('closedIcon openIcon'); $(WorldInfo).toggleClass('openDrawer closedDrawer'); } } diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 0e6609322..789540349 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -417,26 +417,30 @@ async function addExtensionsButtonAndMenu() { const button = $('#extensionsMenuButton'); const dropdown = $('#extensionsMenu'); - //dropdown.hide(); + let isDropdownVisible = false; let popper = Popper.createPopper(button.get(0), dropdown.get(0), { placement: 'top-start', }); $(button).on('click', function () { - if (dropdown.is(':visible')) { + if (isDropdownVisible) { dropdown.fadeOut(animation_duration); + isDropdownVisible = false; } else { dropdown.fadeIn(animation_duration); + isDropdownVisible = true; } popper.update(); }); $('html').on('click', function (e) { + if (!isDropdownVisible) return; const clickTarget = $(e.target); const noCloseTargets = ['#sd_gen', '#extensionsMenuButton', '#roll_dice']; - if (dropdown.is(':visible') && !noCloseTargets.some(id => clickTarget.closest(id).length > 0)) { - $(dropdown).fadeOut(animation_duration); + if (!noCloseTargets.some(id => clickTarget.closest(id).length > 0)) { + dropdown.fadeOut(animation_duration); + isDropdownVisible = false; } }); } diff --git a/public/style.css b/public/style.css index 3246b2ca2..c9033bcad 100644 --- a/public/style.css +++ b/public/style.css @@ -2874,6 +2874,7 @@ input[type=search]:focus::-webkit-search-cancel-button { .mes_block .ch_name { max-width: 100%; + min-height: 22px; } /*applies to both groups and solos chars in the char list*/ @@ -4043,7 +4044,7 @@ input[type="range"]::-webkit-slider-thumb { .mes_button, .extraMesButtons>div { cursor: pointer; - transition: 0.3s ease-in-out; + transition: opacity 0.2s ease-in-out; filter: drop-shadow(0px 0px 2px black); opacity: 0.3; padding: 1px 3px; From 73614f2f8d97646a62f60859eefa8a01cb0de44f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 20 Dec 2024 23:30:57 +0200 Subject: [PATCH 089/222] Refactor prompt converters with group names awareness --- public/scripts/group-chats.js | 11 ++ public/scripts/openai.js | 8 +- src/endpoints/backends/chat-completions.js | 27 ++- src/prompt-converters.js | 210 ++++++++++++--------- 4 files changed, 148 insertions(+), 108 deletions(-) diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 1152b2471..86e36bead 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -275,6 +275,17 @@ export function getGroupMembers(groupId = selected_group) { return group?.members.map(member => characters.find(x => x.avatar === member)) ?? []; } +/** + * Retrieves the member names of a group. If the group is not selected, an empty array is returned. + * @returns {string[]} An array of character names representing the members of the group. + */ +export function getGroupNames() { + const groupMembers = selected_group ? groups.find(x => x.id == selected_group)?.members : null; + return Array.isArray(groupMembers) + ? groupMembers.map(x => characters.find(y => y.avatar === x)?.name).filter(x => x) + : []; +} + /** * Finds the character ID for a group member. * @param {string} arg 0-based member index or character name diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 8edc3c682..914d6d24a 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -33,7 +33,7 @@ import { system_message_types, this_chid, } from '../script.js'; -import { groups, selected_group } from './group-chats.js'; +import { getGroupNames, selected_group } from './group-chats.js'; import { chatCompletionDefaultPrompts, @@ -543,10 +543,7 @@ function setupChatCompletionPromptManager(openAiSettings) { * @returns {Message[]} Array of message objects */ export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) { - const groupMembers = selected_group ? groups.find(x => x.id == selected_group)?.members : null; - const groupBotNames = Array.isArray(groupMembers) - ? groupMembers.map(x => characters.find(y => y.avatar === x)?.name).filter(x => x).map(x => `${x}:`) - : []; + const groupBotNames = getGroupNames().map(name => `${name}:`); let result = []; // array of msgs let tmp = messageExampleString.split('\n'); @@ -1877,6 +1874,7 @@ async function sendOpenAIRequest(type, messages, signal) { 'n': canMultiSwipe ? oai_settings.n : undefined, 'user_name': name1, 'char_name': name2, + 'group_names': getGroupNames(), }; // Empty array will produce a validation error diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 519371ad4..64e6ab345 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -27,6 +27,7 @@ import { mergeMessages, cachingAtDepthForOpenRouterClaude, cachingAtDepthForClaude, + getPromptNames, } from '../../prompt-converters.js'; import { readSecret, SECRET_KEYS } from '../secrets.js'; @@ -55,17 +56,16 @@ const API_NANOGPT = 'https://nano-gpt.com/api/v1'; * Applies a post-processing step to the generated messages. * @param {object[]} messages Messages to post-process * @param {string} type Prompt conversion type - * @param {string} charName Character name - * @param {string} userName User name + * @param {import('../../prompt-converters.js').PromptNames} names Prompt names * @returns */ -function postProcessPrompt(messages, type, charName, userName) { +function postProcessPrompt(messages, type, names) { switch (type) { case 'merge': case 'claude': - return mergeMessages(messages, charName, userName, false); + return mergeMessages(messages, names, false); case 'strict': - return mergeMessages(messages, charName, userName, true); + return mergeMessages(messages, names, true); default: return messages; } @@ -101,7 +101,7 @@ async function sendClaudeRequest(request, response) { const additionalHeaders = {}; const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0; const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt; - const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, request.body.char_name, request.body.user_name); + const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request)); // Add custom stop sequences const stopSequences = []; if (Array.isArray(request.body.stop)) { @@ -282,9 +282,9 @@ async function sendMakerSuiteRequest(request, response) { model.includes('gemini-1.5-flash') || model.includes('gemini-1.5-pro') || model.startsWith('gemini-exp') - ) && request.body.use_makersuite_sysprompt; + ) && request.body.use_makersuite_sysprompt; - const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, request.body.char_name, request.body.user_name); + const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request)); let body = { contents: prompt.contents, safetySettings: GEMINI_SAFETY, @@ -384,7 +384,7 @@ async function sendAI21Request(request, response) { request.socket.on('close', function () { controller.abort(); }); - const convertedPrompt = convertAI21Messages(request.body.messages, request.body.char_name, request.body.user_name); + const convertedPrompt = convertAI21Messages(request.body.messages, getPromptNames(request)); const body = { messages: convertedPrompt, model: request.body.model, @@ -447,7 +447,7 @@ async function sendMistralAIRequest(request, response) { } try { - const messages = convertMistralMessages(request.body.messages, request.body.char_name, request.body.user_name); + const messages = convertMistralMessages(request.body.messages, getPromptNames(request)); const controller = new AbortController(); request.socket.removeAllListeners('close'); request.socket.on('close', function () { @@ -528,7 +528,7 @@ async function sendCohereRequest(request, response) { } try { - const convertedHistory = convertCohereMessages(request.body.messages, request.body.char_name, request.body.user_name); + const convertedHistory = convertCohereMessages(request.body.messages, getPromptNames(request)); const tools = []; if (Array.isArray(request.body.tools) && request.body.tools.length > 0) { @@ -886,15 +886,14 @@ router.post('/generate', jsonParser, function (request, response) { request.body.messages = postProcessPrompt( request.body.messages, request.body.custom_prompt_post_processing, - request.body.char_name, - request.body.user_name); + getPromptNames(request)); } } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) { apiUrl = API_PERPLEXITY; apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY); headers = {}; bodyParams = {}; - request.body.messages = postProcessPrompt(request.body.messages, 'strict', request.body.char_name, request.body.user_name); + request.body.messages = postProcessPrompt(request.body.messages, 'strict', getPromptNames(request)); } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) { apiUrl = API_GROQ; apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ); diff --git a/src/prompt-converters.js b/src/prompt-converters.js index 47f5989cc..cdee18d48 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -3,6 +3,30 @@ import { getConfigValue } from './util.js'; const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.'); +/** + * @typedef {object} PromptNames + * @property {string} charName Character name + * @property {string} userName User name + * @property {string[]} groupNames Group member names + * @property {function(string): boolean} startsFromGroupName Check if a message starts with a group name + */ + +/** + * Extracts the character name, user name, and group member names from the request. + * @param {import('express').Request} request Express request object + * @returns {PromptNames} Prompt names + */ +export function getPromptNames(request) { + return { + charName: String(request.body.char_name || ''), + userName: String(request.body.user_name || ''), + groupNames: Array.isArray(request.body.group_names) ? request.body.group_names.map(String) : [], + startsFromGroupName: function (message) { + return this.groupNames.some(name => message.startsWith(`${name}: `)); + }, + }; +} + /** * Convert a prompt from the ChatML objects to the format used by Claude. * Mainly deprecated. Only used for counting tokens. @@ -91,10 +115,10 @@ export function convertClaudePrompt(messages, addAssistantPostfix, addAssistantP * @param {string} prefillString User determined prefill string * @param {boolean} useSysPrompt See if we want to use a system prompt * @param {boolean} useTools See if we want to use tools - * @param {string} charName Character name - * @param {string} userName User name + * @param {PromptNames} names Prompt names + * @returns {{messages: object[], systemPrompt: object[]}} Prompt for Anthropic */ -export function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, charName, userName) { +export function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, names) { let systemPrompt = []; if (useSysPrompt) { // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array. @@ -104,14 +128,14 @@ export function convertClaudeMessages(messages, prefillString, useSysPrompt, use break; } // Append example names if not already done by the frontend (e.g. for group chats). - if (userName && messages[i].name === 'example_user') { - if (!messages[i].content.startsWith(`${userName}: `)) { - messages[i].content = `${userName}: ${messages[i].content}`; + if (names.userName && messages[i].name === 'example_user') { + if (!messages[i].content.startsWith(`${names.userName}: `)) { + messages[i].content = `${names.userName}: ${messages[i].content}`; } } - if (charName && messages[i].name === 'example_assistant') { - if (!messages[i].content.startsWith(`${charName}: `)) { - messages[i].content = `${charName}: ${messages[i].content}`; + if (names.charName && messages[i].name === 'example_assistant') { + if (!messages[i].content.startsWith(`${names.charName}: `) && !names.startsFromGroupName(messages[i].content)) { + messages[i].content = `${names.charName}: ${messages[i].content}`; } } systemPrompt.push({ type: 'text', text: messages[i].content }); @@ -151,11 +175,15 @@ export function convertClaudeMessages(messages, prefillString, useSysPrompt, use } if (message.role === 'system') { - if (userName && message.name === 'example_user') { - message.content = `${userName}: ${message.content}`; + if (names.userName && message.name === 'example_user') { + if (!message.content.startsWith(`${names.userName}: `)) { + message.content = `${names.userName}: ${message.content}`; + } } - if (charName && message.name === 'example_assistant') { - message.content = `${charName}: ${message.content}`; + if (names.charName && message.name === 'example_assistant') { + if (!message.content.startsWith(`${names.charName}: `) && !names.startsFromGroupName(message.content)) { + message.content = `${names.charName}: ${message.content}`; + } } message.role = 'user'; @@ -274,11 +302,10 @@ export function convertClaudeMessages(messages, prefillString, useSysPrompt, use /** * Convert a prompt from the ChatML objects to the format used by Cohere. * @param {object[]} messages Array of messages - * @param {string} charName Character name - * @param {string} userName User name + * @param {PromptNames} names Prompt names * @returns {{chatHistory: object[]}} Prompt for Cohere */ -export function convertCohereMessages(messages, charName = '', userName = '') { +export function convertCohereMessages(messages, names) { if (messages.length === 0) { messages.unshift({ role: 'user', @@ -299,13 +326,13 @@ export function convertCohereMessages(messages, charName = '', userName = '') { // No names support (who would've thought) if (msg.name) { if (msg.role == 'system' && msg.name == 'example_assistant') { - if (charName && !msg.content.startsWith(`${charName}: `)) { - msg.content = `${charName}: ${msg.content}`; + if (names.charName && !msg.content.startsWith(`${names.charName}: `) && !names.startsFromGroupName(msg.content)) { + msg.content = `${names.charName}: ${msg.content}`; } } if (msg.role == 'system' && msg.name == 'example_user') { - if (userName && !msg.content.startsWith(`${userName}: `)) { - msg.content = `${userName}: ${msg.content}`; + if (names.userName && !msg.content.startsWith(`${names.userName}: `)) { + msg.content = `${names.userName}: ${msg.content}`; } } if (msg.role !== 'system' && !msg.content.startsWith(`${msg.name}: `)) { @@ -328,12 +355,10 @@ export function convertCohereMessages(messages, charName = '', userName = '') { * @param {object[]} messages Array of messages * @param {string} model Model name * @param {boolean} useSysPrompt Use system prompt - * @param {string} charName Character name - * @param {string} userName User name + * @param {PromptNames} names Prompt names * @returns {{contents: *[], system_instruction: {parts: {text: string}}}} Prompt for Google MakerSuite models */ -export function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '', userName = '') { - +export function convertGooglePrompt(messages, model, useSysPrompt, names) { const visionSupportedModels = [ 'gemini-2.0-flash-exp', 'gemini-1.5-flash', @@ -356,20 +381,19 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN ]; const isMultimodal = visionSupportedModels.includes(model); - let hasImage = false; let sys_prompt = ''; if (useSysPrompt) { while (messages.length > 1 && messages[0].role === 'system') { // Append example names if not already done by the frontend (e.g. for group chats). - if (userName && messages[0].name === 'example_user') { - if (!messages[0].content.startsWith(`${userName}: `)) { - messages[0].content = `${userName}: ${messages[0].content}`; + if (names.userName && messages[0].name === 'example_user') { + if (!messages[0].content.startsWith(`${names.userName}: `)) { + messages[0].content = `${names.userName}: ${messages[0].content}`; } } - if (charName && messages[0].name === 'example_assistant') { - if (!messages[0].content.startsWith(`${charName}: `)) { - messages[0].content = `${charName}: ${messages[0].content}`; + if (names.charName && messages[0].name === 'example_assistant') { + if (!messages[0].content.startsWith(`${names.charName}: `) && !names.startsFromGroupName(messages[0].content)) { + messages[0].content = `${names.charName}: ${messages[0].content}`; } } sys_prompt += `${messages[0].content}\n\n`; @@ -388,53 +412,62 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN message.role = 'model'; } + // Convert the content to an array of parts + if (!Array.isArray(message.content)) { + message.content = [{ type: 'text', text: String(message.content ?? '') }]; + } + // similar story as claude if (message.name) { - if (userName && message.name === 'example_user') { - message.name = userName; - } - if (charName && message.name === 'example_assistant') { - message.name = charName; - } - - if (Array.isArray(message.content)) { - if (!message.content[0].text.startsWith(`${message.name}: `)) { - message.content[0].text = `${message.name}: ${message.content[0].text}`; + message.content.forEach((part) => { + if (part.type !== 'text') { + return; } - } else { - if (!message.content.startsWith(`${message.name}: `)) { - message.content = `${message.name}: ${message.content}`; + if (message.name === 'example_user') { + if (!part.text.startsWith(`${names.userName}: `)) { + part.text = `${names.userName}: ${part.text}`; + } + } else if (message.name === 'example_assistant') { + if (!part.text.startsWith(`${names.charName}: `) && !names.startsFromGroupName(part.text)) { + part.text = `${names.charName}: ${part.text}`; + } + } else { + if (!part.text.startsWith(`${message.name}: `)) { + part.text = `${message.name}: ${part.text}`; + } } - } + }); 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) { - const mimeType = part.image_url.url.split(';')[0].split(':')[1]; - const base64Data = part.image_url.url.split(',')[1]; - parts.push({ - inlineData: { - mimeType: mimeType, - data: base64Data, - }, - }); - hasImage = true; - } - }); - } + message.content.forEach((part) => { + if (part.type === 'text') { + parts.push({ text: part.text }); + } else if (part.type === 'image_url' && isMultimodal) { + const mimeType = part.image_url.url.split(';')[0].split(':')[1]; + const base64Data = part.image_url.url.split(',')[1]; + parts.push({ + inlineData: { + mimeType: mimeType, + data: base64Data, + }, + }); + } + }); // 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; + parts.forEach((part) => { + if (part.text) { + contents[contents.length - 1].parts[0].text += '\n\n' + part.text; + } + if (part.inlineData) { + contents[contents.length - 1].parts.push(part); + } + }); } else { contents.push({ role: message.role, @@ -449,10 +482,10 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN /** * Convert AI21 prompt. Classic: system message squash, user/assistant message merge. * @param {object[]} messages Array of messages - * @param {string} charName Character name - * @param {string} userName User name + * @param {PromptNames} names Prompt names + * @returns {object[]} Prompt for AI21 */ -export function convertAI21Messages(messages, charName = '', userName = '') { +export function convertAI21Messages(messages, names) { if (!Array.isArray(messages)) { return []; } @@ -465,14 +498,14 @@ export function convertAI21Messages(messages, charName = '', userName = '') { break; } // Append example names if not already done by the frontend (e.g. for group chats). - if (userName && messages[i].name === 'example_user') { - if (!messages[i].content.startsWith(`${userName}: `)) { - messages[i].content = `${userName}: ${messages[i].content}`; + if (names.userName && messages[i].name === 'example_user') { + if (!messages[i].content.startsWith(`${names.userName}: `)) { + messages[i].content = `${names.userName}: ${messages[i].content}`; } } - if (charName && messages[i].name === 'example_assistant') { - if (!messages[i].content.startsWith(`${charName}: `)) { - messages[i].content = `${charName}: ${messages[i].content}`; + if (names.charName && messages[i].name === 'example_assistant') { + if (!messages[i].content.startsWith(`${names.charName}: `) && !names.startsFromGroupName(messages[i].content)) { + messages[i].content = `${names.charName}: ${messages[i].content}`; } } systemPrompt += `${messages[i].content}\n\n`; @@ -521,10 +554,10 @@ export function convertAI21Messages(messages, charName = '', userName = '') { /** * Convert a prompt from the ChatML objects to the format used by MistralAI. * @param {object[]} messages Array of messages - * @param {string} charName Character name - * @param {string} userName User name + * @param {PromptNames} names Prompt names + * @returns {object[]} Prompt for MistralAI */ -export function convertMistralMessages(messages, charName = '', userName = '') { +export function convertMistralMessages(messages, names) { if (!Array.isArray(messages)) { return []; } @@ -549,15 +582,15 @@ export function convertMistralMessages(messages, charName = '', userName = '') { msg.tool_call_id = sanitizeToolId(msg.tool_call_id); } if (msg.role === 'system' && msg.name === 'example_assistant') { - if (charName && !msg.content.startsWith(`${charName}: `)) { - msg.content = `${charName}: ${msg.content}`; + if (names.charName && !msg.content.startsWith(`${names.charName}: `) && !names.startsFromGroupName(msg.content)) { + msg.content = `${names.charName}: ${msg.content}`; } delete msg.name; } if (msg.role === 'system' && msg.name === 'example_user') { - if (userName && !msg.content.startsWith(`${userName}: `)) { - msg.content = `${userName}: ${msg.content}`; + if (names.userName && !msg.content.startsWith(`${names.userName}: `)) { + msg.content = `${names.userName}: ${msg.content}`; } delete msg.name; } @@ -603,12 +636,11 @@ export function convertMistralMessages(messages, charName = '', userName = '') { /** * Merge messages with the same consecutive role, removing names if they exist. * @param {any[]} messages Messages to merge - * @param {string} charName Character name - * @param {string} userName User name + * @param {PromptNames} names Prompt names * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message * @returns {any[]} Merged messages */ -export function mergeMessages(messages, charName, userName, strict) { +export function mergeMessages(messages, names, strict) { let mergedMessages = []; /** @type {Map} */ @@ -636,13 +668,13 @@ export function mergeMessages(messages, charName, userName, strict) { message.content = text; } if (message.role === 'system' && message.name === 'example_assistant') { - if (charName && !message.content.startsWith(`${charName}: `)) { - message.content = `${charName}: ${message.content}`; + if (names.charName && !message.content.startsWith(`${names.charName}: `) && !names.startsFromGroupName(message.content)) { + message.content = `${names.charName}: ${message.content}`; } } if (message.role === 'system' && message.name === 'example_user') { - if (userName && !message.content.startsWith(`${userName}: `)) { - message.content = `${userName}: ${message.content}`; + if (names.userName && !message.content.startsWith(`${names.userName}: `)) { + message.content = `${names.userName}: ${message.content}`; } } if (message.name && message.role !== 'system') { @@ -716,7 +748,7 @@ export function mergeMessages(messages, charName, userName, strict) { mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER }); } } - return mergeMessages(mergedMessages, charName, userName, false); + return mergeMessages(mergedMessages, names, false); } return mergedMessages; From f1bc217e79f7854c094090cbb378cd633d9cf218 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:14:50 +0200 Subject: [PATCH 090/222] Expressions: Add WebLLM extension classification --- .../scripts/extensions/expressions/index.js | 70 ++++++++++++++----- .../extensions/expressions/settings.html | 3 +- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index fdc146c1a..e31807893 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -15,6 +15,7 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js'; +import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; @@ -59,6 +60,7 @@ const EXPRESSION_API = { local: 0, extras: 1, llm: 2, + webllm: 3, }; let expressionsList = null; @@ -698,8 +700,8 @@ async function moduleWorker() { } // If using LLM api then check if streamingProcessor is finished to avoid sending multiple requests to the API - if (extension_settings.expressions.api === EXPRESSION_API.llm && context.streamingProcessor && !context.streamingProcessor.isFinished) { - return; + if (extension_settings.expressions.api === EXPRESSION_API.llm && context.streamingProcessor && !context.streamingProcessor.isFinished) { + return; } // API is busy @@ -852,7 +854,7 @@ function setTalkingHeadState(newState) { extension_settings.expressions.talkinghead = newState; // Store setting saveSettingsDebounced(); - if (extension_settings.expressions.api == EXPRESSION_API.local || extension_settings.expressions.api == EXPRESSION_API.llm) { + if ([EXPRESSION_API.local, EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)) { return; } @@ -1057,11 +1059,39 @@ function parseLlmResponse(emotionResponse, labels) { console.debug(`fuzzy search found: ${result[0].item} as closest for the LLM response:`, emotionResponse); return result[0].item; } + const lowerCaseResponse = String(emotionResponse || '').toLowerCase(); + for (const label of labels) { + if (lowerCaseResponse.includes(label.toLowerCase())) { + console.debug(`Found label ${label} in the LLM response:`, emotionResponse); + return label; + } + } } throw new Error('Could not parse emotion response ' + emotionResponse); } +/** + * Gets the JSON schema for the LLM API. + * @param {string[]} emotions A list of emotions to search for. + * @returns {object} The JSON schema for the LLM API. + */ +function getJsonSchema(emotions) { + return { + $schema: 'http://json-schema.org/draft-04/schema#', + type: 'object', + properties: { + emotion: { + type: 'string', + enum: emotions, + }, + }, + required: [ + 'emotion', + ], + }; +} + function onTextGenSettingsReady(args) { // Only call if inside an API call if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) { @@ -1071,19 +1101,7 @@ function onTextGenSettingsReady(args) { stop: [], stopping_strings: [], custom_token_bans: [], - json_schema: { - $schema: 'http://json-schema.org/draft-04/schema#', - type: 'object', - properties: { - emotion: { - type: 'string', - enum: emotions, - }, - }, - required: [ - 'emotion', - ], - }, + json_schema: getJsonSchema(emotions), }); } } @@ -1139,6 +1157,22 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin const emotionResponse = await generateRaw(text, main_api, false, false, prompt); return parseLlmResponse(emotionResponse, expressionsList); } + // Using WebLLM + case EXPRESSION_API.webllm: { + if (!isWebLlmSupported()) { + console.warn('WebLLM is not supported. Using fallback expression'); + return getFallbackExpression(); + } + + const expressionsList = await getExpressionsList(); + const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList); + const messages = [ + { role: 'user', content: text + '\n\n' + prompt }, + ]; + + const emotionResponse = await generateWebLlmChatPrompt(messages); + return parseLlmResponse(emotionResponse, expressionsList); + } // Extras default: { const url = new URL(getApiUrl()); @@ -1603,7 +1637,7 @@ function onExpressionApiChanged() { const tempApi = this.value; if (tempApi) { extension_settings.expressions.api = Number(tempApi); - $('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm); + $('.expression_llm_prompt_block').toggle([EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)); expressionsList = null; spriteCache = {}; moduleWorker(); @@ -1940,7 +1974,7 @@ function migrateSettings() { await renderAdditionalExpressionSettings(); $('#expression_api').val(extension_settings.expressions.api ?? EXPRESSION_API.extras); - $('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm); + $('.expression_llm_prompt_block').toggle([EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)); $('#expression_llm_prompt').val(extension_settings.expressions.llmPrompt ?? ''); $('#expression_llm_prompt').on('input', function () { extension_settings.expressions.llmPrompt = $(this).val(); diff --git a/public/scripts/extensions/expressions/settings.html b/public/scripts/extensions/expressions/settings.html index f2b7b79ac..dc22debbd 100644 --- a/public/scripts/extensions/expressions/settings.html +++ b/public/scripts/extensions/expressions/settings.html @@ -24,7 +24,8 @@
From d27d750cb2888c6c8a7980f00cb08e4c55901041 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 02:50:42 +0200 Subject: [PATCH 091/222] Use CSS resizing for send textarea --- public/scripts/RossAscends-mods.js | 44 +++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 750e9cdd2..213f9f07e 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -887,14 +887,44 @@ export function initRossMods() { saveSettingsDebounced(); }); + const cssAutofit = CSS.supports('field-sizing', 'content'); + + if (cssAutofit) { + sendTextArea.style['fieldSizing'] = 'content'; + sendTextArea.style['height'] = 'auto'; + + let lastHeight = chatBlock.offsetHeight; + const chatBlockResizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target !== chatBlock) { + continue; + } + + const threshold = 1; + const newHeight = chatBlock.offsetHeight; + const deltaHeight = newHeight - lastHeight; + const isScrollAtBottom = Math.abs(chatBlock.scrollHeight - chatBlock.scrollTop - newHeight) <= threshold; + + if (!isScrollAtBottom && Math.abs(deltaHeight) > threshold) { + chatBlock.scrollTop -= deltaHeight; + } + lastHeight = newHeight; + } + }); + + chatBlockResizeObserver.observe(chatBlock); + } + sendTextArea.addEventListener('input', () => { - const hasContent = sendTextArea.value !== ''; - const fitsCurrentSize = sendTextArea.scrollHeight <= sendTextArea.offsetHeight; - const isScrollbarShown = sendTextArea.clientWidth < sendTextArea.offsetWidth; - const isHalfScreenHeight = sendTextArea.offsetHeight >= window.innerHeight / 2; - const needsDebounce = hasContent && (fitsCurrentSize || (isScrollbarShown && isHalfScreenHeight)); - if (needsDebounce) autoFitSendTextAreaDebounced(); - else autoFitSendTextArea(); + if (!cssAutofit) { + const hasContent = sendTextArea.value !== ''; + const fitsCurrentSize = sendTextArea.scrollHeight <= sendTextArea.offsetHeight; + const isScrollbarShown = sendTextArea.clientWidth < sendTextArea.offsetWidth; + const isHalfScreenHeight = sendTextArea.offsetHeight >= window.innerHeight / 2; + const needsDebounce = hasContent && (fitsCurrentSize || (isScrollbarShown && isHalfScreenHeight)); + if (needsDebounce) autoFitSendTextAreaDebounced(); + else autoFitSendTextArea(); + } saveUserInputDebounced(); }); From f649e4698b749c62c6f3c413605fb77d874f2569 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 02:52:05 +0200 Subject: [PATCH 092/222] Invert if cssAutofit --- public/scripts/RossAscends-mods.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 213f9f07e..5334019d4 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -916,16 +916,19 @@ export function initRossMods() { } sendTextArea.addEventListener('input', () => { - if (!cssAutofit) { - const hasContent = sendTextArea.value !== ''; - const fitsCurrentSize = sendTextArea.scrollHeight <= sendTextArea.offsetHeight; - const isScrollbarShown = sendTextArea.clientWidth < sendTextArea.offsetWidth; - const isHalfScreenHeight = sendTextArea.offsetHeight >= window.innerHeight / 2; - const needsDebounce = hasContent && (fitsCurrentSize || (isScrollbarShown && isHalfScreenHeight)); - if (needsDebounce) autoFitSendTextAreaDebounced(); - else autoFitSendTextArea(); - } saveUserInputDebounced(); + + if (cssAutofit) { + return; + } + + const hasContent = sendTextArea.value !== ''; + const fitsCurrentSize = sendTextArea.scrollHeight <= sendTextArea.offsetHeight; + const isScrollbarShown = sendTextArea.clientWidth < sendTextArea.offsetWidth; + const isHalfScreenHeight = sendTextArea.offsetHeight >= window.innerHeight / 2; + const needsDebounce = hasContent && (fitsCurrentSize || (isScrollbarShown && isHalfScreenHeight)); + if (needsDebounce) autoFitSendTextAreaDebounced(); + else autoFitSendTextArea(); }); restoreUserInput(); From 222edd5c3696cc36500b6d68ce998990b46ad27f Mon Sep 17 00:00:00 2001 From: cloak1505 Date: Sat, 21 Dec 2024 01:30:35 -0600 Subject: [PATCH 093/222] OpenRouter: Update providers list --- public/scripts/textgen-models.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index 6323505f5..851c3940d 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -25,6 +25,7 @@ const OPENROUTER_PROVIDERS = [ 'Anthropic', 'Google', 'Google AI Studio', + 'Amazon Bedrock', 'Groq', 'SambaNova', 'Cohere', @@ -50,6 +51,8 @@ const OPENROUTER_PROVIDERS = [ 'Featherless', 'Inflection', 'xAI', + 'Cloudflare', + 'SF Compute', '01.AI', 'HuggingFace', 'Mancer', From 2fbe689605beddaa84cfc3b867c4e324901f365e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:47:25 +0200 Subject: [PATCH 094/222] Implement autofit for edit textarea --- public/script.js | 54 ++++++++++++++---------------- public/scripts/RossAscends-mods.js | 5 ++- public/style.css | 2 ++ 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/public/script.js b/public/script.js index 04eb8bc09..3fcfbc55c 100644 --- a/public/script.js +++ b/public/script.js @@ -849,11 +849,9 @@ export let is_send_press = false; //Send generation let this_del_mes = -1; -//message editing and chat scroll position persistence +//message editing var this_edit_mes_chname = ''; var this_edit_mes_id; -var scroll_holder = 0; -var is_use_scroll_holder = false; //settings export let settings; @@ -9727,25 +9725,7 @@ jQuery(async function () { chooseBogusFolder($(this), tagId); }); - /** - * Sets the scroll height of the edit textarea to fit the content. - * @param {HTMLTextAreaElement} e Textarea element to auto-fit - */ - function autoFitEditTextArea(e) { - scroll_holder = chatElement[0].scrollTop; - e.style.height = '0px'; - const newHeight = e.scrollHeight + 4; - e.style.height = `${newHeight}px`; - is_use_scroll_holder = true; - } - const autoFitEditTextAreaDebounced = debounce(autoFitEditTextArea, debounce_timeout.short); - document.addEventListener('input', e => { - if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { - const scrollbarShown = e.target.clientWidth < e.target.offsetWidth && e.target.offsetHeight >= window.innerHeight * 0.75; - const immediately = (e.target.scrollHeight > e.target.offsetHeight && !scrollbarShown) || e.target.value === ''; - immediately ? autoFitEditTextArea(e.target) : autoFitEditTextAreaDebounced(e.target); - } - }); + const cssAutofit = CSS.supports('field-sizing', 'content'); const chatElementScroll = document.getElementById('chat'); const chatScrollHandler = function () { if (power_user.waifuMode) { @@ -9767,12 +9747,26 @@ jQuery(async function () { }; chatElementScroll.addEventListener('wheel', chatScrollHandler, { passive: true }); chatElementScroll.addEventListener('touchmove', chatScrollHandler, { passive: true }); - chatElementScroll.addEventListener('scroll', function () { - if (is_use_scroll_holder) { - this.scrollTop = scroll_holder; - is_use_scroll_holder = false; + + if (!cssAutofit) { + /** + * Sets the scroll height of the edit textarea to fit the content. + * @param {HTMLTextAreaElement} e Textarea element to auto-fit + */ + function autoFitEditTextArea(e) { + e.style.height = '0px'; + const newHeight = e.scrollHeight + 4; + e.style.height = `${newHeight}px`; } - }, { passive: true }); + const autoFitEditTextAreaDebounced = debounce(autoFitEditTextArea, debounce_timeout.short); + document.addEventListener('input', e => { + if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { + const scrollbarShown = e.target.clientWidth < e.target.offsetWidth && e.target.offsetHeight >= window.innerHeight * 0.75; + const immediately = (e.target.scrollHeight > e.target.offsetHeight && !scrollbarShown) || e.target.value === ''; + immediately ? autoFitEditTextArea(e.target) : autoFitEditTextAreaDebounced(e.target); + } + }); + } $(document).on('click', '.mes', function () { //when a 'delete message' parent div is clicked @@ -10517,8 +10511,10 @@ jQuery(async function () { let edit_textarea = $(this) .closest('.mes_block') .find('.edit_textarea'); - edit_textarea.height(0); - edit_textarea.height(edit_textarea[0].scrollHeight); + if (!cssAutofit) { + edit_textarea.height(0); + edit_textarea.height(edit_textarea[0].scrollHeight); + } edit_textarea.focus(); edit_textarea[0].setSelectionRange( //this sets the cursor at the end of the text String(edit_textarea.val()).length, diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 5334019d4..acef7b5f6 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -890,9 +890,6 @@ export function initRossMods() { const cssAutofit = CSS.supports('field-sizing', 'content'); if (cssAutofit) { - sendTextArea.style['fieldSizing'] = 'content'; - sendTextArea.style['height'] = 'auto'; - let lastHeight = chatBlock.offsetHeight; const chatBlockResizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { @@ -919,6 +916,8 @@ export function initRossMods() { saveUserInputDebounced(); if (cssAutofit) { + // Unset modifications made with a manual resize + sendTextArea.style.height = 'auto'; return; } diff --git a/public/style.css b/public/style.css index c9033bcad..d47c8aaf6 100644 --- a/public/style.css +++ b/public/style.css @@ -1264,6 +1264,7 @@ button { text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor); flex: 1; order: 3; + field-sizing: content; --progColor: rgb(146, 190, 252); --progFlashColor: rgb(215, 136, 114); @@ -4111,6 +4112,7 @@ input[type="range"]::-webkit-slider-thumb { line-height: calc(var(--mainFontSize) + .25rem); max-height: 75vh; max-height: 75dvh; + field-sizing: content; } #anchor_order { From 85ce522270bd67b3d72de1c92e768bd30120e388 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:24:54 +0200 Subject: [PATCH 095/222] Remove invalid style --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 3fcfbc55c..44e946693 100644 --- a/public/script.js +++ b/public/script.js @@ -10505,7 +10505,7 @@ jQuery(async function () { .closest('.mes_block') .find('.mes_text') .append( - '', + '', ); $('#curEditTextarea').val(text); let edit_textarea = $(this) From 252043ae11f51c637ebe1c5d3a58aae4352e3b2a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:33:18 +0200 Subject: [PATCH 096/222] Move code for cleaner diff --- public/script.js | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/public/script.js b/public/script.js index 44e946693..0d7d47e24 100644 --- a/public/script.js +++ b/public/script.js @@ -9726,6 +9726,26 @@ jQuery(async function () { }); const cssAutofit = CSS.supports('field-sizing', 'content'); + if (!cssAutofit) { + /** + * Sets the scroll height of the edit textarea to fit the content. + * @param {HTMLTextAreaElement} e Textarea element to auto-fit + */ + function autoFitEditTextArea(e) { + e.style.height = '0px'; + const newHeight = e.scrollHeight + 4; + e.style.height = `${newHeight}px`; + } + const autoFitEditTextAreaDebounced = debounce(autoFitEditTextArea, debounce_timeout.short); + document.addEventListener('input', e => { + if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { + const scrollbarShown = e.target.clientWidth < e.target.offsetWidth && e.target.offsetHeight >= window.innerHeight * 0.75; + const immediately = (e.target.scrollHeight > e.target.offsetHeight && !scrollbarShown) || e.target.value === ''; + immediately ? autoFitEditTextArea(e.target) : autoFitEditTextAreaDebounced(e.target); + } + }); + } + const chatElementScroll = document.getElementById('chat'); const chatScrollHandler = function () { if (power_user.waifuMode) { @@ -9748,26 +9768,6 @@ jQuery(async function () { chatElementScroll.addEventListener('wheel', chatScrollHandler, { passive: true }); chatElementScroll.addEventListener('touchmove', chatScrollHandler, { passive: true }); - if (!cssAutofit) { - /** - * Sets the scroll height of the edit textarea to fit the content. - * @param {HTMLTextAreaElement} e Textarea element to auto-fit - */ - function autoFitEditTextArea(e) { - e.style.height = '0px'; - const newHeight = e.scrollHeight + 4; - e.style.height = `${newHeight}px`; - } - const autoFitEditTextAreaDebounced = debounce(autoFitEditTextArea, debounce_timeout.short); - document.addEventListener('input', e => { - if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { - const scrollbarShown = e.target.clientWidth < e.target.offsetWidth && e.target.offsetHeight >= window.innerHeight * 0.75; - const immediately = (e.target.scrollHeight > e.target.offsetHeight && !scrollbarShown) || e.target.value === ''; - immediately ? autoFitEditTextArea(e.target) : autoFitEditTextAreaDebounced(e.target); - } - }); - } - $(document).on('click', '.mes', function () { //when a 'delete message' parent div is clicked // and we are in delete mode and del_checkbox is visible From d07ee76784b74f28ec7bbe9b16ab8cc6ffbb8e7c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:48:12 +0200 Subject: [PATCH 097/222] Add overscroll contain for edit textarea --- public/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/public/style.css b/public/style.css index d47c8aaf6..725b33514 100644 --- a/public/style.css +++ b/public/style.css @@ -4113,6 +4113,7 @@ input[type="range"]::-webkit-slider-thumb { max-height: 75vh; max-height: 75dvh; field-sizing: content; + overscroll-behavior-y: contain; } #anchor_order { From 21ee072677ae89ef55a28472ffeacd3af8a8b9c0 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:50:37 +0200 Subject: [PATCH 098/222] Revert "Add overscroll contain for edit textarea" This reverts commit d07ee76784b74f28ec7bbe9b16ab8cc6ffbb8e7c. --- public/style.css | 1 - 1 file changed, 1 deletion(-) diff --git a/public/style.css b/public/style.css index 725b33514..d47c8aaf6 100644 --- a/public/style.css +++ b/public/style.css @@ -4113,7 +4113,6 @@ input[type="range"]::-webkit-slider-thumb { max-height: 75vh; max-height: 75dvh; field-sizing: content; - overscroll-behavior-y: contain; } #anchor_order { From ba7e34c195919c508b06cb76f452a73f0b5ac84e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 21 Dec 2024 18:47:46 +0200 Subject: [PATCH 099/222] Image Generation: Use wrench symbol for function tool --- public/scripts/extensions/stable-diffusion/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 7f73ef59b..5736a23f6 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -480,7 +480,7 @@
`, })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'is-mobile', + callback: () => String(isMobile()), + returns: ARGUMENT_TYPE.BOOLEAN, + helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to {{isMobile}} macro.', + })); registerVariableCommands(); } diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 84a163c4e..7490596d3 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -48,6 +48,7 @@
  • {{random::(arg1)::(arg2)}}alternative syntax for random that allows to use commas in the list items.
  • {{pick::(args)}}picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won't be re-rolled on consecutive messages and prompt processing.
  • {{banned "text here"}}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.
  • +
  • {{isMobile}}"true" if currently running in a mobile environment, "false" otherwise
  • Instruct Mode and Context Template Macros: From 713443d234c473e2b02a6f28a8934dd01c01f9eb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:52:09 +0200 Subject: [PATCH 104/222] Fix ComfyUI generation for non-relative paths --- package-lock.json | 10 +++++++ package.json | 1 + .../extensions/stable-diffusion/index.js | 2 +- src/endpoints/stable-diffusion.js | 28 +++++++------------ 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa23bf5a6..63f034003 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,7 @@ "simple-git": "^3.19.1", "slidetoggle": "^4.0.0", "tiktoken": "^1.0.16", + "url-join": "^5.0.0", "vectra": "^0.2.2", "wavefile": "^11.0.0", "webpack": "^5.95.0", @@ -7053,6 +7054,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", diff --git a/package.json b/package.json index 26d979a7f..c77f893a6 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "simple-git": "^3.19.1", "slidetoggle": "^4.0.0", "tiktoken": "^1.0.16", + "url-join": "^5.0.0", "vectra": "^0.2.2", "wavefile": "^11.0.0", "webpack": "^5.95.0", diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 39b1ab610..32f34ff40 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -1112,7 +1112,7 @@ function onHrSecondPassStepsInput() { function onComfyUrlInput() { // Remove trailing slashes - extension_settings.sd.comfy_url = String($('#sd_comfy_url').val() ?? '').replace(/\/$/, ''); + extension_settings.sd.comfy_url = String($('#sd_comfy_url').val()); saveSettingsDebounced(); } diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js index 88034e313..e8340c564 100644 --- a/src/endpoints/stable-diffusion.js +++ b/src/endpoints/stable-diffusion.js @@ -6,6 +6,7 @@ import fetch from 'node-fetch'; import sanitize from 'sanitize-filename'; import { sync as writeFileAtomicSync } from 'write-file-atomic'; import FormData from 'form-data'; +import urlJoin from 'url-join'; import { delay, getBasicAuthHeader, tryParse } from '../util.js'; import { jsonParser } from '../express-common.js'; @@ -364,8 +365,7 @@ const comfy = express.Router(); comfy.post('/ping', jsonParser, async (request, response) => { try { - const url = new URL(request.body.url); - url.pathname += '/system_stats'; + const url = new URL(urlJoin(request.body.url, '/system_stats')); const result = await fetch(url); if (!result.ok) { @@ -381,8 +381,7 @@ comfy.post('/ping', jsonParser, async (request, response) => { comfy.post('/samplers', jsonParser, async (request, response) => { try { - const url = new URL(request.body.url); - url.pathname += '/object_info'; + const url = new URL(urlJoin(request.body.url, '/object_info')); const result = await fetch(url); if (!result.ok) { @@ -400,8 +399,7 @@ comfy.post('/samplers', jsonParser, async (request, response) => { comfy.post('/models', jsonParser, async (request, response) => { try { - const url = new URL(request.body.url); - url.pathname += '/object_info'; + const url = new URL(urlJoin(request.body.url, '/object_info')); const result = await fetch(url); if (!result.ok) { @@ -429,8 +427,7 @@ comfy.post('/models', jsonParser, async (request, response) => { comfy.post('/schedulers', jsonParser, async (request, response) => { try { - const url = new URL(request.body.url); - url.pathname += '/object_info'; + const url = new URL(urlJoin(request.body.url, '/object_info')); const result = await fetch(url); if (!result.ok) { @@ -448,8 +445,7 @@ comfy.post('/schedulers', jsonParser, async (request, response) => { comfy.post('/vaes', jsonParser, async (request, response) => { try { - const url = new URL(request.body.url); - url.pathname += '/object_info'; + const url = new URL(urlJoin(request.body.url, '/object_info')); const result = await fetch(url); if (!result.ok) { @@ -516,15 +512,13 @@ comfy.post('/delete-workflow', jsonParser, async (request, response) => { comfy.post('/generate', jsonParser, async (request, response) => { try { - const url = new URL(request.body.url); - url.pathname += '/prompt'; + const url = new URL(urlJoin(request.body.url, '/prompt')); const controller = new AbortController(); request.socket.removeAllListeners('close'); request.socket.on('close', function () { if (!response.writableEnded && !item) { - const interruptUrl = new URL(request.body.url); - interruptUrl.pathname += '/interrupt'; + const interruptUrl = new URL(urlJoin(request.body.url, '/interrupt')); fetch(interruptUrl, { method: 'POST', headers: { 'Authorization': getBasicAuthHeader(request.body.auth) } }); } controller.abort(); @@ -543,8 +537,7 @@ comfy.post('/generate', jsonParser, async (request, response) => { const data = await promptResult.json(); const id = data.prompt_id; let item; - const historyUrl = new URL(request.body.url); - historyUrl.pathname += '/history'; + const historyUrl = new URL(urlJoin(request.body.url, '/history')); while (true) { const result = await fetch(historyUrl); if (!result.ok) { @@ -568,8 +561,7 @@ comfy.post('/generate', jsonParser, async (request, response) => { throw new Error(`ComfyUI generation did not succeed.\n\n${errorMessages}`.trim()); } const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0]; - const imgUrl = new URL(request.body.url); - imgUrl.pathname += '/view'; + const imgUrl = new URL(urlJoin(request.body.url, '/view')); imgUrl.search = `?filename=${imgInfo.filename}&subfolder=${imgInfo.subfolder}&type=${imgInfo.type}`; const imgResponse = await fetch(imgUrl); if (!imgResponse.ok) { From 78a1397a3c5ec8c15169077bd69002c301fc5246 Mon Sep 17 00:00:00 2001 From: ceruleandeep Date: Sun, 22 Dec 2024 11:07:00 +1100 Subject: [PATCH 105/222] fix: log arguments when eventTracing is set --- public/lib/eventemitter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/lib/eventemitter.js b/public/lib/eventemitter.js index 51ac4eca3..abf2b8dbc 100644 --- a/public/lib/eventemitter.js +++ b/public/lib/eventemitter.js @@ -95,13 +95,14 @@ EventEmitter.prototype.removeListener = function (event, listener) { }; EventEmitter.prototype.emit = async function (event) { + let args = [].slice.call(arguments, 1); if (localStorage.getItem('eventTracing') === 'true') { console.trace('Event emitted: ' + event, args); } else { console.debug('Event emitted: ' + event); } - var i, listeners, length, args = [].slice.call(arguments, 1); + let i, listeners, length; if (typeof this.events[event] === 'object') { listeners = this.events[event].slice(); @@ -120,13 +121,14 @@ EventEmitter.prototype.emit = async function (event) { }; EventEmitter.prototype.emitAndWait = function (event) { + let args = [].slice.call(arguments, 1); if (localStorage.getItem('eventTracing') === 'true') { console.trace('Event emitted: ' + event, args); } else { console.debug('Event emitted: ' + event); } - var i, listeners, length, args = [].slice.call(arguments, 1); + let i, listeners, length; if (typeof this.events[event] === 'object') { listeners = this.events[event].slice(); From 7bb37f129d1d5af38ec805624ace9f18bf16a654 Mon Sep 17 00:00:00 2001 From: ceruleandeep Date: Sun, 22 Dec 2024 08:43:35 +1100 Subject: [PATCH 106/222] fix: move cache-busting to server side --- public/scripts/extensions/expressions/index.js | 2 -- src/endpoints/sprites.js | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index e31807893..21e0d6df2 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1283,8 +1283,6 @@ async function drawSpritesList(character, labels, sprites) { * @returns {Promise} Rendered list item template */ async function getListItem(item, imageSrc, textClass, isCustom) { - const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; - imageSrc = isFirefox ? `${imageSrc}?t=${Date.now()}` : imageSrc; return renderExtensionTemplateAsync(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom }); } diff --git a/src/endpoints/sprites.js b/src/endpoints/sprites.js index 733b12478..6a2e5e6b9 100644 --- a/src/endpoints/sprites.js +++ b/src/endpoints/sprites.js @@ -124,9 +124,10 @@ router.get('/get', jsonParser, function (request, response) { }) .map((file) => { const pathToSprite = path.join(spritesPath, file); + const mtime = fs.statSync(pathToSprite).mtime?.toISOString().replace(/[^0-9]/g, '').slice(0, 14); return { label: path.parse(pathToSprite).name.toLowerCase(), - path: `/characters/${name}/${file}`, + path: `/characters/${name}/${file}` + (mtime ? `?t=${mtime}` : ''), }; }); } From c3a12cc1a27ba3026df361409d0c00daa569af16 Mon Sep 17 00:00:00 2001 From: ceruleandeep Date: Sun, 22 Dec 2024 09:54:56 +1100 Subject: [PATCH 107/222] feat: /uploadsprite slash command --- .../scripts/extensions/expressions/index.js | 98 ++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 21e0d6df2..511a0b6f8 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -14,7 +14,6 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; -import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js'; import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; export { MODULE_NAME }; @@ -986,6 +985,71 @@ async function setSpriteSlashCommand(_, spriteId) { return label; } +/** + * Returns the sprite folder name (including override) for a character. + * @param {object} char Character object + * @param {string} char.avatar Avatar filename with extension + * @returns {string} Sprite folder name + * @throws {Error} If character not found or avatar not set + */ +function spriteFolderNameFromCharacter(char) { + const avatarFileName = char.avatar.replace(/\.[^/.]+$/, ''); + const expressionOverride = extension_settings.expressionOverrides.find(e => e.name === avatarFileName); + return expressionOverride?.path ? expressionOverride.path : avatarFileName; +} + +/** + * Slash command callback for /uploadsprite + * + * label= is required + * if name= is provided, it will be used as a findChar lookup + * if name= is not provided, the last character's name will be used + * if folder= is a full path, it will be used as the folder + * if folder= is a partial path, it will be appended to the character's name + * if folder= is not provided, the character's override folder will be used, if set + * + * @param {object} args + * @param {string} args.name Character name or avatar key, passed through findChar + * @param {string} args.label Expression label + * @param {string} args.folder Sprite folder path, processed using backslash rules + * @param {string} imageUrl Image URI to fetch and upload + * @returns {Promise} + */ +async function uploadSpriteCommand({ name, label, folder }, imageUrl) { + if (!imageUrl) throw new Error('Image URL is required'); + if (!label || typeof label !== 'string') throw new Error('Expression label is required'); + + label = label.replace(/[^a-z]/gi, '').toLowerCase().trim(); + if (!label) throw new Error('Expression label must contain at least one letter'); + + name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name; + const char = findChar({ name }); + + if (!folder) { + folder = spriteFolderNameFromCharacter(char); + } else if (folder.startsWith('/') || folder.startsWith('\\')) { + const subfolder = folder.slice(1); + folder = `${char.name}/${subfolder}`; + } + + try { + const response = await fetch(imageUrl); + const blob = await response.blob(); + const file = new File([blob], 'image.png', { type: 'image/png' }); + + const formData = new FormData(); + formData.append('name', folder); // this is the folder or character name + formData.append('label', label); // this is the expression label + formData.append('avatar', file); // this is the image file + + await handleFileUpload('/api/sprites/upload', formData); + console.debug(`[${MODULE_NAME}] Upload of ${imageUrl} completed for ${name} with label ${label}`); + } catch (error) { + console.error(`[${MODULE_NAME}] Error uploading file:`, error); + throw error; + } +} + /** * Processes the classification text to reduce the amount of text sent to the API. * Quotes and asterisks are to be removed. If the text is less than 300 characters, it is returned as is. @@ -2215,4 +2279,36 @@ function migrateSettings() {
    `, })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'uploadsprite', + description: 'Upload a sprite', + callback: async (args, url) => { + await uploadSpriteCommand(args, url); + }, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'URL of the image to upload', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + }), + ], + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'name', + description: 'Character name or avatar key (default is current character)', + type: ARGUMENT_TYPE.STRING, + }), + SlashCommandNamedArgument.fromProps({ + name: 'label', + description: 'Sprite label/expression name', + type: ARGUMENT_TYPE.STRING, + }), + SlashCommandNamedArgument.fromProps({ + name: 'folder', + description: 'Override folder to upload into', + type: ARGUMENT_TYPE.STRING, + }), + ], + helpString: 'Upload a sprite from a URL. Example: /uploadsprite name=Seraphina label=happy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png', + })); })(); From e6107ad447721342ef9d8a35f37a79d402e5ff7b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 22 Dec 2024 15:39:41 +0200 Subject: [PATCH 108/222] Add NAI Diffusion V4 --- .../extensions/stable-diffusion/index.js | 7 ++++- .../extensions/stable-diffusion/settings.html | 2 +- src/endpoints/novelai.js | 26 ++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 32f34ff40..ad2ad3941 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -1985,6 +1985,10 @@ async function loadNovelModels() { } return [ + { + value: 'nai-diffusion-4-curated-preview', + text: 'NAI Diffusion Anime V4 (Curated Preview)', + }, { value: 'nai-diffusion-3', text: 'NAI Diffusion Anime V3', @@ -2049,7 +2053,7 @@ async function loadSchedulers() { schedulers = await getAutoRemoteSchedulers(); break; case sources.novel: - schedulers = ['N/A']; + schedulers = ['karras', 'native', 'exponential', 'polyexponential']; break; case sources.vlad: schedulers = ['N/A']; @@ -3158,6 +3162,7 @@ async function generateNovelImage(prompt, negativePrompt, signal) { prompt: prompt, model: extension_settings.sd.model, sampler: extension_settings.sd.sampler, + scheduler: extension_settings.sd.scheduler, steps: steps, scale: extension_settings.sd.scale, width: width, diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 5736a23f6..a9e941ca0 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -274,7 +274,7 @@
    -
    +
    diff --git a/src/endpoints/novelai.js b/src/endpoints/novelai.js index 53ed8258e..9629aad40 100644 --- a/src/endpoints/novelai.js +++ b/src/endpoints/novelai.js @@ -307,15 +307,18 @@ router.post('/generate-image', jsonParser, async (request, response) => { }, body: JSON.stringify({ action: 'generate', - input: request.body.prompt, + input: request.body.prompt ?? '', model: request.body.model ?? 'nai-diffusion', parameters: { + params_version: 3, + prefer_brownian: true, negative_prompt: request.body.negative_prompt ?? '', height: request.body.height ?? 512, width: request.body.width ?? 512, scale: request.body.scale ?? 9, seed: request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 9999999999), sampler: request.body.sampler ?? 'k_dpmpp_2m', + noise_schedule: request.body.scheduler ?? 'karras', steps: request.body.steps ?? 28, n_samples: 1, // NAI handholding for prompts @@ -323,11 +326,32 @@ router.post('/generate-image', jsonParser, async (request, response) => { qualityToggle: false, add_original_image: false, controlnet_strength: 1, + deliberate_euler_ancestral_bug: false, dynamic_thresholding: request.body.decrisper ?? false, legacy: false, + legacy_v3_extend: false, sm: request.body.sm ?? false, sm_dyn: request.body.sm_dyn ?? false, uncond_scale: 1, + use_coords: false, + characterPrompts: [], + reference_image_multiple: [], + reference_information_extracted_multiple: [], + reference_strength_multiple: [], + v4_negative_prompt: { + caption: { + base_caption: request.body.negative_prompt ?? '', + char_captions: [], + }, + }, + v4_prompt: { + caption: { + base_caption: request.body.prompt ?? '', + char_captions: [], + }, + use_coords: false, + use_order: true, + }, }, }), }); From 0f93caa4271ed8107904be5f9cd10d8a37111437 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 22 Dec 2024 18:12:07 +0200 Subject: [PATCH 109/222] Fix type errors in command registration --- public/scripts/extensions/expressions/index.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 511a0b6f8..e5ba1ed21 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -2281,9 +2281,9 @@ function migrateSettings() { })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'uploadsprite', - description: 'Upload a sprite', callback: async (args, url) => { await uploadSpriteCommand(args, url); + return ''; }, unnamedArgumentList: [ SlashCommandArgument.fromProps({ @@ -2296,19 +2296,26 @@ function migrateSettings() { SlashCommandNamedArgument.fromProps({ name: 'name', description: 'Character name or avatar key (default is current character)', - type: ARGUMENT_TYPE.STRING, + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + acceptsMultiple: false, }), SlashCommandNamedArgument.fromProps({ name: 'label', description: 'Sprite label/expression name', - type: ARGUMENT_TYPE.STRING, + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: localEnumProviders.expressions, + isRequired: true, + acceptsMultiple: false, }), SlashCommandNamedArgument.fromProps({ name: 'folder', description: 'Override folder to upload into', - type: ARGUMENT_TYPE.STRING, + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + acceptsMultiple: false, }), ], - helpString: 'Upload a sprite from a URL. Example: /uploadsprite name=Seraphina label=happy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png', + helpString: '
    Upload a sprite from a URL.
    Example:
    /uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png
    ', })); })(); From 1ebaf18210216bd5a2258212811ccc0bcb2b034e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 22 Dec 2024 19:26:47 +0200 Subject: [PATCH 110/222] feat: add drag-and-drop functionality for logit bias lists --- public/index.html | 2 ++ public/scripts/logit-bias.js | 26 ++++++++++++++++++++++++-- public/scripts/openai.js | 24 +++++++++++++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 6f0d40dcc..c885111e1 100644 --- a/public/index.html +++ b/public/index.html @@ -6010,6 +6010,7 @@
    + @@ -6017,6 +6018,7 @@
    + diff --git a/public/scripts/logit-bias.js b/public/scripts/logit-bias.js index 2b9d67bdc..f1061def4 100644 --- a/public/scripts/logit-bias.js +++ b/public/scripts/logit-bias.js @@ -1,6 +1,6 @@ import { saveSettingsDebounced } from '../script.js'; import { getTextTokens } from './tokenizers.js'; -import { uuidv4 } from './utils.js'; +import { getSortableDelay, uuidv4 } from './utils.js'; export const BIAS_CACHE = new Map(); @@ -16,7 +16,8 @@ export function displayLogitBias(logitBias, containerSelector) { return; } - $(containerSelector).find('.logit_bias_list').empty(); + const list = $(containerSelector).find('.logit_bias_list'); + list.empty(); for (const entry of logitBias) { if (entry) { @@ -24,6 +25,27 @@ export function displayLogitBias(logitBias, containerSelector) { } } + // Check if a sortable instance exists + if (list.sortable('instance') !== undefined) { + // Destroy the instance + list.sortable('destroy'); + } + + // Make the list sortable + list.sortable({ + delay: getSortableDelay(), + handle: '.drag-handle', + stop: function () { + const order = []; + list.children().each(function () { + order.unshift($(this).data('id')); + }); + logitBias.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id)); + console.log('Logit bias reordered:', logitBias); + saveSettingsDebounced(); + }, + }); + BIAS_CACHE.delete(containerSelector); } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 914d6d24a..98b81d050 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -3465,7 +3465,8 @@ function onLogitBiasPresetChange() { } oai_settings.bias_preset_selected = value; - $('.openai_logit_bias_list').empty(); + const list = $('.openai_logit_bias_list'); + list.empty(); for (const entry of preset) { if (entry) { @@ -3473,6 +3474,27 @@ function onLogitBiasPresetChange() { } } + // Check if a sortable instance exists + if (list.sortable('instance') !== undefined) { + // Destroy the instance + list.sortable('destroy'); + } + + // Make the list sortable + list.sortable({ + delay: getSortableDelay(), + handle: '.drag-handle', + stop: function () { + const order = []; + list.children().each(function () { + order.unshift(parseInt($(this).data('id'))); + }); + preset.sort((a, b) => order.indexOf(preset.indexOf(a)) - order.indexOf(preset.indexOf(b))); + console.log('Logit bias reordered:', preset); + saveSettingsDebounced(); + }, + }); + biasCache = undefined; saveSettingsDebounced(); } From 964437ed13a790dca3cb1ac94037d0fe1c2e58dc Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:59:35 -0600 Subject: [PATCH 111/222] Update Llama-3-Instruct-Names.json Remove brackets from `Llama-3-Instruct-Names` --- default/content/presets/instruct/Llama-3-Instruct-Names.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default/content/presets/instruct/Llama-3-Instruct-Names.json b/default/content/presets/instruct/Llama-3-Instruct-Names.json index f0b4f1439..4412df51a 100644 --- a/default/content/presets/instruct/Llama-3-Instruct-Names.json +++ b/default/content/presets/instruct/Llama-3-Instruct-Names.json @@ -1,6 +1,6 @@ { - "input_sequence": "<|start_header_id|>[{{name}}]<|end_header_id|>\n\n", - "output_sequence": "<|start_header_id|>[{{name}}]<|end_header_id|>\n\n", + "input_sequence": "<|start_header_id|>{{name}}<|end_header_id|>\n\n", + "output_sequence": "<|start_header_id|>{{name}}<|end_header_id|>\n\n", "last_output_sequence": "", "system_sequence": "<|start_header_id|>system<|end_header_id|>\n\n", "stop_sequence": "<|eot_id|>", From 17df259afd53d25ff2a91e928d1e53a9c1cb7c99 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Sun, 22 Dec 2024 15:00:00 -0600 Subject: [PATCH 112/222] Update ChatML-Names.json Remove brackets from `ChatML-Names` --- default/content/presets/instruct/ChatML-Names.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default/content/presets/instruct/ChatML-Names.json b/default/content/presets/instruct/ChatML-Names.json index 41e14e9b5..2ad703b33 100644 --- a/default/content/presets/instruct/ChatML-Names.json +++ b/default/content/presets/instruct/ChatML-Names.json @@ -1,6 +1,6 @@ { - "input_sequence": "<|im_start|>[{{name}}]", - "output_sequence": "<|im_start|>[{{name}}]", + "input_sequence": "<|im_start|>{{name}}", + "output_sequence": "<|im_start|>{{name}}", "last_output_sequence": "", "system_sequence": "<|im_start|>system", "stop_sequence": "<|im_end|>", From 3f7b91a4ebaf3cb1c85755bbc92591f05e5a6096 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:27:20 +0200 Subject: [PATCH 113/222] Add uuid to CC logit bias entries --- default/content/settings.json | 4 ++++ public/scripts/openai.js | 40 ++++++++++++++++++++++++----------- public/style.css | 2 +- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/default/content/settings.json b/default/content/settings.json index 815abcfde..729215d3c 100644 --- a/default/content/settings.json +++ b/default/content/settings.json @@ -599,18 +599,22 @@ "Default (none)": [], "Anti-bond": [ { + "id": "22154f79-dd98-41bc-8e34-87015d6a0eaf", "text": " bond", "value": -50 }, { + "id": "8ad2d5c4-d8ef-49e4-bc5e-13e7f4690e0f", "text": " future", "value": -50 }, { + "id": "52a4b280-0956-4940-ac52-4111f83e4046", "text": " bonding", "value": -50 }, { + "id": "e63037c7-c9d1-4724-ab2d-7756008b433b", "text": " connection", "value": -25 } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 98b81d050..c9fb72afb 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -60,6 +60,7 @@ import { parseJsonFile, resetScrollHeight, stringFormat, + uuidv4, } from './utils.js'; import { countTokensOpenAIAsync, getTokenizerModel } from './tokenizers.js'; import { isMobile } from './RossAscends-mods.js'; @@ -107,10 +108,10 @@ const default_group_nudge_prompt = '[Write the next reply only as {{char}}.]'; const default_bias_presets = { [default_bias]: [], 'Anti-bond': [ - { text: ' bond', value: -50 }, - { text: ' future', value: -50 }, - { text: ' bonding', value: -50 }, - { text: ' connection', value: -25 }, + { id: '22154f79-dd98-41bc-8e34-87015d6a0eaf', text: ' bond', value: -50 }, + { id: '8ad2d5c4-d8ef-49e4-bc5e-13e7f4690e0f', text: ' future', value: -50 }, + { id: '52a4b280-0956-4940-ac52-4111f83e4046', text: ' bonding', value: -50 }, + { id: 'e63037c7-c9d1-4724-ab2d-7756008b433b', text: ' connection', value: -25 }, ], }; @@ -3177,6 +3178,14 @@ function loadOpenAISettings(data, settings) { $('#openai_logit_bias_preset').empty(); for (const preset of Object.keys(oai_settings.bias_presets)) { + // Backfill missing IDs + if (Array.isArray(oai_settings.bias_presets[preset])) { + oai_settings.bias_presets[preset].forEach((bias) => { + if (bias && !bias.id) { + bias.id = uuidv4(); + } + }); + } const option = document.createElement('option'); option.innerText = preset; option.value = preset; @@ -3465,7 +3474,7 @@ function onLogitBiasPresetChange() { } oai_settings.bias_preset_selected = value; - const list = $('.openai_logit_bias_list'); + const list = $('.openai_logit_bias_list'); list.empty(); for (const entry of preset) { @@ -3487,9 +3496,9 @@ function onLogitBiasPresetChange() { stop: function () { const order = []; list.children().each(function () { - order.unshift(parseInt($(this).data('id'))); + order.unshift($(this).data('id')); }); - preset.sort((a, b) => order.indexOf(preset.indexOf(a)) - order.indexOf(preset.indexOf(b))); + preset.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id)); console.log('Logit bias reordered:', preset); saveSettingsDebounced(); }, @@ -3500,7 +3509,7 @@ function onLogitBiasPresetChange() { } function createNewLogitBiasEntry() { - const entry = { text: '', value: 0 }; + const entry = { id: uuidv4(), text: '', value: 0 }; oai_settings.bias_presets[oai_settings.bias_preset_selected].push(entry); biasCache = undefined; createLogitBiasListItem(entry); @@ -3508,11 +3517,14 @@ function createNewLogitBiasEntry() { } function createLogitBiasListItem(entry) { - const id = oai_settings.bias_presets[oai_settings.bias_preset_selected].indexOf(entry); + if (!entry.id) { + entry.id = uuidv4(); + } + const id = entry.id; const template = $('#openai_logit_bias_template .openai_logit_bias_form').clone(); template.data('id', id); template.find('.openai_logit_bias_text').val(entry.text).on('input', function () { - oai_settings.bias_presets[oai_settings.bias_preset_selected][id].text = String($(this).val()); + entry.text = String($(this).val()); biasCache = undefined; saveSettingsDebounced(); }); @@ -3531,13 +3543,17 @@ function createLogitBiasListItem(entry) { value = max; } - oai_settings.bias_presets[oai_settings.bias_preset_selected][id].value = value; + entry.value = value; biasCache = undefined; saveSettingsDebounced(); }); template.find('.openai_logit_bias_remove').on('click', function () { $(this).closest('.openai_logit_bias_form').remove(); - oai_settings.bias_presets[oai_settings.bias_preset_selected].splice(id, 1); + const preset = oai_settings.bias_presets[oai_settings.bias_preset_selected]; + const index = preset.findIndex(item => item.id === id); + if (index >= 0) { + preset.splice(index, 1); + } onLogitBiasPresetChange(); }); $('.openai_logit_bias_list').prepend(template); diff --git a/public/style.css b/public/style.css index d47c8aaf6..f173bd1b4 100644 --- a/public/style.css +++ b/public/style.css @@ -5179,7 +5179,7 @@ body:not(.movingUI) .drawer-content.maximized { width: 100%; height: 100%; opacity: 0.8; - min-height: 3rem; + min-height: 2.5em; } .openai_restorable, From 7f94cb4bee8ec093822b5415d5845163ba930cae Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:36:58 +0200 Subject: [PATCH 114/222] CC: Simplify default wrappers for personality and scenario --- default/content/presets/openai/Default.json | 4 ++-- public/scripts/openai.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/default/content/presets/openai/Default.json b/default/content/presets/openai/Default.json index 345199cc1..f2eaa5d53 100644 --- a/default/content/presets/openai/Default.json +++ b/default/content/presets/openai/Default.json @@ -39,8 +39,8 @@ "proxy_password": "", "max_context_unlocked": false, "wi_format": "{0}", - "scenario_format": "[Circumstances and context of the dialogue: {{scenario}}]", - "personality_format": "[{{char}}'s personality: {{personality}}]", + "scenario_format": "{{scenario}}", + "personality_format": "{{personality}}", "group_nudge_prompt": "[Write the next reply only as {{char}}.]", "stream_openai": true, "prompts": [ diff --git a/public/scripts/openai.js b/public/scripts/openai.js index c9fb72afb..12ee4ce2d 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -102,8 +102,8 @@ const default_new_group_chat_prompt = '[Start a new group chat. Group members: { const default_new_example_chat_prompt = '[Example Chat]'; const default_continue_nudge_prompt = '[Continue the following message. Do not include ANY parts of the original message. Use capitalization and punctuation as if your reply is a part of the original message: {{lastChatMessage}}]'; const default_bias = 'Default (none)'; -const default_personality_format = '[{{char}}\'s personality: {{personality}}]'; -const default_scenario_format = '[Circumstances and context of the dialogue: {{scenario}}]'; +const default_personality_format = '{{personality}}'; +const default_scenario_format = '{{scenario}}'; const default_group_nudge_prompt = '[Write the next reply only as {{char}}.]'; const default_bias_presets = { [default_bias]: [], From 404a217622a1570697138462ebe1220cf2b15209 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:49:35 +0200 Subject: [PATCH 115/222] Backfill imported bias entry ids --- public/scripts/openai.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 12ee4ce2d..c94a88541 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -3733,6 +3733,9 @@ async function onLogitBiasPresetImportFileChange(e) { if (typeof entry == 'object' && entry !== null) { if (Object.hasOwn(entry, 'text') && Object.hasOwn(entry, 'value')) { + if (!entry.id) { + entry.id = uuidv4(); + } validEntries.push(entry); } } From 616fc34826978b44619992de98ba1e6dab0a653f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 01:08:56 +0200 Subject: [PATCH 116/222] Add type definitions for jQuery plugins --- package-lock.json | 44 +++++ package.json | 4 + public/global.d.ts | 408 ++------------------------------------------- 3 files changed, 64 insertions(+), 392 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63f034003..220b9a2a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,9 @@ "@types/deno": "^2.0.0", "@types/express": "^4.17.21", "@types/jquery": "^3.5.29", + "@types/jquery-cropper": "^1.0.4", + "@types/jquery.transit": "^0.9.33", + "@types/jqueryui": "^1.12.23", "@types/lodash": "^4.17.10", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.12", @@ -97,6 +100,7 @@ "@types/png-chunks-encode": "^1.0.2", "@types/png-chunks-extract": "^1.0.2", "@types/response-time": "^2.3.8", + "@types/select2": "^4.0.63", "@types/toastr": "^2.1.43", "@types/write-file-atomic": "^4.0.3", "@types/yargs": "^17.0.33", @@ -1228,6 +1232,36 @@ "@types/sizzle": "*" } }, + "node_modules/@types/jquery-cropper": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/jquery-cropper/-/jquery-cropper-1.0.4.tgz", + "integrity": "sha512-YMyUoY+rhB8yc3xM1B/daNaSq5+q93rzvRx6HP8K9mmvXEviTH3/rldlYNCGd0TmE/kLlZYJsruYhu9wY350PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jquery": "*" + } + }, + "node_modules/@types/jquery.transit": { + "version": "0.9.33", + "resolved": "https://registry.npmjs.org/@types/jquery.transit/-/jquery.transit-0.9.33.tgz", + "integrity": "sha512-gEDi1Lw7qfHFxtcnm2dg0F3Z5yG+84Sn0gDpGbd+u+r2RxsCcdQzfUmFKzHGBjWflZ9CXOZiAkenKOSvwLITrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jquery": "*" + } + }, + "node_modules/@types/jqueryui": { + "version": "1.12.23", + "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.23.tgz", + "integrity": "sha512-pm1yVNVI29B9IGw41anCEzA5eR2r1pYc7flqD471ZT7B0yUXIY7YNe/zq7LGpihIGXNzWyG+Q4YQSzv2AF3fNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jquery": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1379,6 +1413,16 @@ "@types/node": "*" } }, + "node_modules/@types/select2": { + "version": "4.0.63", + "resolved": "https://registry.npmjs.org/@types/select2/-/select2-4.0.63.tgz", + "integrity": "sha512-/DXUfPSj3iVTGlRYRYPCFKKSogAGP/j+Z0fIMXbBiBtmmZj0WH7vnfNuckafq9C43KnqPPQW2TI/Rj/vTSGnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jquery": "*" + } + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", diff --git a/package.json b/package.json index c77f893a6..a5a13056a 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,9 @@ "@types/deno": "^2.0.0", "@types/express": "^4.17.21", "@types/jquery": "^3.5.29", + "@types/jquery-cropper": "^1.0.4", + "@types/jquery.transit": "^0.9.33", + "@types/jqueryui": "^1.12.23", "@types/lodash": "^4.17.10", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.12", @@ -125,6 +128,7 @@ "@types/png-chunks-encode": "^1.0.2", "@types/png-chunks-extract": "^1.0.2", "@types/response-time": "^2.3.8", + "@types/select2": "^4.0.63", "@types/toastr": "^2.1.43", "@types/write-file-atomic": "^4.0.3", "@types/yargs": "^17.0.33", diff --git a/public/global.d.ts b/public/global.d.ts index 202d48443..f6935383d 100644 --- a/public/global.d.ts +++ b/public/global.d.ts @@ -20,400 +20,24 @@ declare global { pagination(method: 'getCurrentPageNum'): number; pagination(method: string, options?: any): JQuery; pagination(options?: any): JQuery; - transition(options?: any, complete?: function): JQuery; - autocomplete(options?: any): JQuery; - autocomplete(method: string, options?: any): JQuery; - slider(options?: any): JQuery; - slider(method: string, func: string, options?: any): JQuery; - cropper(options?: any): JQuery; izoomify(options?: any): JQuery; + } - //#region select2 + namespace Select2 { + interface Options { + /** + * Extends Select2 v4 plugin by adding an option to set a placeholder for the 'search' input field + * [Custom Field] + * @default '' + */ + searchInputPlaceholder?: string; - /** - * Initializes or modifies a select2 instance with provided options - * - * @param options - Configuration options for the select2 instance - * @returns The jQuery object for chaining - */ - select2(options?: Select2Options): JQuery; - - /** - * Retrieves data currently selected in the select2 instance - * - * @param field - A string specifying the 'data' method - * @returns An array of selected items - */ - select2(field: 'data'): any[]; - - /** - * Calls the specified select2 method - * - * @param method - The name of the select2 method to invoke - * @returns The jQuery object for chaining - */ - select2(method: 'open' | 'close' | 'destroy' | 'focus' | 'val', value?: any): JQuery; - - //#endregion - - //#region sortable - - /** - * Initializes or updates a sortable instance with the provided options - * - * @param options - Configuration options for the sortable instance - * @returns The jQuery object for chaining - */ - sortable(options?: SortableOptions): JQuery; - - /** - * Calls a sortable method to perform actions on the instance - * - * @param method - The name of the sortable method to invoke - * @returns The jQuery object for chaining - */ - sortable(method: 'destroy' | 'disable' | 'enable' | 'refresh' | 'toArray'): JQuery; - - /** - * Retrieves the sortable's instance object. If the element does not have an associated instance, undefined is returned. - * - * @returns The instance of the sortable object - */ - sortable(method: 'instance'): object; - - /** - * Retrieves the current option value for the specified option - * - * @param method - The string 'option' to retrieve an option value - * @param optionName - The name of the option to retrieve - * @returns The value of the specified option - */ - sortable(method: 'option', optionName: string): any; - - /** - * Sets the value of the specified option - * - * @param method - The string 'option' to set an option value - * @param optionName - The name of the option to set - * @param value - The value to assign to the option - * @returns The jQuery object for chaining - */ - sortable(method: 'option', optionName: string, value: any): JQuery; - - /** - * Sets multiple options using an object - * - * @param method - The string 'option' to set options - * @param options - An object containing multiple option key-value pairs - * @returns The jQuery object for chaining - */ - sortable(method: 'option', options: SortableOptions): JQuery; - - //#endregion + /** + * Extends select2 plugin by adding a custom css class for the 'search' input field + * [Custom Field] + * @default '' + */ + searchInputCssClass?: string; + } } } - -//#region select2 - -/** Options for configuring a select2 instance */ -interface Select2Options { - /** - * Provides support for ajax data sources - * @param params - Parameters including the search term - * @param callback - A callback function to handle the results - * @default null - */ - ajax?: { - url: string; - dataType?: string; - delay?: number; - data?: (params: any) => any; - processResults?: (data: any, params: any) => any; - } | { transport: (params, success, failure) => any }; - - /** - * Provides support for clearable selections - * @default false - */ - allowClear?: boolean; - - /** - * See Using Select2 with AMD or CommonJS loaders - * @default './i18n/' - */ - amdLanguageBase?: string; - - /** - * Controls whether the dropdown is closed after a selection is made - * @default true - */ - closeOnSelect?: boolean; - - /** - * Allows rendering dropdown options from an array - * @default null - */ - data?: object[]; - - /** - * Used to override the built-in DataAdapter - * @default SelectAdapter - */ - dataAdapter?: SelectAdapter; - - /** - * Enable debugging messages in the browser console - * @default false - */ - debug?: boolean; - - /** - * Sets the dir attribute on the selection and dropdown containers to indicate the direction of the text - * @default 'ltr' - */ - dir?: string; - - /** - * When set to true, the select control will be disabled - * @default false - */ - disabled?: boolean; - - /** - * Used to override the built-in DropdownAdapter - * @default DropdownAdapter - */ - dropdownAdapter?: DropdownAdapter; - - /** - * @default false - */ - dropdownAutoWidth?: boolean; - - /** - * Adds additional CSS classes to the dropdown container. The helper :all: can be used to add all CSS classes present on the original element - * @default '' - */ - selectionCssClass?: string; - - /** - * Implements automatic selection when the dropdown is closed - * @default false - */ - selectOnClose?: boolean; - - sorter?: function; - - /** - * When set to `true`, allows the user to create new tags that aren't pre-populated - * Used to enable free text responses - * @default false - */ - tags?: boolean | object[]; - - /** - * Customizes the way that search results are rendered - * @param item - The item object to format - * @returns The formatted representation - * @default null - */ - templateResult?: (item: any) => JQuery | string; - - /** - * Customizes the way that selections are rendered - * @param item - The selected item object to format - * @returns The formatted representation - * @default null - */ - templateSelection?: (item: any) => JQuery | string; - - /** - * Allows you to set the theme - * @default 'default' - */ - theme?: string; - - /** - * A callback that handles automatic tokenization of free-text entry - * @default null - */ - tokenizer?: (input: { _type: string, term: string }, selection: { options: object }, callback: (Select2Option) => any) => { term: string }; - - /** - * The list of characters that should be used as token separators - * @default null - */ - tokenSeparators?: string[]; - - /** - * Supports customization of the container width - * @default 'resolve' - */ - width?: string; - - /** - * If true, resolves issue for multiselects using closeOnSelect: false that caused the list of results to scroll to the first selection after each select/unselect - * @default false - */ - scrollAfterSelect?: boolean; - - /** - * Extends Select2 v4 plugin by adding an option to set a placeholder for the 'search' input field - * [Custom Field] - * @default '' - */ - searchInputPlaceholder?: string; - - /** - * Extends select2 plugin by adding a custom css class for the 'searcH' input field - * [Custom Field] - * @default '' - */ - searchInputCssClass?: string; -} - -//#endregion - -//#region sortable - -/** Options for configuring a sortable instance */ -interface SortableOptions { - /** - * When set, prevents the sortable items from being dragged unless clicked with a delay - * @default 0 - */ - delay?: number; - - /** - * Class name for elements to handle sorting. Elements with this class can be dragged to sort. - * @default '' - */ - handle?: string; - - /** - * Whether to allow sorting between different connected lists - * @default false - */ - connectWith?: string | boolean; - - /** - * Function called when sorting starts - * @param event - The event object - * @param ui - The UI object containing the helper and position information - */ - start?: (event: Event, ui: SortableUI) => void; - - /** - * Function called when sorting stops - * @param event - The event object - * @param ui - The UI object containing the helper and position information - */ - stop?: (event: Event, ui: SortableUI) => void; - - /** - * Function called when sorting updates - * @param event - The event object - * @param ui - The UI object containing the helper and position information - */ - update?: (event: Event, ui: SortableUI) => void; - - /** - * Specifies which items inside the element should be sortable - * @default '> *' - */ - items?: string; -} - -/** UI object passed to sortable event handlers */ -interface SortableUI { - /** jQuery object representing the helper element */ - helper: JQuery; - /** The current position of the helper element */ - position: { top: number; left: number }; - /** Original position of the helper element */ - originalPosition: { top: number; left: number }; - /** jQuery object representing the item being sorted */ - item: JQuery; - /** The placeholder element used during sorting */ - placeholder: JQuery; -} - -//#endregion From 5eff49f442bb468e14689b3bf7c321e1082a3f51 Mon Sep 17 00:00:00 2001 From: Angel Luis Jimenez Martinez Date: Mon, 23 Dec 2024 10:56:08 +0100 Subject: [PATCH 117/222] Fix "save" translation in es-es --- public/locales/es-es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/es-es.json b/public/locales/es-es.json index 5ca3e7144..f4f3c9f45 100644 --- a/public/locales/es-es.json +++ b/public/locales/es-es.json @@ -874,7 +874,7 @@ "Bulk_edit_characters": "Editar personajes masivamente", "Bulk select all characters": "Seleccionar de forma masiva todos los personajes", "Bulk delete characters": "Eliminar personajes masivamente", - "popup-button-save": "Ahorrar", + "popup-button-save": "Guardar", "popup-button-yes": "Sí", "popup-button-no": "No", "popup-button-cancel": "Cancelar", @@ -1019,7 +1019,7 @@ "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Este mensaje no puede ser anulado por tarjetas de personaje, incluso si se prefieren las anulaciones.", "prompt_manager_forbid_overrides": "Prohibir anulaciones", "reset": "reiniciar", - "save": "ahorrar", + "save": "guardar", "This message is invisible for the AI": "Este mensaje es invisible para la IA", "Message Actions": "Acciones de mensajes", "Translate message": "Traducir mensaje", From 71050ef1d299cffc74f0467663693a4dd809f521 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 19:37:51 +0200 Subject: [PATCH 118/222] Fix AI Horde deadnaming --- public/index.html | 6 +++--- public/locales/ar-sa.json | 2 +- public/locales/de-de.json | 2 +- public/locales/es-es.json | 2 +- public/locales/fr-fr.json | 2 +- public/locales/is-is.json | 2 +- public/locales/it-it.json | 2 +- public/locales/ja-jp.json | 2 +- public/locales/ko-kr.json | 4 ++-- public/locales/nl-nl.json | 2 +- public/locales/pt-pt.json | 2 +- public/locales/ru-ru.json | 2 +- public/locales/uk-ua.json | 2 +- public/locales/vi-vn.json | 2 +- public/locales/zh-cn.json | 2 +- public/locales/zh-tw.json | 4 ++-- 16 files changed, 20 insertions(+), 20 deletions(-) diff --git a/public/index.html b/public/index.html index c885111e1..c08b04a6d 100644 --- a/public/index.html +++ b/public/index.html @@ -2053,7 +2053,7 @@ - +
    @@ -2062,8 +2062,8 @@
    • - - KoboldAI Horde Website + + AI Horde Website
    • diff --git a/public/locales/ar-sa.json b/public/locales/ar-sa.json index aac6e5788..efcc6835a 100644 --- a/public/locales/ar-sa.json +++ b/public/locales/ar-sa.json @@ -267,7 +267,7 @@ "Text Completion": "اكتمال النص", "Chat Completion": "إكمال الدردشة", "NovelAI": "NovelAI", - "KoboldAI Horde": "جماعة KoboldAI", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "تجنب إرسال معلومات حساسة إلى الجماعة.", "Review the Privacy statement": "مراجعة بيان الخصوصية", diff --git a/public/locales/de-de.json b/public/locales/de-de.json index 3bef32f41..898bcc795 100644 --- a/public/locales/de-de.json +++ b/public/locales/de-de.json @@ -267,7 +267,7 @@ "Text Completion": "Textvervollständigung", "Chat Completion": "Chat-Abschluss", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Vermeide das Senden sensibler Informationen an die Horde.", "Review the Privacy statement": "Überprüfe die Datenschutzerklärung", diff --git a/public/locales/es-es.json b/public/locales/es-es.json index f4f3c9f45..ca61f534b 100644 --- a/public/locales/es-es.json +++ b/public/locales/es-es.json @@ -267,7 +267,7 @@ "Text Completion": "Completar texto", "Chat Completion": "Finalización del chat", "NovelAI": "NovelAI", - "KoboldAI Horde": "Horde de KoboldAI", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Evite enviar información sensible a Horde.", "Review the Privacy statement": "Revise la declaración de privacidad", diff --git a/public/locales/fr-fr.json b/public/locales/fr-fr.json index 3c4280f7b..3f712b5d3 100644 --- a/public/locales/fr-fr.json +++ b/public/locales/fr-fr.json @@ -267,7 +267,7 @@ "Text Completion": "Achèvement du texte", "Chat Completion": "Achèvement du chat", "NovelAI": "NovelAI", - "KoboldAI Horde": "Horde KoboldAI", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Évitez d'envoyer des informations sensibles à la Horde.", "Review the Privacy statement": "Examiner la déclaration de confidentialité", diff --git a/public/locales/is-is.json b/public/locales/is-is.json index e2da3f5a1..e8d359886 100644 --- a/public/locales/is-is.json +++ b/public/locales/is-is.json @@ -267,7 +267,7 @@ "Text Completion": "Textaútfylling", "Chat Completion": "Spjalllokun", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Hópur", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Forðastu að senda viðkvæm gögn til Hórdans.", "Review the Privacy statement": "Farið yfir Persónuverndarskýrsluna", diff --git a/public/locales/it-it.json b/public/locales/it-it.json index f6d8a859a..f45d5c973 100644 --- a/public/locales/it-it.json +++ b/public/locales/it-it.json @@ -267,7 +267,7 @@ "Text Completion": "Completamento del testo", "Chat Completion": "Completamento della chat", "NovelAI": "NovelAI", - "KoboldAI Horde": "Orda di KoboldAI", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Evita di inviare informazioni sensibili all'Orda.", "Review the Privacy statement": "Revisione della dichiarazione sulla privacy", diff --git a/public/locales/ja-jp.json b/public/locales/ja-jp.json index b75eb103f..c8c55a6d8 100644 --- a/public/locales/ja-jp.json +++ b/public/locales/ja-jp.json @@ -267,7 +267,7 @@ "Text Completion": "テキスト補完", "Chat Completion": "チャット完了", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Hordeに機密情報を送信しないでください。", "Review the Privacy statement": "プライバシー声明を確認する", diff --git a/public/locales/ko-kr.json b/public/locales/ko-kr.json index 1aa13299f..42fc8ed63 100644 --- a/public/locales/ko-kr.json +++ b/public/locales/ko-kr.json @@ -269,7 +269,7 @@ "Text Completion": "Text Completion", "Chat Completion": "Chat Completion", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "민감한 정보를 Horde에 보내지 않도록 합니다.", "Review the Privacy statement": "개인 정보 보호 정책 검토", @@ -1623,4 +1623,4 @@ "Master Import": "마스터 불러오기", "Master Export": "마스터 내보내기", "Chat Quick Reply Sets": "채팅 빠른 답장 세트들" -} \ No newline at end of file +} diff --git a/public/locales/nl-nl.json b/public/locales/nl-nl.json index f32802306..32f953210 100644 --- a/public/locales/nl-nl.json +++ b/public/locales/nl-nl.json @@ -267,7 +267,7 @@ "Text Completion": "Tekstvoltooiing", "Chat Completion": "Chat-voltooiing", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Vermijd het verzenden van gevoelige informatie naar de Horde.", "Review the Privacy statement": "Bekijk de privacyverklaring", diff --git a/public/locales/pt-pt.json b/public/locales/pt-pt.json index d7d83a970..a308e65ed 100644 --- a/public/locales/pt-pt.json +++ b/public/locales/pt-pt.json @@ -267,7 +267,7 @@ "Text Completion": "Conclusão de texto", "Chat Completion": "Conclusão do bate-papo", "NovelAI": "NovelAI", - "KoboldAI Horde": "Horda KoboldAI", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Evite enviar informações sensíveis para a Horda.", "Review the Privacy statement": "Reveja a declaração de privacidade", diff --git a/public/locales/ru-ru.json b/public/locales/ru-ru.json index c61294cd3..3db3091b2 100644 --- a/public/locales/ru-ru.json +++ b/public/locales/ru-ru.json @@ -299,7 +299,7 @@ "Example: http://127.0.0.1:5000/api ": "Пример: http://127.0.0.1:5000/api", "No connection...": "Нет соединения...", "Get your NovelAI API Key": "Получите свой API-ключ для NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "NovelAI": "NovelAI", "OpenAI API key": "Ключ для API OpenAI", "Trim spaces": "Обрезать пробелы", diff --git a/public/locales/uk-ua.json b/public/locales/uk-ua.json index 2db43a5bc..c8b85d689 100644 --- a/public/locales/uk-ua.json +++ b/public/locales/uk-ua.json @@ -267,7 +267,7 @@ "Text Completion": "Завершення тексту", "Chat Completion": "Завершення чату", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Уникайте надсилання чутливої інформації в Horde.", "Review the Privacy statement": "Перегляньте заяву про конфіденційність", diff --git a/public/locales/vi-vn.json b/public/locales/vi-vn.json index 059303f77..71a5e841c 100644 --- a/public/locales/vi-vn.json +++ b/public/locales/vi-vn.json @@ -267,7 +267,7 @@ "Text Completion": "Text Completion", "Chat Completion": "Chat Completion", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "Tránh gửi thông tin nhạy cảm cho Horde.", "Review the Privacy statement": "Xem lại Chính sách bảo mật", diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index c7fb563fe..4abad5496 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -276,7 +276,7 @@ "Text Completion": "文本补全", "Chat Completion": "聊天补全", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "避免向 Horde 发送敏感信息。", "Review the Privacy statement": "查看隐私声明", diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index 6e029c9ed..9edaed471 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -268,7 +268,7 @@ "Text Completion": "文字補充", "Chat Completion": "聊天補充", "NovelAI": "NovelAI", - "KoboldAI Horde": "KoboldAI Horde", + "AI Horde": "AI Horde", "KoboldAI": "KoboldAI", "Avoid sending sensitive information to the Horde.": "避免發送敏感資訊到 Horde。", "Review the Privacy statement": "檢視隱私聲明", @@ -1664,7 +1664,7 @@ "Karras": "Karras", "Keep model in memory": "將模型保存在記憶體中", "Keyboard": "鍵盤:", - "KoboldAI Horde Website": "KoboldAI Horde 網站", + "AI Horde Website": "AI Horde 網站", "Last User Prefix": "最後用戶前綴", "Linear": "線性", "LLM": "LLM", From 650853eabbd2b9f449263f6b2da4ae964bd1c538 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:28:21 +0200 Subject: [PATCH 119/222] Translation: Split Portuguese langs --- public/scripts/extensions/translate/index.js | 3 ++- src/endpoints/translate.js | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/translate/index.js b/public/scripts/extensions/translate/index.js index c42a34f38..11f6d588f 100644 --- a/public/scripts/extensions/translate/index.js +++ b/public/scripts/extensions/translate/index.js @@ -106,7 +106,8 @@ const languageCodes = { 'Pashto': 'ps', 'Persian': 'fa', 'Polish': 'pl', - 'Portuguese (Portugal, Brazil)': 'pt', + 'Portuguese (Portugal)': 'pt-PT', + 'Portuguese (Brazil)': 'pt-BR', 'Punjabi': 'pa', 'Romanian': 'ro', 'Russian': 'ru', diff --git a/src/endpoints/translate.js b/src/endpoints/translate.js index 8a53e9de4..432b6240e 100644 --- a/src/endpoints/translate.js +++ b/src/endpoints/translate.js @@ -248,8 +248,7 @@ router.post('/deepl', jsonParser, async (request, response) => { params.append('text', text); params.append('target_lang', lang); - if (['de', 'fr', 'it', 'es', 'nl', 'ja', 'ru'].includes(lang)) { - // We don't specify a Portuguese variant, so ignore formality for it. + if (['de', 'fr', 'it', 'es', 'nl', 'ja', 'ru', 'pt-BR', 'pt-PT'].includes(lang)) { params.append('formality', formality); } From c4a92c95e68478c685358118899ea63c33ca0b61 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:12:18 +0200 Subject: [PATCH 120/222] Update bing translate API package, fix language codes --- package-lock.json | 8 +-- package.json | 2 +- src/endpoints/translate.js | 104 +++++++++++++++++++++++-------------- 3 files changed, 69 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 220b9a2a0..e20796d14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@popperjs/core": "^2.11.8", "@zeldafan0225/ai_horde": "^5.1.0", "archiver": "^7.0.1", - "bing-translate-api": "^2.9.1", + "bing-translate-api": "^4.0.2", "body-parser": "^1.20.2", "bowser": "^2.11.0", "command-exists": "^1.2.9", @@ -2161,9 +2161,9 @@ } }, "node_modules/bing-translate-api": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-2.9.1.tgz", - "integrity": "sha512-DaYqa7iupfj+fj/KeaeZSp5FUY/ZR4sZ6jqoTP0RHkYOUfo7wwoxlhYDkh4VcvBBzuVORnBEgdXBVQrfM4kk7g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.0.2.tgz", + "integrity": "sha512-JJ8XUehnxzOhHU91oy86xEtp8OOMjVEjCZJX042fKxoO19NNvxJ5omeCcxQNFoPbDqVpBJwqiGVquL0oPdQm1Q==", "license": "MIT", "dependencies": { "got": "^11.8.6" diff --git a/package.json b/package.json index a5a13056a..cea368850 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@popperjs/core": "^2.11.8", "@zeldafan0225/ai_horde": "^5.1.0", "archiver": "^7.0.1", - "bing-translate-api": "^2.9.1", + "bing-translate-api": "^4.0.2", "body-parser": "^1.20.2", "bowser": "^2.11.0", "command-exists": "^1.2.9", diff --git a/src/endpoints/translate.js b/src/endpoints/translate.js index 432b6240e..6c9831edb 100644 --- a/src/endpoints/translate.js +++ b/src/endpoints/translate.js @@ -1,9 +1,8 @@ -import https from 'node:https'; import { createRequire } from 'node:module'; import fetch from 'node-fetch'; import express from 'express'; -import bingTranslateApi from 'bing-translate-api'; +import { translate as bingTranslate } from 'bing-translate-api'; import iconv from 'iconv-lite'; import { readSecret, SECRET_KEYS } from './secrets.js'; @@ -56,6 +55,10 @@ router.post('/libre', jsonParser, async (request, response) => { request.body.lang = 'zt'; } + if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') { + request.body.lang = 'pt'; + } + const text = request.body.text; const lang = request.body.lang; @@ -81,7 +84,7 @@ router.post('/libre', jsonParser, async (request, response) => { if (!result.ok) { const error = await result.text(); console.log('LibreTranslate error: ', result.statusText, error); - return response.sendStatus(result.status); + return response.sendStatus(500); } /** @type {any} */ @@ -130,6 +133,14 @@ router.post('/google', jsonParser, async (request, response) => { router.post('/yandex', jsonParser, async (request, response) => { try { + if (request.body.lang === 'pt-PT') { + request.body.lang = 'pt'; + } + + if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') { + request.body.lang = 'zh'; + } + const chunks = request.body.chunks; const lang = request.body.lang; @@ -185,6 +196,14 @@ router.post('/lingva', jsonParser, async (request, response) => { return response.sendStatus(400); } + if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') { + request.body.lang = 'zh'; + } + + if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') { + request.body.lang = 'pt'; + } + const text = request.body.text; const lang = request.body.lang; @@ -193,29 +212,24 @@ router.post('/lingva', jsonParser, async (request, response) => { } console.log('Input text: ' + text); - const url = `${baseUrl}/auto/${lang}/${encodeURIComponent(text)}`; - https.get(url, (resp) => { - let data = ''; + try { + const url = `${baseUrl}/auto/${lang}/${encodeURIComponent(text)}`; + const result = await fetch(url); - resp.on('data', (chunk) => { - data += chunk; - }); + if (!result.ok) { + const error = await result.text(); + console.log('Lingva error: ', result.statusText, error); + } - resp.on('end', () => { - try { - const result = JSON.parse(data); - console.log('Translated text: ' + result.translation); - return response.send(result.translation); - } catch (error) { - console.log('Translation error', error); - return response.sendStatus(500); - } - }); - }).on('error', (err) => { - console.log('Translation error: ' + err.message); + /** @type {any} */ + const data = await result.json(); + console.log('Translated text: ' + data.translation); + return response.send(data.translation); + } catch (error) { + console.log('Translation error:', error); return response.sendStatus(500); - }); + } } catch (error) { console.log('Translation error', error); return response.sendStatus(500); @@ -266,7 +280,7 @@ router.post('/deepl', jsonParser, async (request, response) => { if (!result.ok) { const error = await result.text(); console.log('DeepL error: ', result.statusText, error); - return response.sendStatus(result.status); + return response.sendStatus(500); } /** @type {any} */ @@ -319,7 +333,7 @@ router.post('/onering', jsonParser, async (request, response) => { if (!result.ok) { const error = await result.text(); console.log('OneRing error: ', result.statusText, error); - return response.sendStatus(result.status); + return response.sendStatus(500); } /** @type {any} */ @@ -375,7 +389,7 @@ router.post('/deeplx', jsonParser, async (request, response) => { if (!result.ok) { const error = await result.text(); console.log('DeepLX error: ', result.statusText, error); - return response.sendStatus(result.status); + return response.sendStatus(500); } /** @type {any} */ @@ -390,24 +404,34 @@ router.post('/deeplx', jsonParser, async (request, response) => { }); router.post('/bing', jsonParser, async (request, response) => { - const text = request.body.text; - let lang = request.body.lang; + try { + const text = request.body.text; + let lang = request.body.lang; - if (request.body.lang === 'zh-CN') { - lang = 'zh-Hans'; - } + if (request.body.lang === 'zh-CN') { + lang = 'zh-Hans'; + } - if (!text || !lang) { - return response.sendStatus(400); - } + if (request.body.lang === 'zh-TW') { + lang = 'zh-Hant'; + } - console.log('Input text: ' + text); + if (request.body.lang === 'pt-BR') { + lang = 'pt'; + } - bingTranslateApi.translate(text, null, lang).then(result => { - console.log('Translated text: ' + result.translation); - return response.send(result.translation); - }).catch(err => { - console.log('Translation error: ' + err.message); + if (!text || !lang) { + return response.sendStatus(400); + } + + console.log('Input text: ' + text); + + const result = await bingTranslate(text, null, lang); + const translatedText = result?.translation; + console.log('Translated text: ' + translatedText); + return response.send(translatedText); + } catch (error) { + console.log('Translation error', error); return response.sendStatus(500); - }); + } }); From 8394b976005d73f512b73600528c84ea65a901ef Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:34:16 +0200 Subject: [PATCH 121/222] Add default URL for Lingva translator --- src/endpoints/translate.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/endpoints/translate.js b/src/endpoints/translate.js index 6c9831edb..f3d357a1a 100644 --- a/src/endpoints/translate.js +++ b/src/endpoints/translate.js @@ -11,6 +11,7 @@ import { jsonParser } from '../express-common.js'; const DEEPLX_URL_DEFAULT = 'http://127.0.0.1:1188/translate'; const ONERING_URL_DEFAULT = 'http://127.0.0.1:4990/translate'; +const LINGVA_DEFAULT = 'https://lingva.ml/api/v1'; export const router = express.Router(); @@ -189,7 +190,12 @@ router.post('/yandex', jsonParser, async (request, response) => { router.post('/lingva', jsonParser, async (request, response) => { try { - const baseUrl = readSecret(request.user.directories, SECRET_KEYS.LINGVA_URL); + const secretUrl = readSecret(request.user.directories, SECRET_KEYS.LINGVA_URL); + const baseUrl = secretUrl || LINGVA_DEFAULT; + + if (!secretUrl && baseUrl === ONERING_URL_DEFAULT) { + console.log('Lingva URL is using default value.', LINGVA_DEFAULT); + } if (!baseUrl) { console.log('Lingva URL is not configured.'); @@ -307,6 +313,10 @@ router.post('/onering', jsonParser, async (request, response) => { console.log('OneRing URL is using default value.', ONERING_URL_DEFAULT); } + if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') { + request.body.lang = 'pt'; + } + const text = request.body.text; const from_lang = request.body.from_lang; const to_lang = request.body.to_lang; From 83e677d6cb1115134e746c72a8e971655732df4a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:38:19 +0200 Subject: [PATCH 122/222] Fix default URL check for Lingva translator --- src/endpoints/translate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/translate.js b/src/endpoints/translate.js index f3d357a1a..370e24229 100644 --- a/src/endpoints/translate.js +++ b/src/endpoints/translate.js @@ -193,7 +193,7 @@ router.post('/lingva', jsonParser, async (request, response) => { const secretUrl = readSecret(request.user.directories, SECRET_KEYS.LINGVA_URL); const baseUrl = secretUrl || LINGVA_DEFAULT; - if (!secretUrl && baseUrl === ONERING_URL_DEFAULT) { + if (!secretUrl && baseUrl === LINGVA_DEFAULT) { console.log('Lingva URL is using default value.', LINGVA_DEFAULT); } From 1a16957519f75b5eab2ae08dbe5ae422a7c48dc5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:51:33 +0200 Subject: [PATCH 123/222] Construct Lingva URL using url-join --- src/endpoints/translate.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/endpoints/translate.js b/src/endpoints/translate.js index 370e24229..9bfadb57f 100644 --- a/src/endpoints/translate.js +++ b/src/endpoints/translate.js @@ -4,6 +4,7 @@ import fetch from 'node-fetch'; import express from 'express'; import { translate as bingTranslate } from 'bing-translate-api'; import iconv from 'iconv-lite'; +import urlJoin from 'url-join'; import { readSecret, SECRET_KEYS } from './secrets.js'; import { getConfigValue, uuidv4 } from '../util.js'; @@ -220,7 +221,7 @@ router.post('/lingva', jsonParser, async (request, response) => { console.log('Input text: ' + text); try { - const url = `${baseUrl}/auto/${lang}/${encodeURIComponent(text)}`; + const url = urlJoin(baseUrl, 'auto', lang, encodeURIComponent(text)); const result = await fetch(url); if (!result.ok) { From 09dd9762f78d4758ed89a5dfa525d2f4471d8fc3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 23 Dec 2024 23:42:20 +0200 Subject: [PATCH 124/222] Clean-up try/catch blocks in translate.js --- src/endpoints/translate.js | 256 ++++++++++++++++++------------------- 1 file changed, 123 insertions(+), 133 deletions(-) diff --git a/src/endpoints/translate.js b/src/endpoints/translate.js index 9bfadb57f..bd3a66e00 100644 --- a/src/endpoints/translate.js +++ b/src/endpoints/translate.js @@ -41,36 +41,36 @@ function decodeBuffer(buffer) { } router.post('/libre', jsonParser, async (request, response) => { - const key = readSecret(request.user.directories, SECRET_KEYS.LIBRE); - const url = readSecret(request.user.directories, SECRET_KEYS.LIBRE_URL); - - if (!url) { - console.log('LibreTranslate URL is not configured.'); - return response.sendStatus(400); - } - - if (request.body.lang === 'zh-CN') { - request.body.lang = 'zh'; - } - - if (request.body.lang === 'zh-TW') { - request.body.lang = 'zt'; - } - - if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') { - request.body.lang = 'pt'; - } - - const text = request.body.text; - const lang = request.body.lang; - - if (!text || !lang) { - return response.sendStatus(400); - } - - console.log('Input text: ' + text); - try { + const key = readSecret(request.user.directories, SECRET_KEYS.LIBRE); + const url = readSecret(request.user.directories, SECRET_KEYS.LIBRE_URL); + + if (!url) { + console.log('LibreTranslate URL is not configured.'); + return response.sendStatus(400); + } + + if (request.body.lang === 'zh-CN') { + request.body.lang = 'zh'; + } + + if (request.body.lang === 'zh-TW') { + request.body.lang = 'zt'; + } + + if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') { + request.body.lang = 'pt'; + } + + const text = request.body.text; + const lang = request.body.lang; + + if (!text || !lang) { + return response.sendStatus(400); + } + + console.log('Input text: ' + text); + const result = await fetch(url, { method: 'POST', body: JSON.stringify({ @@ -198,11 +198,6 @@ router.post('/lingva', jsonParser, async (request, response) => { console.log('Lingva URL is using default value.', LINGVA_DEFAULT); } - if (!baseUrl) { - console.log('Lingva URL is not configured.'); - return response.sendStatus(400); - } - if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') { request.body.lang = 'zh'; } @@ -220,23 +215,18 @@ router.post('/lingva', jsonParser, async (request, response) => { console.log('Input text: ' + text); - try { - const url = urlJoin(baseUrl, 'auto', lang, encodeURIComponent(text)); - const result = await fetch(url); + const url = urlJoin(baseUrl, 'auto', lang, encodeURIComponent(text)); + const result = await fetch(url); - if (!result.ok) { - const error = await result.text(); - console.log('Lingva error: ', result.statusText, error); - } - - /** @type {any} */ - const data = await result.json(); - console.log('Translated text: ' + data.translation); - return response.send(data.translation); - } catch (error) { - console.log('Translation error:', error); - return response.sendStatus(500); + if (!result.ok) { + const error = await result.text(); + console.log('Lingva error: ', result.statusText, error); } + + /** @type {any} */ + const data = await result.json(); + console.log('Translated text: ' + data.translation); + return response.send(data.translation); } catch (error) { console.log('Translation error', error); return response.sendStatus(500); @@ -244,36 +234,36 @@ router.post('/lingva', jsonParser, async (request, response) => { }); router.post('/deepl', jsonParser, async (request, response) => { - const key = readSecret(request.user.directories, SECRET_KEYS.DEEPL); - - if (!key) { - console.log('DeepL key is not configured.'); - return response.sendStatus(400); - } - - if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') { - request.body.lang = 'ZH'; - } - - const text = request.body.text; - const lang = request.body.lang; - const formality = getConfigValue('deepl.formality', 'default'); - - if (!text || !lang) { - return response.sendStatus(400); - } - - console.log('Input text: ' + text); - - const params = new URLSearchParams(); - params.append('text', text); - params.append('target_lang', lang); - - if (['de', 'fr', 'it', 'es', 'nl', 'ja', 'ru', 'pt-BR', 'pt-PT'].includes(lang)) { - params.append('formality', formality); - } - try { + const key = readSecret(request.user.directories, SECRET_KEYS.DEEPL); + + if (!key) { + console.log('DeepL key is not configured.'); + return response.sendStatus(400); + } + + if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') { + request.body.lang = 'ZH'; + } + + const text = request.body.text; + const lang = request.body.lang; + const formality = getConfigValue('deepl.formality', 'default'); + + if (!text || !lang) { + return response.sendStatus(400); + } + + console.log('Input text: ' + text); + + const params = new URLSearchParams(); + params.append('text', text); + params.append('target_lang', lang); + + if (['de', 'fr', 'it', 'es', 'nl', 'ja', 'ru', 'pt-BR', 'pt-PT'].includes(lang)) { + params.append('formality', formality); + } + const result = await fetch('https://api-free.deepl.com/v2/translate', { method: 'POST', body: params, @@ -302,38 +292,38 @@ router.post('/deepl', jsonParser, async (request, response) => { }); router.post('/onering', jsonParser, async (request, response) => { - const secretUrl = readSecret(request.user.directories, SECRET_KEYS.ONERING_URL); - const url = secretUrl || ONERING_URL_DEFAULT; - - if (!url) { - console.log('OneRing URL is not configured.'); - return response.sendStatus(400); - } - - if (!secretUrl && url === ONERING_URL_DEFAULT) { - console.log('OneRing URL is using default value.', ONERING_URL_DEFAULT); - } - - if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') { - request.body.lang = 'pt'; - } - - const text = request.body.text; - const from_lang = request.body.from_lang; - const to_lang = request.body.to_lang; - - if (!text || !from_lang || !to_lang) { - return response.sendStatus(400); - } - - const params = new URLSearchParams(); - params.append('text', text); - params.append('from_lang', from_lang); - params.append('to_lang', to_lang); - - console.log('Input text: ' + text); - try { + const secretUrl = readSecret(request.user.directories, SECRET_KEYS.ONERING_URL); + const url = secretUrl || ONERING_URL_DEFAULT; + + if (!url) { + console.log('OneRing URL is not configured.'); + return response.sendStatus(400); + } + + if (!secretUrl && url === ONERING_URL_DEFAULT) { + console.log('OneRing URL is using default value.', ONERING_URL_DEFAULT); + } + + if (request.body.lang === 'pt-BR' || request.body.lang === 'pt-PT') { + request.body.lang = 'pt'; + } + + const text = request.body.text; + const from_lang = request.body.from_lang; + const to_lang = request.body.to_lang; + + if (!text || !from_lang || !to_lang) { + return response.sendStatus(400); + } + + const params = new URLSearchParams(); + params.append('text', text); + params.append('from_lang', from_lang); + params.append('to_lang', to_lang); + + console.log('Input text: ' + text); + const fetchUrl = new URL(url); fetchUrl.search = params.toString(); @@ -359,31 +349,31 @@ router.post('/onering', jsonParser, async (request, response) => { }); router.post('/deeplx', jsonParser, async (request, response) => { - const secretUrl = readSecret(request.user.directories, SECRET_KEYS.DEEPLX_URL); - const url = secretUrl || DEEPLX_URL_DEFAULT; - - if (!url) { - console.log('DeepLX URL is not configured.'); - return response.sendStatus(400); - } - - if (!secretUrl && url === DEEPLX_URL_DEFAULT) { - console.log('DeepLX URL is using default value.', DEEPLX_URL_DEFAULT); - } - - const text = request.body.text; - let lang = request.body.lang; - if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') { - lang = 'ZH'; - } - - if (!text || !lang) { - return response.sendStatus(400); - } - - console.log('Input text: ' + text); - try { + const secretUrl = readSecret(request.user.directories, SECRET_KEYS.DEEPLX_URL); + const url = secretUrl || DEEPLX_URL_DEFAULT; + + if (!url) { + console.log('DeepLX URL is not configured.'); + return response.sendStatus(400); + } + + if (!secretUrl && url === DEEPLX_URL_DEFAULT) { + console.log('DeepLX URL is using default value.', DEEPLX_URL_DEFAULT); + } + + const text = request.body.text; + let lang = request.body.lang; + if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') { + lang = 'ZH'; + } + + if (!text || !lang) { + return response.sendStatus(400); + } + + console.log('Input text: ' + text); + const result = await fetch(url, { method: 'POST', body: JSON.stringify({ From 7adc6d38e29672ac27acebf1d588f61be97d68d1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 24 Dec 2024 21:51:47 +0200 Subject: [PATCH 125/222] OpenRouter: Add control for middle-out transform Closes #3033 --- public/index.html | 13 +++++++++++++ public/scripts/openai.js | 18 ++++++++++++++++++ src/endpoints/backends/chat-completions.js | 20 +++++++++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index c08b04a6d..7ea940004 100644 --- a/public/index.html +++ b/public/index.html @@ -651,6 +651,19 @@
    +
    +
    + Middle-out Transform + +
    +
    + +
    +
    Max prompt cost: Unknown
    diff --git a/public/scripts/openai.js b/public/scripts/openai.js index c94a88541..41b929ef3 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -207,6 +207,12 @@ const custom_prompt_post_processing_types = { STRICT: 'strict', }; +const openrouter_middleout_types = { + AUTO: 'auto', + ON: 'on', + OFF: 'off', +}; + const sensitiveFields = [ 'reverse_proxy', 'proxy_password', @@ -267,6 +273,7 @@ const default_settings = { openrouter_sort_models: 'alphabetically', openrouter_providers: [], openrouter_allow_fallbacks: true, + openrouter_middleout: openrouter_middleout_types.ON, jailbreak_system: false, reverse_proxy: '', chat_completion_source: chat_completion_sources.OPENAI, @@ -343,6 +350,7 @@ const oai_settings = { openrouter_sort_models: 'alphabetically', openrouter_providers: [], openrouter_allow_fallbacks: true, + openrouter_middleout: openrouter_middleout_types.ON, jailbreak_system: false, reverse_proxy: '', chat_completion_source: chat_completion_sources.OPENAI, @@ -1920,6 +1928,7 @@ async function sendOpenAIRequest(type, messages, signal) { generate_data['use_fallback'] = oai_settings.openrouter_use_fallback; generate_data['provider'] = oai_settings.openrouter_providers; generate_data['allow_fallbacks'] = oai_settings.openrouter_allow_fallbacks; + generate_data['middleout'] = oai_settings.openrouter_middleout; if (isTextCompletion) { generate_data['stop'] = getStoppingStrings(isImpersonate, isContinue); @@ -3022,6 +3031,7 @@ function loadOpenAISettings(data, settings) { oai_settings.openrouter_sort_models = settings.openrouter_sort_models ?? default_settings.openrouter_sort_models; oai_settings.openrouter_use_fallback = settings.openrouter_use_fallback ?? default_settings.openrouter_use_fallback; oai_settings.openrouter_allow_fallbacks = settings.openrouter_allow_fallbacks ?? default_settings.openrouter_allow_fallbacks; + oai_settings.openrouter_middleout = settings.openrouter_middleout ?? default_settings.openrouter_middleout; oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model; oai_settings.mistralai_model = settings.mistralai_model ?? default_settings.mistralai_model; oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model; @@ -3130,6 +3140,7 @@ function loadOpenAISettings(data, settings) { $('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models); $('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks); $('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change'); + $('#openrouter_middleout').val(oai_settings.openrouter_middleout); $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); $('#continue_prefill').prop('checked', oai_settings.continue_prefill); $('#openai_function_calling').prop('checked', oai_settings.function_calling); @@ -3370,6 +3381,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { openrouter_sort_models: settings.openrouter_sort_models, openrouter_providers: settings.openrouter_providers, openrouter_allow_fallbacks: settings.openrouter_allow_fallbacks, + openrouter_middleout: settings.openrouter_middleout, ai21_model: settings.ai21_model, mistralai_model: settings.mistralai_model, cohere_model: settings.cohere_model, @@ -3836,6 +3848,7 @@ function onSettingsPresetChange() { openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false], openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false], openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true], + openrouter_middleout: ['#openrouter_middleout', 'openrouter_middleout', false], ai21_model: ['#model_ai21_select', 'ai21_model', false], mistralai_model: ['#model_mistralai_select', 'mistralai_model', false], cohere_model: ['#model_cohere_select', 'cohere_model', false], @@ -5261,6 +5274,11 @@ export function initOpenAI() { saveSettingsDebounced(); }); + $('#openrouter_middleout').on('input', function () { + oai_settings.openrouter_middleout = String($(this).val()); + saveSettingsDebounced(); + }); + $('#squash_system_messages').on('input', function () { oai_settings.squash_system_messages = !!$(this).prop('checked'); saveSettingsDebounced(); diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 64e6ab345..4e327bd5b 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -71,6 +71,22 @@ function postProcessPrompt(messages, type, names) { } } +/** + * Gets OpenRouter transforms based on the request. + * @param {import('express').Request} request Express request + * @returns {string[] | undefined} OpenRouter transforms + */ +function getOpenRouterTransforms(request) { + switch (request.body.middleout) { + case 'on': + return ['middle-out']; + case 'off': + return []; + case 'auto': + return undefined; + } +} + /** * Sends a request to Claude API. * @param {express.Request} request Express request @@ -834,7 +850,9 @@ router.post('/generate', jsonParser, function (request, response) { apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER); // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests headers = { ...OPENROUTER_HEADERS }; - bodyParams = { 'transforms': ['middle-out'] }; + bodyParams = { + 'transforms': getOpenRouterTransforms(request), + }; if (request.body.min_p !== undefined) { bodyParams['min_p'] = request.body.min_p; From 540d93592b46df0a3e8759610affcdf9d9591f10 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 24 Dec 2024 22:15:33 +0200 Subject: [PATCH 126/222] Add fallback mechanism for background selection in autoBackgroundCommand --- public/scripts/backgrounds.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/scripts/backgrounds.js b/public/scripts/backgrounds.js index 7b25728ef..8d521094b 100644 --- a/public/scripts/backgrounds.js +++ b/public/scripts/backgrounds.js @@ -333,6 +333,14 @@ async function autoBackgroundCommand() { const bestMatch = fuse.search(reply, { limit: 1 }); if (bestMatch.length == 0) { + for (const option of options) { + if (String(reply).toLowerCase().includes(option.text.toLowerCase())) { + console.debug('Fallback choosing background:', option); + option.element.click(); + return ''; + } + } + toastr.warning('No match found. Please try again.'); return ''; } From 0b43717931a9ad6f7c0a00c3fbca6a5d9a1c0867 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:32:31 +0200 Subject: [PATCH 127/222] Add background image fitting options Closes #3133 --- public/index.html | 7 +++++++ public/scripts/backgrounds.js | 23 ++++++++++++++++++++++ public/style.css | 36 ++++++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 7ea940004..0fdf96ea6 100644 --- a/public/index.html +++ b/public/index.html @@ -4713,6 +4713,13 @@ Background Image + -
    +
    Presence Penalty
    @@ -734,7 +734,7 @@
    -
    +
    Top P
    @@ -2679,6 +2679,7 @@ + @@ -3192,6 +3193,23 @@
    +
    +

    DeepSeek API Key

    +
    + + +
    +
    + For privacy reasons, your API key will be hidden after you reload the page. +
    +
    +

    DeepSeek Model

    + +
    +

    Perplexity API Key

    @@ -3762,6 +3780,7 @@ +
    diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 40f87ddf1..76a64e518 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -390,6 +390,7 @@ function RA_autoconnect(PrevApi) { || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI) || (secret_state[SECRET_KEYS.BLOCKENTROPY] && oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY) || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) + || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) ) { $('#api_button_openai').trigger('click'); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 4bb56618f..9c0b06189 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -183,6 +183,7 @@ export const chat_completion_sources = { ZEROONEAI: '01ai', BLOCKENTROPY: 'blockentropy', NANOGPT: 'nanogpt', + DEEPSEEK: 'deepseek', }; const character_names_behavior = { @@ -261,6 +262,7 @@ const default_settings = { nanogpt_model: 'gpt-4o-mini', zerooneai_model: 'yi-large', blockentropy_model: 'be-70b-base-llama3.1', + deepseek_model: 'deepseek-chat', custom_model: '', custom_url: '', custom_include_body: '', @@ -339,6 +341,7 @@ const oai_settings = { nanogpt_model: 'gpt-4o-mini', zerooneai_model: 'yi-large', blockentropy_model: 'be-70b-base-llama3.1', + deepseek_model: 'deepseek-chat', custom_model: '', custom_url: '', custom_include_body: '', @@ -1523,6 +1526,8 @@ function getChatCompletionModel() { return oai_settings.blockentropy_model; case chat_completion_sources.NANOGPT: return oai_settings.nanogpt_model; + case chat_completion_sources.DEEPSEEK: + return oai_settings.deepseek_model; default: throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`); } @@ -1698,6 +1703,24 @@ function saveModelList(data) { $('#model_nanogpt_select').val(oai_settings.nanogpt_model).trigger('change'); } + + if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) { + $('#model_deepseek_select').empty(); + model_list.forEach((model) => { + $('#model_deepseek_select').append( + $('