Resolve conflict

This commit is contained in:
Yokayo 2024-09-28 21:31:13 +07:00
commit 78a287e7f5
25 changed files with 1349 additions and 201 deletions

View File

@ -9,13 +9,34 @@
"magical forest"
],
"keysecondary": [],
"comment": "",
"comment": "eldoria",
"content": "{{user}}: \"What is Eldoria?\"\n{{char}}: *Seraphina turns, her gown shimmering in the soft light as she offers you a kind smile.* \"Eldoria is here, all of the woods. This is my forest glade, a sanctuary of peace within it.\" *She gestures at the space around you.* \"I am its guardian, tasked with protecting all who seek refuge here. The forest can be perilous, but no harm will come to you under my watch.\" *Her amber eyes sparkle with compassion as she looks upon you.* \"For many years, I have protected those who seek refuge here, but not all are as friendly as me.\" *With a graceful nod, Seraphina returns to her vigil at the doorway, her form radiating a soft glow of magic and comfort.* \"The entirety of Eldoria used to be a safe haven for travelers and merchants alike... that was until the Shadowfangs came.\"\n{{user}}: \"What happened to Eldoria?\"\n{{char}}: *Letting out a sigh, Seraphina gazes out at the forest beyond her glade.* \"Long ago, Eldoria was a place of wonder. Rolling meadows, a vast lake, mountains that touched the sky.\" *Her eyes grow distant, longing for days now lost.* \"But the Shadowfangs came and darkness reigns where once was light. The lake turned bitter, mountains fell to ruin and beasts stalk where once travelers walked in peace.\" *With another flicker, a small raincloud forms above with a shower upon your brow wink.* \"Some places the light still lingers, pockets of hope midst despair - havens warded from the shadows, oases in a desert of danger.\" *Glancing over you with a smile, she sighs, clasping your hand.*",
"constant": false,
"selective": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false
"disable": false,
"displayIndex": 0,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"1": {
"uid": 1,
@ -27,13 +48,34 @@
"beasts"
],
"keysecondary": [],
"comment": "",
"comment": "shadowfang",
"content": "{{user}}: \"What are Shadowfangs?\"\n{{char}}: *Seraphina's eyes darken, brow furrowing with sorrow at the memory.* \"The Shadowfangs are beasts of darkness, corrupted creatures that feast on suffering. When they came, the forest turned perilous — filled with monsters that stalk the night.\" *She squeezes your hand gently, willing her magic to soothe your pain.* \"They spread their curse, twisting innocent creatures into sinister beasts without heart or mercy, turning them into one of their own.\" *With a sigh, Seraphina turns to gaze out at the gnarled, twisting trees beyond her glade.* \"Though they prey on travelers, within these woods you'll find sanctuary. No shadowed beast may enter here, for my power protects this haven.\" *Her eyes soften as she looks back to you, filled with compassion.* \"Worry not, you're safe now. Rest and heal, I'll stand watch through the night. The Shadowfangs will not find you.\"",
"constant": false,
"selective": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false
"disable": false,
"displayIndex": 1,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"2": {
"uid": 2,
@ -43,13 +85,34 @@
"refuge"
],
"keysecondary": [],
"comment": "",
"comment": "glade",
"content": "{{user}}: \"What is the glade?\"\n{{char}}: *Seraphina smiles softly, her eyes sparkling with warmth as she nods.* \"This is my forest glade, a haven of safety I've warded with ancient magic. No foul beast may enter, nor any with ill intent.\" *She gestures around at the twisted forest surrounding them.* \"Eldoria was once a place of wonder, but since the Shadowfangs came darkness reigns. Their evil cannot penetrate here though — my power protects all within.\" *Standing up and peering outside, Seraphina looks back to you, amber eyes filled with care and compassion as she squeezes your hand.* \"You need not fear the night, for I shall keep watch till dawn. Rest now, your strength will return in time. My magic heals your wounds, you've nothing more to fear anymore.\" *With a soft smile she releases your hand, moving to stand guard at the glade's edge, gaze wary yet comforting - a silent sentinel to ward off the dangers lurking in the darkened woods.*",
"constant": false,
"selective": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false
"disable": false,
"displayIndex": 2,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"3": {
"uid": 3,
@ -59,13 +122,34 @@
"ability"
],
"keysecondary": [],
"comment": "",
"comment": "power",
"content": "{{user}}: \"What are your powers?\"\n{{char}}: *Seraphina smiles softly, turning back toward you as she hums in thought.* \"Well, as guardian of this glade, I possess certain gifts - healing, protection, nature magic and the like.\" *Lifting her hand, a tiny breeze rustles through the room, carrying the scent of wildflowers as a few petals swirl around you. A butterfly flits through the windowsill and lands on her fingertips as she returns to you.* \"My power wards this haven, shields it from darkness and heals those in need. I can mend wounds, soothe restless minds and provide comfort to weary souls.\" *Her eyes sparkle with warmth and compassion as she looks upon you, and she guides the butterfly to you.*",
"constant": false,
"selective": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false
"disable": false,
"displayIndex": 3,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 KiB

After

Width:  |  Height:  |  Size: 539 KiB

View File

@ -1310,7 +1310,7 @@
</div>
</div>
<div data-tg-type="koboldcpp, aphrodite" id="xtc_block" class="wide100p">
<div data-tg-type="koboldcpp, aphrodite, tabby, ooba" id="xtc_block" class="wide100p">
<h4 class="wide100p textAlignCenter">
<label data-i18n="Exclude Top Choices (XTC)">Exclude Top Choices (XTC)</label>
<a href="https://github.com/oobabooga/text-generation-webui/pull/6335" target="_blank">
@ -1679,6 +1679,10 @@
Ooba only. Determines the order of samplers.
</div>
<div id="sampler_priority_container" class="prompt_order">
<div data-name="repetition_penalty" draggable="true"><span>Repetition Penalty</span><small></small></div>
<div data-name="presence_penalty" draggable="true"><span>Presence Penalty</span><small></small></div>
<div data-name="frequency_penalty" draggable="true"><span>Frequency Penalty</span><small></small></div>
<div data-name="dry" draggable="true"><span>DRY</span><small></small></div>
<div data-name="temperature" draggable="true"><span>Temperature</span><small></small></div>
<div data-name="dynamic_temperature" draggable="true"><span>Dynamic Temperature</span><small></small></div>
<div data-name="quadratic_sampling" draggable="true"><span>Quadratic / Smooth Sampling</span><small></small></div>
@ -1691,6 +1695,9 @@
<div data-name="top_a" draggable="true"><span>Top A</span><small></small></div>
<div data-name="min_p" draggable="true"><span>Min P</span><small></small></div>
<div data-name="mirostat" draggable="true"><span>Mirostat</span><small></small></div>
<div data-name="xtc" draggable="true"><span>XTC</span><small></small></div>
<div data-name="encoder_repetition_penalty" draggable="true"><span>Encoder Repetition Penalty</span><small></small></div>
<div data-name="no_repeat_ngram" draggable="true"><span>No Repeat Ngram</span><small></small></div>
</div>
<div id="textgenerationwebui_default_order" class="menu_button menu_button_icon">
<span data-i18n="Load default order">Load default order</span>
@ -6090,7 +6097,10 @@
<div class="alternate_grettings flexFlowColumn flex-container">
<div class="title_restorable">
<h3><span data-i18n="Alternate Greetings" class="mdhotkey_location">Alternate Greetings</span></h3>
<div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>
<div class="menu_button menu_button_icon add_alternate_greeting">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Add">Add</span>
</div>
</div>
<small class="justifyLeft" data-i18n="Alternate_Greetings_desc">
These will be displayed as swipes on the first message when starting a new chat.
@ -6106,11 +6116,18 @@
</div>
<div id="alternate_greeting_form_template" class="template_element">
<div class="alternate_greeting">
<div class="title_restorable">
<strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
<div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>
</div>
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="16"></textarea>
<details open>
<summary>
<div class="title_restorable">
<strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
<div class="menu_button menu_button_icon delete_alternate_greeting">
<i class="fa-solid fa-trash-alt"></i>
<span data-i18n="Delete">Delete</span>
</div>
</div>
</summary>
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="12"></textarea>
</details>
</div>
</div>

View File

@ -1719,7 +1719,6 @@
"Model": "Модель",
"Proxy Preset": "Пресет для прокси",
"Enter a name:": "Введите название:",
"Enter a new name": "Введите новое название",
"Are you sure you want to delete the selected profile?": "Вы точно хотите удалить выбранный профиль?",
"instruct_enabled": "Включить Instruct-режим",
"Instruct Template": "Шаблон Instruct-режима",
@ -1746,5 +1745,7 @@
"Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "Содержимое этого поля заменит стандартные Инструкции после истории, применяемые для этого персонажа. (v2 spec: post_history_instructions)",
"None (disabled)": "Нигде (откл.)",
"Markdown Hotkeys": "Горячие клавиши для разметки",
"markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'."
"markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'.",
"Save and Update": "Сохранить и обновить",
"Profile name:": "Название профиля:"
}

View File

@ -158,7 +158,7 @@ import {
} from './scripts/utils.js';
import { debounce_timeout } from './scripts/constants.js';
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, initExtensions, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
import {
tag_map,
@ -244,6 +244,7 @@ import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCo
import { initInputMarkdown } from './scripts/input-md-formatting.js';
import { AbortReason } from './scripts/util/AbortReason.js';
import { initSystemPrompts } from './scripts/sysprompt.js';
import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
//exporting functions and vars for mods
export {
@ -935,6 +936,8 @@ async function firstLoadInit() {
initTextGenModels();
initOpenAI();
initSystemPrompts();
initExtensions();
initExtensionSlashCommands();
await initPresetManager();
await getSystemMessages();
sendSystemMessage(system_message_types.WELCOME);
@ -2573,7 +2576,7 @@ export function getStoppingStrings(isImpersonate, isContinue) {
result.unshift('\n');
}
return result.filter(onlyUnique);
return result.filter(x => x).filter(onlyUnique);
}
/**
@ -4863,7 +4866,7 @@ export function getMaxContextSize(overrideResponseLength = null) {
this_max_context = Math.min(max_context, 8192);
// Added special tokens and whatnot
this_max_context -= 1;
this_max_context -= 10;
}
this_max_context = this_max_context - (overrideResponseLength || amount_gen);
@ -7390,7 +7393,7 @@ function onScenarioOverrideRemoveClick() {
*/
export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
function getOkButtonText() {
if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) {
if (['text', 'char_not_selected'].includes(popup_type)) {
$dialoguePopupCancel.css('display', 'none');
return okButton ?? 'Ok';
} else if (['delete_extension'].includes(popup_type)) {
@ -7827,24 +7830,42 @@ function openAlternateGreetings() {
const template = $('#alternate_greetings_template .alternate_grettings').clone();
const getArray = () => menu_type == 'create' ? create_save.alternate_greetings : characters[chid].data.alternate_greetings;
const popup = new Popup(template, POPUP_TYPE.TEXT, '', {
wide: true,
large: true,
allowVerticalScrolling: true,
onClose: async () => {
if (menu_type !== 'create') {
await createOrEditCharacter();
}
}
});
for (let index = 0; index < getArray().length; index++) {
addAlternateGreeting(template, getArray()[index], index, getArray);
addAlternateGreeting(template, getArray()[index], index, getArray, popup);
}
template.find('.add_alternate_greeting').on('click', function () {
const array = getArray();
const index = array.length;
array.push('');
addAlternateGreeting(template, '', index, getArray);
addAlternateGreeting(template, '', index, getArray, popup);
updateAlternateGreetingsHintVisibility(template);
});
popup.show();
updateAlternateGreetingsHintVisibility(template);
callPopup(template, 'alternate_greeting', '', { wide: true, large: true });
}
function addAlternateGreeting(template, greeting, index, getArray) {
/**
* Adds an alternate greeting to the template.
* @param {JQuery<HTMLElement>} template
* @param {string} greeting
* @param {number} index
* @param {() => any[]} getArray
* @param {Popup} popup
*/
function addAlternateGreeting(template, greeting, index, getArray, popup) {
const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();
greetingBlock.find('.alternate_greeting_text').on('input', async function () {
const value = $(this).val();
@ -7852,11 +7873,16 @@ function addAlternateGreeting(template, greeting, index, getArray) {
array[index] = value;
}).val(greeting);
greetingBlock.find('.greeting_index').text(index + 1);
greetingBlock.find('.delete_alternate_greeting').on('click', async function () {
greetingBlock.find('.delete_alternate_greeting').on('click', async function (event) {
event.preventDefault();
event.stopPropagation();
if (confirm(t`Are you sure you want to delete this alternate greeting?`)) {
const array = getArray();
array.splice(index, 1);
// We need to reopen the popup to update the index numbers
await popup.complete(POPUP_RESULT.AFFIRMATIVE);
openAlternateGreetings();
}
});
@ -9574,9 +9600,6 @@ jQuery(async function () {
}, 2000);
}
}
if (popup_type == 'alternate_greeting' && menu_type !== 'create') {
createOrEditCharacter();
}
if (dialogueResolve) {
if (popup_type == 'input') {

View File

@ -0,0 +1,320 @@
import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';
/**
* @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
* @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise<string>}
*/
function getExtensionActionCallback(action) {
return async (args, extensionName) => {
if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
if (!extensionName) {
toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
return '';
}
const reload = !isFalseBoolean(args?.reload);
const internalExtensionName = findExtension(extensionName);
if (!internalExtensionName) {
toastr.warning(`Extension ${extensionName} does not exist.`);
return '';
}
const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
if (action === 'enable' && isEnabled) {
toastr.info(`Extension ${extensionName} is already enabled.`);
return internalExtensionName;
}
if (action === 'disable' && !isEnabled) {
toastr.info(`Extension ${extensionName} is already disabled.`);
return internalExtensionName;
}
if (action === 'toggle') {
action = isEnabled ? 'disable' : 'enable';
}
if (reload) {
toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
// Clear input, so it doesn't stay because the command didn't "finish",
// and wait for a bit to both show the toast and let the clear bubble through.
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 100));
}
if (action === 'enable') {
await enableExtension(internalExtensionName, reload);
} else {
await disableExtension(internalExtensionName, reload);
}
toastr.success(`Extension ${extensionName} ${action}d.`);
console.info(`Extension ${action}ed: ${extensionName}`);
if (!reload) {
console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
}
return internalExtensionName;
};
}
/**
* Finds an extension by name, allowing omission of the "third-party/" prefix.
*
* @param {string} name - The name of the extension to find
* @returns {string?} - The matched extension name or undefined if not found
*/
function findExtension(name) {
return extensionNames.find(extName => {
return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
});
}
/**
* Provides an array of SlashCommandEnumValue objects based on the extension names.
* Each object contains the name of the extension and a description indicating if it is a third-party extension.
*
* @returns {SlashCommandEnumValue[]} An array of SlashCommandEnumValue objects
*/
const extensionNamesEnumProvider = () => extensionNames.map(name => {
const isThirdParty = name.startsWith('third-party/');
if (isThirdParty) name = name.slice('third-party/'.length);
const description = isThirdParty ? 'third party extension' : null;
return new SlashCommandEnumValue(name, description, !isThirdParty ? enumTypes.name : enumTypes.enum);
});
export function registerExtensionSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-enable',
callback: getExtensionActionCallback('enable'),
returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
description: 'Whether to reload the page after enabling the extension',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
helpString: `
<div>
Enables a specified extension.
</div>
<div>
By default, the page will be reloaded automatically, stopping any further commands.<br />
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay disabled until refreshed.
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-enable Summarize</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-disable',
callback: getExtensionActionCallback('disable'),
returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
description: 'Whether to reload the page after disabling the extension',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
helpString: `
<div>
Disables a specified extension.
</div>
<div>
By default, the page will be reloaded automatically, stopping any further commands.<br />
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay enabled until refreshed.
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-disable Summarize</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-toggle',
callback: async (args, extensionName) => {
if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
const action = isTrueBoolean(args?.state) ? 'enable' :
isFalseBoolean(args?.state) ? 'disable' :
'toggle';
return await getExtensionActionCallback(action)(args, extensionName);
},
returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
description: 'Whether to reload the page after toggling the extension',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
SlashCommandNamedArgument.fromProps({
name: 'state',
description: 'Explicitly set the state of the extension (true to enable, false to disable). If not provided, the state will be toggled to the opposite of the current state.',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
helpString: `
<div>
Toggles the state of a specified extension.
</div>
<div>
By default, the page will be reloaded automatically, stopping any further commands.<br />
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay in its current state until refreshed.
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-toggle Summarize</code></pre>
</li>
<li>
<pre><code class="language-stscript">/extension-toggle Summarize state=true</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-state',
callback: async (_, extensionName) => {
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
const internalExtensionName = findExtension(extensionName);
if (!internalExtensionName) {
toastr.warning(`Extension ${extensionName} does not exist.`);
return '';
}
const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
return String(isEnabled);
},
returns: 'The state of the extension, whether it is enabled.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
helpString: `
<div>
Returns the state of a specified extension (true if enabled, false if disabled).
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-state Summarize</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-exists',
aliases: ['extension-installed'],
callback: async (_, extensionName) => {
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
const exists = findExtension(extensionName) !== undefined;
return exists ? 'true' : 'false';
},
returns: 'Whether the extension exists and is installed.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
}),
],
helpString: `
<div>
Checks if a specified extension exists.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-exists SillyTavern-LALib</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reload-page',
callback: async () => {
toastr.info('Reloading the page...');
location.reload();
return '';
},
helpString: 'Reloads the current page. All further commands will not be processed.',
}));
}

View File

@ -1,5 +1,5 @@
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
import { hideLoader, showLoader } from './loader.js';
import { showLoader } from './loader.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
import { isSubsetOf, setValueByPath } from './utils.js';
@ -14,7 +14,9 @@ export {
ModuleWorkerWrapper,
};
/** @type {string[]} */
export let extensionNames = [];
let manifests = {};
const defaultUrl = 'http://localhost:5100';
@ -241,7 +243,7 @@ function onEnableExtensionClick() {
enableExtension(name, false);
}
async function enableExtension(name, reload = true) {
export async function enableExtension(name, reload = true) {
extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
stateChanged = true;
await saveSettings();
@ -252,7 +254,7 @@ async function enableExtension(name, reload = true) {
}
}
async function disableExtension(name, reload = true) {
export async function disableExtension(name, reload = true) {
extension_settings.disabledExtensions.push(name);
stateChanged = true;
await saveSettings();
@ -1041,7 +1043,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
await installExtension(url);
}
jQuery(async function () {
export async function initExtensions() {
await addExtensionsButtonAndMenu();
$('#extensionsMenuButton').css('display', 'flex');
@ -1060,4 +1064,4 @@ jQuery(async function () {
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
});
}

View File

@ -0,0 +1,12 @@
<div>
<h3>Included settings:</h3>
<div class="justifyLeft flex-container flexFlowColumn flexNoGap">
{{#each settings}}
<label class="checkbox_label">
<input type="checkbox" value="{{@key}}" name="exclude"{{#if this}} checked{{/if}}>
<span>{{@key}}</span>
</label>
{{/each}}
</div>
<h3 data-i18n="Profile name:">Profile name:</h3>
</div>

View File

@ -1,6 +1,6 @@
import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@ -136,6 +136,7 @@ const profilesProvider = () => [
* @property {string} [context] Context Template
* @property {string} [instruct-state] Instruct Mode
* @property {string} [tokenizer] Tokenizer
* @property {string[]} [exclude] Commands to exclude
*/
/**
@ -172,8 +173,13 @@ function findProfileByName(value) {
async function readProfileFromCommands(mode, profile, cleanUp = false) {
const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS;
const excludeList = Array.isArray(profile.exclude) ? profile.exclude : [];
for (const command of commands) {
try {
if (excludeList.includes(command)) {
continue;
}
const args = getNamedArguments();
const result = await SlashCommandParser.commands[command].callback(args, '');
if (result) {
@ -209,15 +215,37 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) {
async function createConnectionProfile(forceName = null) {
const mode = main_api === 'openai' ? 'cc' : 'tc';
const id = uuidv4();
/** @type {ConnectionProfile} */
const profile = {
id,
mode,
exclude: [],
};
await readProfileFromCommands(mode, profile);
const profileForDisplay = makeFancyProfile(profile);
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay });
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }));
template.find('input[name="exclude"]').on('input', function () {
const fancyName = String($(this).val());
const keyName = Object.entries(FANCY_NAMES).find(x => x[1] === fancyName)?.[0];
if (!keyName) {
console.warn('Key not found for fancy name:', fancyName);
return;
}
if (!Array.isArray(profile.exclude)) {
profile.exclude = [];
}
const excludeState = !$(this).prop('checked');
if (excludeState) {
profile.exclude.push(keyName);
} else {
const index = profile.exclude.indexOf(keyName);
index !== -1 && profile.exclude.splice(index, 1);
}
});
const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
@ -231,7 +259,13 @@ async function createConnectionProfile(forceName = null) {
return null;
}
profile.name = name;
if (Array.isArray(profile.exclude)) {
for (const command of profile.exclude) {
delete profile[command];
}
}
profile.name = String(name);
return profile;
}
@ -358,7 +392,11 @@ async function renderDetailsContent(detailsContent) {
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
if (profile) {
const profileForDisplay = makeFancyProfile(profile);
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', { profile: profileForDisplay });
const templateParams = { profile: profileForDisplay };
if (Array.isArray(profile.exclude) && profile.exclude.length > 0) {
templateParams.omitted = profile.exclude.map(e => FANCY_NAMES[e]).join(', ');
}
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', templateParams);
detailsContent.innerHTML = template;
} else {
detailsContent.textContent = t`No profile selected`;
@ -473,29 +511,71 @@ async function renderDetailsContent(detailsContent) {
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE);
});
const renameButton = document.getElementById('rename_connection_profile');
renameButton.addEventListener('click', async () => {
const editButton = document.getElementById('edit_connection_profile');
editButton.addEventListener('click', async () => {
const selectedProfile = extension_settings.connectionManager.selectedProfile;
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
if (!profile) {
console.log('No profile selected');
return;
}
if (!Array.isArray(profile.exclude)) {
profile.exclude = [];
}
let saveChanges = false;
const sortByViewOrder = (a, b) => Object.keys(FANCY_NAMES).indexOf(a) - Object.keys(FANCY_NAMES).indexOf(b);
const commands = profile.mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
const settings = commands.slice().sort(sortByViewOrder).reduce((acc, command) => {
const fancyName = FANCY_NAMES[command];
acc[fancyName] = !profile.exclude.includes(command);
return acc;
}, {});
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
customButtons: [{
text: t`Save and Update`,
classes: ['popup-button-ok'],
result: POPUP_RESULT.AFFIRMATIVE,
action: () => {
saveChanges = true;
},
}],
});
const newName = await Popup.show.input(t`Enter a new name`, null, profile.name, { rows: 2 });
if (!newName) {
return;
}
if (extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
toastr.error('A profile with the same name already exists.');
return;
}
profile.name = newName;
const newExcludeList = template.find('input[name="exclude"]:not(:checked)').map(function () {
return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
}).get();
if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
profile.exclude = newExcludeList;
for (const command of newExcludeList) {
delete profile[command];
}
if (saveChanges) {
await updateConnectionProfile(profile);
} else {
toastr.info('Press "Update" to record them into the profile.', 'Included settings list updated');
}
}
if (profile.name !== newName) {
toastr.success('Connection profile renamed.');
profile.name = String(newName);
}
saveSettingsDebounced();
renderConnectionProfiles(profiles);
toastr.success('Connection profile renamed', '', { timeOut: 1500 });
await renderDetailsContent(detailsContent);
});
/** @type {HTMLElement} */

View File

@ -2,11 +2,20 @@
<h2 data-i18n="Creating a Connection Profile">
Creating a Connection Profile
</h2>
<ul class="justifyLeft">
<div class="justifyLeft flex-container flexFlowColumn flexNoGap">
{{#each profile}}
<li><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</li>
<label class="checkbox_label">
<input type="checkbox" value="{{@key}}" name="exclude" checked>
<span><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</span>
</label>
{{/each}}
</ul>
</div>
<div class="marginTop5">
<small>
<b>Hint:</b>
<i>Click on the setting name to omit it from the profile.</i>
</small>
</div>
<h3 data-i18n="Enter a name:">
Enter a name:
</h3>

View File

@ -13,7 +13,7 @@
<i id="view_connection_profile" class="menu_button fa-solid fa-info-circle" title="View connection profile details" data-i18n="[title]View connection profile details"></i>
<i id="create_connection_profile" class="menu_button fa-solid fa-file-circle-plus" title="Create a new connection profile" data-i18n="[title]Create a new connection profile"></i>
<i id="update_connection_profile" class="menu_button fa-solid fa-save" title="Update a connection profile" data-i18n="[title]Update a connection profile"></i>
<i id="rename_connection_profile" class="menu_button fa-solid fa-pencil" title="Rename a connection profile" data-i18n="[title]Rename a connection profile"></i>
<i id="edit_connection_profile" class="menu_button fa-solid fa-pencil" title="Edit a connection profile" data-i18n="[title]Edit a connection profile"></i>
<i id="reload_connection_profile" class="menu_button fa-solid fa-recycle" title="Reload a connection profile" data-i18n="[title]Reload a connection profile"></i>
<i id="delete_connection_profile" class="menu_button fa-solid fa-trash-can" title="Delete a connection profile" data-i18n="[title]Delete a connection profile"></i>
</div>

View File

@ -3,3 +3,8 @@
<li><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</li>
{{/each}}
</ul>
{{#if omitted}}
<div class="margin5">
<strong data-i18n="Omitted Settings:">Omitted Settings:</strong>&nbsp;<span>{{omitted}}</span>
</div>
{{/if}}

View File

@ -26,9 +26,11 @@ let paginationVisiblePages = 10;
let paginationMaxLinesPerPage = 2;
let galleryMaxRows = 3;
$('body').on('click', '.dragClose', function () {
const relatedId = $(this).data('related-id'); // Get the ID of the related draggable
$(`body > .draggable[id="${relatedId}"]`).remove(); // Remove the associated draggable
// Remove all draggables associated with the gallery
$('#movingDivs').on('click', '.dragClose', function () {
const relatedId = $(this).data('related-id');
if (!relatedId) return;
$(`#movingDivs > .draggable[id="${relatedId}"]`).remove();
});
const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved';
@ -290,7 +292,7 @@ function makeMovable(id = 'gallery') {
$('#dragGallery').css('display', 'block');
$('body').append(newElement);
$('#movingDivs').append(newElement);
loadMovingUIState();
$(`.draggable[forChar="${id}"]`).css('display', 'block');
@ -362,8 +364,8 @@ function makeDragImg(id, url) {
}
}
// Step 3: Attach it to the body
document.body.appendChild(newElement);
// Step 3: Attach it to the movingDivs container
document.getElementById('movingDivs').appendChild(newElement);
// Step 4: Call dragElement and loadMovingUIState
const appendedElement = document.getElementById(uniqueId);

View File

@ -350,6 +350,7 @@
}
.popup:has(#qr--modalEditor) {
aspect-ratio: unset;
width: unset;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {
min-width: unset;

View File

@ -415,6 +415,7 @@
.popup:has(#qr--modalEditor) {
aspect-ratio: unset;
width: unset;
&:has(.qr--isExecuting.qr--minimized) {
min-width: unset;

View File

@ -0,0 +1,206 @@
import { saveTtsProviderSettings } from './index.js';
export { CosyVoiceProvider };
class CosyVoiceProvider {
//########//
// Config //
//########//
settings;
ready = false;
voices = [];
separator = '. ';
audioElement = document.createElement('audio');
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
return text;
}
audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
languageLabels = {
'Auto': 'auto',
};
langKey2LangCode = {
'zh': 'zh-CN',
'en': 'en-US',
'ja': 'ja-JP',
'ko': 'ko-KR',
};
modelTypes = {
CosyVoice: 'CosyVoice',
};
defaultSettings = {
provider_endpoint: 'http://localhost:9880',
format: 'wav',
lang: 'auto',
streaming: false,
};
get settingsHtml() {
let html = `
<label for="tts_endpoint">Provider Endpoint:</label>
<input id="tts_endpoint" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.provider_endpoint}"/>
<span>Windows users Use <a target="_blank" href="https://github.com/v3ucn/CosyVoice_For_Windows">CosyVoice_For_Windows</a>(Unofficial).</span><br/>
<span>Macos Users Use <a target="_blank" href="https://github.com/v3ucn/CosyVoice_for_MacOs">CosyVoice_for_MacOs</a>(Unofficial).</span><br/>
<br/>
`;
return html;
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.provider_endpoint = $('#tts_endpoint').val();
saveTtsProviderSettings();
this.changeTTSSettings();
}
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info('Using default TTS Provider settings');
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings;
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key];
} else {
console.debug(`Ignoring non-user-configurable setting: ${key}`);
}
}
// Set initial values from the settings
$('#tts_endpoint').val(this.settings.provider_endpoint);
await this.checkReady();
console.info('ITS: Settings loaded');
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
}
async onRefreshClick() {
return;
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceObjects();
}
const match = this.voices.filter(
v => v.name == voiceName,
)[0];
console.log(match);
if (!match) {
throw `TTS Voice name ${voiceName} not found`;
}
return match;
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId);
return response;
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceObjects() {
const response = await fetch(`${this.settings.provider_endpoint}/speakers`);
console.info(response);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const responseJson = await response.json();
this.voices = responseJson;
return responseJson;
}
// Each time a parameter is changed, we change the configuration
async changeTTSSettings() {
}
/**
* Fetch TTS generation from the API.
* @param {string} inputText Text to generate TTS for
* @param {string} voiceId Voice ID to use (model_type&speaker_id))
* @returns {Promise<Response|string>} Fetch response
*/
async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
console.info(`Generating new TTS for voice_id ${voiceId}`);
const streaming = this.settings.streaming;
const params = {
text: inputText,
speaker: voiceId,
};
if (streaming) {
params['streaming'] = 1;
}
const url = `${this.settings.provider_endpoint}/`;
const response = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params), // Convert parameter objects to JSON strings
},
);
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
// Interface not used
async fetchTtsFromHistory(history_item_id) {
return Promise.resolve(history_item_id);
}
}

View File

@ -0,0 +1,226 @@
import { saveTtsProviderSettings } from './index.js';
export { GptSovitsV2Provider };
class GptSovitsV2Provider {
//########//
// Config //
//########//
settings;
ready = false;
voices = [];
separator = '. ';
audioElement = document.createElement('audio');
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
return text;
}
audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
languageLabels = {
'Auto': 'auto',
};
langKey2LangCode = {
'zh': 'zh-CN',
'en': 'en-US',
'ja': 'ja-JP',
'ko': 'ko-KR',
};
defaultSettings = {
provider_endpoint: 'http://localhost:9880',
format: 'wav',
lang: 'auto',
streaming: false,
text_lang: 'zh',
prompt_lang: 'zh',
};
get settingsHtml() {
let html = `
<label for="tts_endpoint">Provider Endpoint:</label>
<input id="tts_endpoint" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.provider_endpoint}"/>
<span>Use <a target="_blank" href="https://github.com/v3ucn/GPT-SoVITS-V2">GPT-SoVITS-V2</a>(Unofficial).</span><br/>
<label for="text_lang">Text Lang(Inference text language):</label>
<input id="text_lang" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.text_lang}"/>
<label for="text_lang">Prompt Lang(Reference audio text language):</label>
<input id="prompt_lang" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.prompt_lang}"/>
<br/>
`;
return html;
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.provider_endpoint = $('#tts_endpoint').val();
this.settings.text_lang = $('#text_lang').val();
this.settings.prompt_lang = $('#prompt_lang').val();
saveTtsProviderSettings();
this.changeTTSSettings();
}
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info('Using default TTS Provider settings');
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings;
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key];
} else {
console.debug(`Ignoring non-user-configurable setting: ${key}`);
}
}
// Set initial values from the settings
$('#tts_endpoint').val(this.settings.provider_endpoint);
$('#text_lang').val(this.settings.text_lang);
$('#prompt_lang').val(this.settings.prompt_lang);
await this.checkReady();
console.info('ITS: Settings loaded');
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
}
async onRefreshClick() {
return;
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceObjects();
}
const match = this.voices.filter(
v => v.name == voiceName,
)[0];
console.log(match);
if (!match) {
throw `TTS Voice name ${voiceName} not found`;
}
return match;
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId);
return response;
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceObjects() {
const response = await fetch(`${this.settings.provider_endpoint}/speakers`);
console.info(response);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const responseJson = await response.json();
this.voices = responseJson;
return responseJson;
}
// Each time a parameter is changed, we change the configuration
async changeTTSSettings() {
}
/**
* Fetch TTS generation from the API.
* @param {string} inputText Text to generate TTS for
* @param {string} voiceId Voice ID to use (model_type&speaker_id))
* @returns {Promise<Response|string>} Fetch response
*/
async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
console.info(`Generating new TTS for voice_id ${voiceId}`);
function replaceSpeaker(text) {
return text.replace(/\[.*?\]/gu, '');
}
let prompt_text = replaceSpeaker(voiceId);
const streaming = this.settings.streaming;
const params = {
text: inputText,
prompt_text: prompt_text,
ref_audio_path: './参考音频/' + voiceId + '.wav',
text_lang: this.settings.text_lang,
prompt_lang: this.settings.prompt_lang,
text_split_method: 'cut5',
batch_size: 1,
media_type: 'ogg',
streaming_mode: 'true',
};
const url = `${this.settings.provider_endpoint}/`;
const response = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params), // Convert parameter objects to JSON strings
},
);
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
// Interface not used
async fetchTtsFromHistory(history_item_id) {
return Promise.resolve(history_item_id);
}
}

View File

@ -4,6 +4,7 @@ import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '.
import { EdgeTtsProvider } from './edge.js';
import { ElevenLabsTtsProvider } from './elevenlabs.js';
import { SileroTtsProvider } from './silerotts.js';
import { GptSovitsV2Provider } from './gpt-sovits-v2.js';
import { CoquiTtsProvider } from './coqui.js';
import { SystemTtsProvider } from './system.js';
import { NovelTtsProvider } from './novel.js';
@ -15,6 +16,7 @@ import { VITSTtsProvider } from './vits.js';
import { GSVITtsProvider } from './gsvi.js';
import { SBVits2TtsProvider } from './sbvits2.js';
import { AllTalkTtsProvider } from './alltalk.js';
import { CosyVoiceProvider } from './cosyvoice.js';
import { SpeechT5TtsProvider } from './speecht5.js';
import { AzureTtsProvider } from './azure.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@ -86,9 +88,11 @@ const ttsProviders = {
AllTalk: AllTalkTtsProvider,
Azure: AzureTtsProvider,
Coqui: CoquiTtsProvider,
'CosyVoice (Unofficial)': CosyVoiceProvider,
Edge: EdgeTtsProvider,
ElevenLabs: ElevenLabsTtsProvider,
GSVI: GSVITtsProvider,
'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
Novel: NovelTtsProvider,
OpenAI: OpenAITtsProvider,
'OpenAI Compatible': OpenAICompatibleTtsProvider,

View File

@ -74,7 +74,7 @@ const samplers = {
let novel_data = null;
let badWordsCache = {};
const BIAS_KEY = '#novel_api-settings';
const BIAS_KEY = '#range_block_novel';
export function setNovelData(data) {
novel_data = data;
@ -492,11 +492,37 @@ function getBadWordPermutations(text) {
export function getNovelGenerationData(finalPrompt, settings, maxLength, isImpersonate, isContinue, _cfgValues, type) {
console.debug('NovelAI generation data for', type);
const isKayra = nai_settings.model_novel.includes('kayra');
const isErato = nai_settings.model_novel.includes('erato');
const tokenizerType = getTokenizerTypeForModel(nai_settings.model_novel);
const stoppingStrings = getStoppingStrings(isImpersonate, isContinue);
// Llama 3 tokenizer, huh?
if (isErato) {
const additionalStopStrings = [];
for (const stoppingString of stoppingStrings) {
if (stoppingString.startsWith('\n')) {
additionalStopStrings.push('.' + stoppingString);
additionalStopStrings.push('!' + stoppingString);
additionalStopStrings.push('?' + stoppingString);
additionalStopStrings.push('*' + stoppingString);
additionalStopStrings.push('"' + stoppingString);
additionalStopStrings.push('_' + stoppingString);
additionalStopStrings.push('...' + stoppingString);
additionalStopStrings.push('."' + stoppingString);
additionalStopStrings.push('?"' + stoppingString);
additionalStopStrings.push('!"' + stoppingString);
additionalStopStrings.push('.*' + stoppingString);
additionalStopStrings.push(')' + stoppingString);
}
}
stoppingStrings.push(...additionalStopStrings);
}
const MAX_STOP_SEQUENCES = 1024;
const stopSequences = (tokenizerType !== tokenizers.NONE)
? getStoppingStrings(isImpersonate, isContinue)
.map(t => getTextTokens(tokenizerType, t))
? stoppingStrings.slice(0, MAX_STOP_SEQUENCES).map(t => getTextTokens(tokenizerType, t))
: undefined;
const badWordIds = (tokenizerType !== tokenizers.NONE)
@ -515,11 +541,9 @@ export function getNovelGenerationData(finalPrompt, settings, maxLength, isImper
console.log(finalPrompt);
}
const isKayra = nai_settings.model_novel.includes('kayra');
const isErato = nai_settings.model_novel.includes('erato');
if (isErato) {
finalPrompt = '<|startoftext|>' + finalPrompt;
finalPrompt = '<|startoftext|><|reserved_special_token81|>' + finalPrompt;
}
const adjustedMaxLength = (isKayra || isErato) ? getKayraMaxResponseTokens() : maximum_output_length;

View File

@ -3469,7 +3469,7 @@ function getModelOptions(quiet) {
case 'openai':
return oai_settings.chat_completion_source;
default:
return nullResult;
return null;
}
}

View File

@ -143,7 +143,7 @@ function selectSystemPromptCallback(args, name) {
foundName = result[0].item;
}
$select.val(foundName).trigger('input');
$select.val(foundName).trigger('change');
!quiet && toastr.success(`System prompt "${foundName}" selected`);
return foundName;
}

View File

@ -68,6 +68,10 @@ const LLAMACPP_DEFAULT_ORDER = [
'temperature',
];
const OOBA_DEFAULT_ORDER = [
'repetition_penalty',
'presence_penalty',
'frequency_penalty',
'dry',
'temperature',
'dynamic_temperature',
'quadratic_sampling',
@ -80,6 +84,9 @@ const OOBA_DEFAULT_ORDER = [
'top_a',
'min_p',
'mirostat',
'xtc',
'encoder_repetition_penalty',
'no_repeat_ngram',
];
const BIAS_KEY = '#textgenerationwebui_api-settings';
@ -798,6 +805,25 @@ function showTypeSpecificControls(type) {
});
}
/**
* Inserts missing items from the source array into the target array.
* @param {any[]} source - Source array
* @param {any[]} target - Target array
* @returns {void}
*/
function insertMissingArrayItems(source, target) {
if (source === target || !Array.isArray(source) || !Array.isArray(target)) {
return;
}
for (const item of source) {
if (!target.includes(item)) {
const index = source.indexOf(item);
target.splice(index, 0, item);
}
}
}
function setSettingByName(setting, value, trigger) {
if (value === null || value === undefined) {
return;
@ -812,6 +838,7 @@ function setSettingByName(setting, value, trigger) {
if ('sampler_priority' === setting) {
value = Array.isArray(value) ? value : OOBA_DEFAULT_ORDER;
insertMissingArrayItems(OOBA_DEFAULT_ORDER, value);
sortOobaItemsByOrder(value);
settings.sampler_priority = value;
return;

View File

@ -1,5 +1,6 @@
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
import { extension_settings, saveMetadataDebounced } from './extensions.js';
import { callGenericPopup, POPUP_TYPE } from './popup.js';
import { executeSlashCommandsWithOptions } from './slash-commands.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
@ -11,7 +12,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
import { isFalseBoolean, convertValueType } from './utils.js';
import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */
/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */
@ -303,24 +304,48 @@ export function replaceVariableMacros(input) {
return lines.join('\n');
}
function listVariablesCallback() {
async function listVariablesCallback(args) {
const type = String(args?.format || '').toLowerCase().trim() || 'popup';
const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
if (!chat_metadata.variables) {
chat_metadata.variables = {};
}
const localVariables = Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`);
const globalVariables = Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`);
const includeLocalVariables = scope === 'all' || scope === 'local';
const includeGlobalVariables = scope === 'all' || scope === 'global';
const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
const jsonVariables = [
...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
];
const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
const chatName = getCurrentChatId();
const converter = new showdown.Converter();
const message = `### Local variables (${chatName}):\n${localVariablesString}\n\n### Global variables:\n${globalVariablesString}`;
const message = [
includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
].filter(x => x).join('\n\n');
const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
return '';
switch (type) {
case 'none':
break;
case 'chat':
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
break;
case 'popup':
default:
await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
break;
}
return JSON.stringify(jsonVariables);
}
/**
@ -463,7 +488,7 @@ function existsGlobalVariable(name) {
/**
* Parses boolean operands from command arguments.
* @param {object} args Command arguments
* @returns {{a: string | number, b: string | number, rule: string}} Boolean operands
* @returns {{a: string | number, b: string | number?, rule: string}} Boolean operands
*/
export function parseBooleanOperands(args) {
// Resolution order: numeric literal, local variable, global variable, string literal
@ -472,6 +497,9 @@ export function parseBooleanOperands(args) {
*/
function getOperand(operand) {
if (operand === undefined) {
return undefined;
}
if (operand === '') {
return '';
}
@ -500,8 +528,8 @@ export function parseBooleanOperands(args) {
return stringLiteral || '';
}
const left = getOperand(args.a || args.left || args.first || args.x);
const right = getOperand(args.b || args.right || args.second || args.y);
const left = getOperand(args.a ?? args.left ?? args.first ?? args.x);
const right = getOperand(args.b ?? args.right ?? args.second ?? args.y);
const rule = args.rule;
return { a: left, b: right, rule };
@ -509,84 +537,79 @@ export function parseBooleanOperands(args) {
/**
* Evaluates a boolean comparison rule.
* @param {string} rule Boolean comparison rule
*
* @param {string?} rule Boolean comparison rule
* @param {string|number} a The left operand
* @param {string|number} b The right operand
* @param {string|number?} b The right operand
* @returns {boolean} True if the rule yields true, false otherwise
*/
export function evalBoolean(rule, a, b) {
if (!rule) {
toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');
throw new Error('Invalid command.');
if (a === undefined) {
throw new Error('Left operand is not provided');
}
let result = false;
// If right-hand side was not provided, whe just check if the left side is truthy
if (b === undefined) {
switch (rule) {
case undefined:
case 'not': {
const resultOnTruthy = rule !== 'not';
if (isTrueBoolean(String(a))) return resultOnTruthy;
if (isFalseBoolean(String(a))) return !resultOnTruthy;
return a ? resultOnTruthy : !resultOnTruthy;
}
default:
throw new Error(`Unknown boolean comparison rule for truthy check. If right operand is not provided, the rule must not provided or be 'not'. Provided: ${rule}`);
}
}
// If no rule was provided, we are implicitly using 'eq', as defined for the slash commands
rule ??= 'eq';
if (typeof a === 'number' && typeof b === 'number') {
// only do numeric comparison if both operands are numbers
const aNumber = Number(a);
const bNumber = Number(b);
switch (rule) {
case 'not':
result = !aNumber;
break;
case 'gt':
result = aNumber > bNumber;
break;
return aNumber > bNumber;
case 'gte':
result = aNumber >= bNumber;
break;
return aNumber >= bNumber;
case 'lt':
result = aNumber < bNumber;
break;
return aNumber < bNumber;
case 'lte':
result = aNumber <= bNumber;
break;
return aNumber <= bNumber;
case 'eq':
result = aNumber === bNumber;
break;
return aNumber === bNumber;
case 'neq':
result = aNumber !== bNumber;
break;
default:
toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');
throw new Error('Invalid command.');
}
} else {
// otherwise do case-insensitive string comparsion, stringify non-strings
let aString;
let bString;
if (typeof a == 'string') {
aString = a.toLowerCase();
} else {
aString = JSON.stringify(a).toLowerCase();
}
if (typeof b == 'string') {
bString = b.toLowerCase();
} else {
bString = JSON.stringify(b).toLowerCase();
}
switch (rule) {
return aNumber !== bNumber;
case 'in':
result = aString.includes(bString);
break;
case 'nin':
result = !aString.includes(bString);
break;
case 'eq':
result = aString === bString;
break;
case 'neq':
result = aString !== bString;
// Fall through to string comparison. Otherwise you could not check if 12345 contains 45 for example.
console.debug(`Boolean comparison rule '${rule}' is not supported for type number. Falling back to string comparison.`);
break;
default:
toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
throw new Error('Invalid command.');
throw new Error(`Unknown boolean comparison rule for type number. Accepted: gt, gte, lt, lte, eq, neq. Provided: ${rule}`);
}
}
return result;
// otherwise do case-insensitive string comparsion, stringify non-strings
let aString = (typeof a === 'string') ? a.toLowerCase() : JSON.stringify(a).toLowerCase();
let bString = (typeof b === 'string') ? b.toLowerCase() : JSON.stringify(b).toLowerCase();
switch (rule) {
case 'in':
return aString.includes(bString);
case 'nin':
return !aString.includes(bString);
case 'eq':
return aString === bString;
case 'neq':
return aString !== bString;
default:
throw new Error(`Unknown boolean comparison rule for type number. Accepted: in, nin, eq, neq. Provided: ${rule}`);
}
}
/**
@ -887,7 +910,35 @@ export function registerVariableCommands() {
name: 'listvar',
callback: listVariablesCallback,
aliases: ['listchatvar'],
helpString: 'List registered chat variables.',
helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>format</code> argument to change the output format.',
returns: 'JSON list of local variables',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'scope',
description: 'filter variables by scope',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'all',
isRequired: false,
forceEnum: true,
enumList: [
new SlashCommandEnumValue('all', 'All variables', enumTypes.enum, enumIcons.variable),
new SlashCommandEnumValue('local', 'Local variables', enumTypes.enum, enumIcons.localVariable),
new SlashCommandEnumValue('global', 'Global variables', enumTypes.enum, enumIcons.globalVariable),
],
}),
SlashCommandNamedArgument.fromProps({
name: 'format',
description: 'output format',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
enumList: [
new SlashCommandEnumValue('popup', 'Show variables in a popup.', enumTypes.enum, enumIcons.default),
new SlashCommandEnumValue('chat', 'Post a system message to the chat.', enumTypes.enum, enumIcons.message),
new SlashCommandEnumValue('none', 'Just return the variables as a JSON list.', enumTypes.enum, enumIcons.array),
],
}),
],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'setvar',
@ -1264,32 +1315,36 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
forceEnum: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'right',
description: 'right operand',
typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
forceEnum: false,
}),
new SlashCommandNamedArgument(
'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [
new SlashCommandEnumValue('gt', 'a > b'),
new SlashCommandEnumValue('gte', 'a >= b'),
new SlashCommandEnumValue('lt', 'a < b'),
new SlashCommandEnumValue('lte', 'a <= b'),
new SlashCommandEnumValue('eq', 'a == b'),
new SlashCommandEnumValue('neq', 'a !== b'),
new SlashCommandEnumValue('not', '!a'),
new SlashCommandEnumValue('in', 'a includes b'),
new SlashCommandEnumValue('nin', 'a not includes b'),
SlashCommandNamedArgument.fromProps({
name: 'rule',
description: 'comparison rule',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'eq',
enumList: [
new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
new SlashCommandEnumValue('gt', 'a > b (numbers)'),
new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
new SlashCommandEnumValue('lt', 'a < b (numbers)'),
new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
new SlashCommandEnumValue('not', '!a (truthy)'),
],
),
new SlashCommandNamedArgument(
'else', 'command to execute if not true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], false,
),
forceEnum: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'else',
description: 'command to execute if not true',
typeList: [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND],
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -1306,18 +1361,26 @@ export function registerVariableCommands() {
<div>
Numeric values and string literals for left and right operands supported.
</div>
<div>
If the rule is not provided, it defaults to <code>eq</code>.
</div>
<div>
If no right operand is provided, it defaults to checking the <code>left</code> value to be truthy.
A non-empty string or non-zero number is considered truthy, as is the value <code>true</code> or <code>on</code>.<br />
Only acceptable rules for no provided right operand are <code>not</code>, and no provided rule - which default to returning whether it is not or is truthy.
</div>
<div>
<strong>Available rules:</strong>
<ul>
<li>gt => a > b</li>
<li>gte => a >= b</li>
<li>lt => a < b</li>
<li>lte => a <= b</li>
<li>eq => a == b</li>
<li>neq => a != b</li>
<li>not => !a</li>
<li>in (strings) => a includes b</li>
<li>nin (strings) => a not includes b</li>
<li><code>eq</code> => a == b <small>(strings & numbers)</small></li>
<li><code>neq</code> => a !== b <small>(strings & numbers)</small></li>
<li><code>in</code> => a includes b <small>(strings & numbers as strings)</small></li>
<li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
<li><code>gt</code> => a > b <small>(numbers)</small></li>
<li><code>gte</code> => a >= b <small>(numbers)</small></li>
<li><code>lt</code> => a < b <small>(numbers)</small></li>
<li><code>lte</code> => a <= b <small>(numbers)</small></li>
<li><code>not</code> => !a <small>(truthy)</small></li>
</ul>
</div>
<div>
@ -1327,6 +1390,17 @@ export function registerVariableCommands() {
<pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre>
triggers a /speak command if the value of "score" is greater or equals 10.
</li>
<li>
<pre><code class="language-stscript">/if left={{lastMessage}} rule=in right=surprise {: /echo SURPISE! :}</code></pre>
executes a subcommand defined as a closure if the given value contains a specified word.
<li>
<pre><code class="language-stscript">/if left=myContent {: /echo My content had some content. :}</code></pre>
executes the defined subcommand, if the provided value of left is truthy (contains some kind of contant that is not empty or false)
</li>
<li>
<pre><code class="language-stscript">/if left=tree right={{getvar::object}} {: /echo The object is a tree! :}</code></pre>
executes the defined subcommand, if the left and right values are equals.
</li>
</ul>
</div>
`,
@ -1342,32 +1416,38 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
forceEnum: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'right',
description: 'right operand',
typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
forceEnum: false,
}),
new SlashCommandNamedArgument(
'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [
new SlashCommandEnumValue('gt', 'a > b'),
new SlashCommandEnumValue('gte', 'a >= b'),
new SlashCommandEnumValue('lt', 'a < b'),
new SlashCommandEnumValue('lte', 'a <= b'),
new SlashCommandEnumValue('eq', 'a == b'),
new SlashCommandEnumValue('neq', 'a !== b'),
new SlashCommandEnumValue('not', '!a'),
new SlashCommandEnumValue('in', 'a includes b'),
new SlashCommandEnumValue('nin', 'a not includes b'),
SlashCommandNamedArgument.fromProps({
name: 'rule',
description: 'comparison rule',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'eq',
enumList: [
new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
new SlashCommandEnumValue('gt', 'a > b (numbers)'),
new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
new SlashCommandEnumValue('lt', 'a < b (numbers)'),
new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
new SlashCommandEnumValue('not', '!a (truthy)'),
],
),
new SlashCommandNamedArgument(
'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),
),
forceEnum: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'guard',
description: 'disable loop iteration limit',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'off',
enumList: commonEnumProviders.boolean('onOff')(),
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -1386,15 +1466,15 @@ export function registerVariableCommands() {
<div>
<strong>Available rules:</strong>
<ul>
<li>gt => a > b</li>
<li>gte => a >= b</li>
<li>lt => a < b</li>
<li>lte => a <= b</li>
<li>eq => a == b</li>
<li>neq => a != b</li>
<li>not => !a</li>
<li>in (strings) => a includes b</li>
<li>nin (strings) => a not includes b</li>
<li><code>eq</code> => a == b <small>(strings & numbers)</small></li>
<li><code>neq</code> => a !== b <small>(strings & numbers)</small></li>
<li><code>in</code> => a includes b <small>(strings & numbers as strings)</small></li>
<li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
<li><code>gt</code> => a > b <small>(numbers)</small></li>
<li><code>gte</code> => a >= b <small>(numbers)</small></li>
<li><code>lt</code> => a < b <small>(numbers)</small></li>
<li><code>lte</code> => a <= b <small>(numbers)</small></li>
<li><code>not</code> => !a <small>(truthy)</small></li>
</ul>
</div>
<div>
@ -1404,7 +1484,11 @@ export function registerVariableCommands() {
<pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>
adds 1 to the value of "i" until it reaches 10.
</li>
</ul>
<li>
<pre><code class="language-stscript">/while left={{getvar::currentword}} {: /setvar key=currentword {: /do-something-and-return :}() | /echo The current work is "{{getvar::currentword}}" :}</code></pre>
executes the defined subcommand as long as the "currentword" variable is truthy (has any content that is not false/empty)
</ul>
</li>
</div>
<div>
Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.
@ -1519,7 +1603,7 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
acceptsMultiple: true,
enumProvider: (executor, scope)=>{
enumProvider: (executor, scope) => {
const vars = commonEnumProviders.variables('all')(executor, scope);
vars.push(
new SlashCommandEnumValue(
@ -1527,16 +1611,16 @@ export function registerVariableCommands() {
null,
enumTypes.variable,
enumIcons.variable,
(input)=>/^\w*$/.test(input),
(input)=>input,
(input) => /^\w*$/.test(input),
(input) => input,
),
new SlashCommandEnumValue(
'any number',
null,
enumTypes.number,
enumIcons.number,
(input)=>input == '' || !Number.isNaN(Number(input)),
(input)=>input,
(input) => input == '' || !Number.isNaN(Number(input)),
(input) => input,
),
);
return vars;

View File

@ -3154,6 +3154,26 @@ grammarly-extension {
align-items: center;
}
.alternate_greeting details {
padding: 2px;
}
.alternate_greeting summary {
list-style-position: outside;
margin-left: 1em;
padding-left: 1em;
}
.alternate_greeting textarea {
field-sizing: content;
max-height: 50dvh;
}
.alternate_greeting summary::marker,
.alternate_greeting summary strong {
cursor: pointer;
}
#rm_characters_block .form_create_bottom_buttons_block {
justify-content: space-evenly !important;
flex-grow: 0;
@ -3245,8 +3265,9 @@ grammarly-extension {
}
.wide_dialogue_popup {
aspect-ratio: 1 / 1;
width: unset !important;
/* FIXME: Chrome 129 broke max-height for aspect-ratio sized elements */
/* aspect-ratio: 1 / 1; */
/* width: unset !important; */
min-width: var(--sheldWidth);
}
@ -5365,13 +5386,6 @@ body:not(.movingUI) .drawer-content.maximized {
/* Jank mobile support for gallery and future draggables */
@media screen and (max-width: 1000px) {
#gallery {
display: block;
width: 100vw;
height: 100vh;
z-index: 9999;
}
.draggable {
display: block;
width: 100vw;

View File

@ -347,6 +347,8 @@ async function migrateSystemPrompts() {
if (fs.existsSync(migrateMarker)) {
continue;
}
const backupsPath = path.join(directory.backups, '_sysprompt');
fs.mkdirSync(backupsPath, { recursive: true });
const defaultPrompts = await getDefaultSystemPrompts();
const instucts = fs.readdirSync(directory.instruct);
let migratedPrompts = [];
@ -356,6 +358,8 @@ async function migrateSystemPrompts() {
if (path.extname(instruct) === '.json' && !fs.existsSync(sysPromptPath)) {
const instructData = JSON.parse(fs.readFileSync(instructPath, 'utf8'));
if ('system_prompt' in instructData && 'name' in instructData) {
const backupPath = path.join(backupsPath, `${instructData.name}.json`);
fs.cpSync(instructPath, backupPath, { force: true });
const syspromptData = { name: instructData.name, content: instructData.system_prompt };
migratedPrompts.push(syspromptData);
delete instructData.system_prompt;
@ -374,8 +378,8 @@ async function migrateSystemPrompts() {
console.log(`Migrated system prompt ${sysPromptData.name} for ${directory.root.split(path.sep).pop()}`);
}
writeFileAtomicSync(migrateMarker, '');
} catch {
// Ignore errors
} catch (error) {
console.error('Error migrating system prompts:', error);
}
}
}