Resolve conflict

78a287e7f589a31ccd2eae161ae844b272f26c88

Yokayo <nonameutubecomm@gmail.com>

25 files changed, +1349 -201Ignore whitespace
default/content/Eldoria.json+96 -12
@@ -9,13 +9,34 @@
9 "magical forest"9 "magical forest"
10 ],10 ],
11 "keysecondary": [],11 "keysecondary": [],
12 "comment": "",12 "comment": "eldoria",
13 "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.*",13 "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.*",
14 "constant": false,14 "constant": false,
15 "selective": false,15 "selective": true,
16 "order": 100,16 "order": 100,
17 "position": 0,17 "position": 0,
18 "disable": false18 "disable": false,
19 "displayIndex": 0,
20 "addMemo": true,
21 "group": "",
22 "groupOverride": false,
23 "groupWeight": 100,
24 "sticky": 0,
25 "cooldown": 0,
26 "delay": 0,
27 "probability": 100,
28 "depth": 4,
29 "useProbability": true,
30 "role": null,
31 "vectorized": false,
32 "excludeRecursion": false,
33 "preventRecursion": false,
34 "delayUntilRecursion": false,
35 "scanDepth": null,
36 "caseSensitive": null,
37 "matchWholeWords": null,
38 "useGroupScoring": null,
39 "automationId": ""
19 },40 },
20 "1": {41 "1": {
21 "uid": 1,42 "uid": 1,
@@ -27,13 +48,34 @@
27 "beasts"48 "beasts"
28 ],49 ],
29 "keysecondary": [],50 "keysecondary": [],
30 "comment": "",51 "comment": "shadowfang",
31 "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.\"",52 "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.\"",
32 "constant": false,53 "constant": false,
33 "selective": false,54 "selective": true,
34 "order": 100,55 "order": 100,
35 "position": 0,56 "position": 0,
36 "disable": false57 "disable": false,
58 "displayIndex": 1,
59 "addMemo": true,
60 "group": "",
61 "groupOverride": false,
62 "groupWeight": 100,
63 "sticky": 0,
64 "cooldown": 0,
65 "delay": 0,
66 "probability": 100,
67 "depth": 4,
68 "useProbability": true,
69 "role": null,
70 "vectorized": false,
71 "excludeRecursion": false,
72 "preventRecursion": false,
73 "delayUntilRecursion": false,
74 "scanDepth": null,
75 "caseSensitive": null,
76 "matchWholeWords": null,
77 "useGroupScoring": null,
78 "automationId": ""
37 },79 },
38 "2": {80 "2": {
39 "uid": 2,81 "uid": 2,
@@ -43,13 +85,34 @@
43 "refuge"85 "refuge"
44 ],86 ],
45 "keysecondary": [],87 "keysecondary": [],
46 "comment": "",88 "comment": "glade",
47 "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.*",89 "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.*",
48 "constant": false,90 "constant": false,
49 "selective": false,91 "selective": true,
50 "order": 100,92 "order": 100,
51 "position": 0,93 "position": 0,
52 "disable": false94 "disable": false,
95 "displayIndex": 2,
96 "addMemo": true,
97 "group": "",
98 "groupOverride": false,
99 "groupWeight": 100,
100 "sticky": 0,
101 "cooldown": 0,
102 "delay": 0,
103 "probability": 100,
104 "depth": 4,
105 "useProbability": true,
106 "role": null,
107 "vectorized": false,
108 "excludeRecursion": false,
109 "preventRecursion": false,
110 "delayUntilRecursion": false,
111 "scanDepth": null,
112 "caseSensitive": null,
113 "matchWholeWords": null,
114 "useGroupScoring": null,
115 "automationId": ""
53 },116 },
54 "3": {117 "3": {
55 "uid": 3,118 "uid": 3,
@@ -59,13 +122,34 @@
59 "ability"122 "ability"
60 ],123 ],
61 "keysecondary": [],124 "keysecondary": [],
62 "comment": "",125 "comment": "power",
63 "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.*",126 "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.*",
64 "constant": false,127 "constant": false,
65 "selective": false,128 "selective": true,
66 "order": 100,129 "order": 100,
67 "position": 0,130 "position": 0,
68 "disable": false131 "disable": false,
132 "displayIndex": 3,
133 "addMemo": true,
134 "group": "",
135 "groupOverride": false,
136 "groupWeight": 100,
137 "sticky": 0,
138 "cooldown": 0,
139 "delay": 0,
140 "probability": 100,
141 "depth": 4,
142 "useProbability": true,
143 "role": null,
144 "vectorized": false,
145 "excludeRecursion": false,
146 "preventRecursion": false,
147 "delayUntilRecursion": false,
148 "scanDepth": null,
149 "caseSensitive": null,
150 "matchWholeWords": null,
151 "useGroupScoring": null,
152 "automationId": ""
69 }153 }
70 }154 }
71}155}
default/content/default_Seraphina.png+0 -0

Binary file

public/index.html+24 -7
@@ -1310,7 +1310,7 @@
1310 </div>1310 </div>
1311 </div>1311 </div>
13121312
1313 <div data-tg-type="koboldcpp, aphrodite" id="xtc_block" class="wide100p">1313 <div data-tg-type="koboldcpp, aphrodite, tabby, ooba" id="xtc_block" class="wide100p">
1314 <h4 class="wide100p textAlignCenter">1314 <h4 class="wide100p textAlignCenter">
1315 <label data-i18n="Exclude Top Choices (XTC)">Exclude Top Choices (XTC)</label>1315 <label data-i18n="Exclude Top Choices (XTC)">Exclude Top Choices (XTC)</label>
1316 <a href="https://github.com/oobabooga/text-generation-webui/pull/6335" target="_blank">1316 <a href="https://github.com/oobabooga/text-generation-webui/pull/6335" target="_blank">
@@ -1679,6 +1679,10 @@
1679 Ooba only. Determines the order of samplers.1679 Ooba only. Determines the order of samplers.
1680 </div>1680 </div>
1681 <div id="sampler_priority_container" class="prompt_order">1681 <div id="sampler_priority_container" class="prompt_order">
1682 <div data-name="repetition_penalty" draggable="true"><span>Repetition Penalty</span><small></small></div>
1683 <div data-name="presence_penalty" draggable="true"><span>Presence Penalty</span><small></small></div>
1684 <div data-name="frequency_penalty" draggable="true"><span>Frequency Penalty</span><small></small></div>
1685 <div data-name="dry" draggable="true"><span>DRY</span><small></small></div>
1682 <div data-name="temperature" draggable="true"><span>Temperature</span><small></small></div>1686 <div data-name="temperature" draggable="true"><span>Temperature</span><small></small></div>
1683 <div data-name="dynamic_temperature" draggable="true"><span>Dynamic Temperature</span><small></small></div>1687 <div data-name="dynamic_temperature" draggable="true"><span>Dynamic Temperature</span><small></small></div>
1684 <div data-name="quadratic_sampling" draggable="true"><span>Quadratic / Smooth Sampling</span><small></small></div>1688 <div data-name="quadratic_sampling" draggable="true"><span>Quadratic / Smooth Sampling</span><small></small></div>
@@ -1691,6 +1695,9 @@
1691 <div data-name="top_a" draggable="true"><span>Top A</span><small></small></div>1695 <div data-name="top_a" draggable="true"><span>Top A</span><small></small></div>
1692 <div data-name="min_p" draggable="true"><span>Min P</span><small></small></div>1696 <div data-name="min_p" draggable="true"><span>Min P</span><small></small></div>
1693 <div data-name="mirostat" draggable="true"><span>Mirostat</span><small></small></div>1697 <div data-name="mirostat" draggable="true"><span>Mirostat</span><small></small></div>
1698 <div data-name="xtc" draggable="true"><span>XTC</span><small></small></div>
1699 <div data-name="encoder_repetition_penalty" draggable="true"><span>Encoder Repetition Penalty</span><small></small></div>
1700 <div data-name="no_repeat_ngram" draggable="true"><span>No Repeat Ngram</span><small></small></div>
1694 </div>1701 </div>
1695 <div id="textgenerationwebui_default_order" class="menu_button menu_button_icon">1702 <div id="textgenerationwebui_default_order" class="menu_button menu_button_icon">
1696 <span data-i18n="Load default order">Load default order</span>1703 <span data-i18n="Load default order">Load default order</span>
@@ -6090,7 +6097,10 @@
6090 <div class="alternate_grettings flexFlowColumn flex-container">6097 <div class="alternate_grettings flexFlowColumn flex-container">
6091 <div class="title_restorable">6098 <div class="title_restorable">
6092 <h3><span data-i18n="Alternate Greetings" class="mdhotkey_location">Alternate Greetings</span></h3>6099 <h3><span data-i18n="Alternate Greetings" class="mdhotkey_location">Alternate Greetings</span></h3>
6093 <div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>6100 <div class="menu_button menu_button_icon add_alternate_greeting">
6101 <i class="fa-solid fa-plus"></i>
6102 <span data-i18n="Add">Add</span>
6103 </div>
6094 </div>6104 </div>
6095 <small class="justifyLeft" data-i18n="Alternate_Greetings_desc">6105 <small class="justifyLeft" data-i18n="Alternate_Greetings_desc">
6096 These will be displayed as swipes on the first message when starting a new chat.6106 These will be displayed as swipes on the first message when starting a new chat.
@@ -6106,11 +6116,18 @@
6106 </div>6116 </div>
6107 <div id="alternate_greeting_form_template" class="template_element">6117 <div id="alternate_greeting_form_template" class="template_element">
6108 <div class="alternate_greeting">6118 <div class="alternate_greeting">
6109 <div class="title_restorable">6119 <details open>
6110 <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>6120 <summary>
6111 <div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>6121 <div class="title_restorable">
6112 </div>6122 <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
6113 <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>6123 <div class="menu_button menu_button_icon delete_alternate_greeting">
6124 <i class="fa-solid fa-trash-alt"></i>
6125 <span data-i18n="Delete">Delete</span>
6126 </div>
6127 </div>
6128 </summary>
6129 <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>
6130 </details>
6114 </div>6131 </div>
6115 </div>6132 </div>
61166133
public/locales/ru-ru.json+3 -2
@@ -1719,7 +1719,6 @@
1719 "Model": "Модель",1719 "Model": "Модель",
1720 "Proxy Preset": "Пресет для прокси",1720 "Proxy Preset": "Пресет для прокси",
1721 "Enter a name:": "Введите название:",1721 "Enter a name:": "Введите название:",
1722 "Enter a new name": "Введите новое название",
1723 "Are you sure you want to delete the selected profile?": "Вы точно хотите удалить выбранный профиль?",1722 "Are you sure you want to delete the selected profile?": "Вы точно хотите удалить выбранный профиль?",
1724 "instruct_enabled": "Включить Instruct-режим",1723 "instruct_enabled": "Включить Instruct-режим",
1725 "Instruct Template": "Шаблон Instruct-режима",1724 "Instruct Template": "Шаблон Instruct-режима",
@@ -1746,5 +1745,7 @@
1746 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "Содержимое этого поля заменит стандартные Инструкции после истории, применяемые для этого персонажа. (v2 spec: post_history_instructions)",1745 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "Содержимое этого поля заменит стандартные Инструкции после истории, применяемые для этого персонажа. (v2 spec: post_history_instructions)",
1747 "None (disabled)": "Нигде (откл.)",1746 "None (disabled)": "Нигде (откл.)",
1748 "Markdown Hotkeys": "Горячие клавиши для разметки",1747 "Markdown Hotkeys": "Горячие клавиши для разметки",
1749 "markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'."1748 "markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'.",
1749 "Save and Update": "Сохранить и обновить",
1750 "Profile name:": "Название профиля:"
1750}1751}
public/script.js+35 -12
@@ -158,7 +158,7 @@ import {
158} from './scripts/utils.js';158} from './scripts/utils.js';
159import { debounce_timeout } from './scripts/constants.js';159import { debounce_timeout } from './scripts/constants.js';
160160
161import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';161import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, initExtensions, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
162import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';162import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
163import {163import {
164 tag_map,164 tag_map,
@@ -244,6 +244,7 @@ import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCo
244import { initInputMarkdown } from './scripts/input-md-formatting.js';244import { initInputMarkdown } from './scripts/input-md-formatting.js';
245import { AbortReason } from './scripts/util/AbortReason.js';245import { AbortReason } from './scripts/util/AbortReason.js';
246import { initSystemPrompts } from './scripts/sysprompt.js';246import { initSystemPrompts } from './scripts/sysprompt.js';
247import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
247248
248//exporting functions and vars for mods249//exporting functions and vars for mods
249export {250export {
@@ -935,6 +936,8 @@ async function firstLoadInit() {
935 initTextGenModels();936 initTextGenModels();
936 initOpenAI();937 initOpenAI();
937 initSystemPrompts();938 initSystemPrompts();
939 initExtensions();
940 initExtensionSlashCommands();
938 await initPresetManager();941 await initPresetManager();
939 await getSystemMessages();942 await getSystemMessages();
940 sendSystemMessage(system_message_types.WELCOME);943 sendSystemMessage(system_message_types.WELCOME);
@@ -2573,7 +2576,7 @@ export function getStoppingStrings(isImpersonate, isContinue) {
2573 result.unshift('\n');2576 result.unshift('\n');
2574 }2577 }
25752578
2576 return result.filter(onlyUnique);2579 return result.filter(x => x).filter(onlyUnique);
2577}2580}
25782581
2579/**2582/**
@@ -4863,7 +4866,7 @@ export function getMaxContextSize(overrideResponseLength = null) {
4863 this_max_context = Math.min(max_context, 8192);4866 this_max_context = Math.min(max_context, 8192);
48644867
4865 // Added special tokens and whatnot4868 // Added special tokens and whatnot
4866 this_max_context -= 1;4869 this_max_context -= 10;
4867 }4870 }
48684871
4869 this_max_context = this_max_context - (overrideResponseLength || amount_gen);4872 this_max_context = this_max_context - (overrideResponseLength || amount_gen);
@@ -7390,7 +7393,7 @@ function onScenarioOverrideRemoveClick() {
7390 */7393 */
7391export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {7394export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
7392 function getOkButtonText() {7395 function getOkButtonText() {
7393 if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) {7396 if (['text', 'char_not_selected'].includes(popup_type)) {
7394 $dialoguePopupCancel.css('display', 'none');7397 $dialoguePopupCancel.css('display', 'none');
7395 return okButton ?? 'Ok';7398 return okButton ?? 'Ok';
7396 } else if (['delete_extension'].includes(popup_type)) {7399 } else if (['delete_extension'].includes(popup_type)) {
@@ -7827,24 +7830,42 @@ function openAlternateGreetings() {
78277830
7828 const template = $('#alternate_greetings_template .alternate_grettings').clone();7831 const template = $('#alternate_greetings_template .alternate_grettings').clone();
7829 const getArray = () => menu_type == 'create' ? create_save.alternate_greetings : characters[chid].data.alternate_greetings;7832 const getArray = () => menu_type == 'create' ? create_save.alternate_greetings : characters[chid].data.alternate_greetings;
7833 const popup = new Popup(template, POPUP_TYPE.TEXT, '', {
7834 wide: true,
7835 large: true,
7836 allowVerticalScrolling: true,
7837 onClose: async () => {
7838 if (menu_type !== 'create') {
7839 await createOrEditCharacter();
7840 }
7841 }
7842 });
78307843
7831 for (let index = 0; index < getArray().length; index++) {7844 for (let index = 0; index < getArray().length; index++) {
7832 addAlternateGreeting(template, getArray()[index], index, getArray);7845 addAlternateGreeting(template, getArray()[index], index, getArray, popup);
7833 }7846 }
78347847
7835 template.find('.add_alternate_greeting').on('click', function () {7848 template.find('.add_alternate_greeting').on('click', function () {
7836 const array = getArray();7849 const array = getArray();
7837 const index = array.length;7850 const index = array.length;
7838 array.push('');7851 array.push('');
7839 addAlternateGreeting(template, '', index, getArray);7852 addAlternateGreeting(template, '', index, getArray, popup);
7840 updateAlternateGreetingsHintVisibility(template);7853 updateAlternateGreetingsHintVisibility(template);
7841 });7854 });
78427855
7856 popup.show();
7843 updateAlternateGreetingsHintVisibility(template);7857 updateAlternateGreetingsHintVisibility(template);
7844 callPopup(template, 'alternate_greeting', '', { wide: true, large: true });
7845}7858}
78467859
7847function addAlternateGreeting(template, greeting, index, getArray) {7860/**
7861 * Adds an alternate greeting to the template.
7862 * @param {JQuery<HTMLElement>} template
7863 * @param {string} greeting
7864 * @param {number} index
7865 * @param {() => any[]} getArray
7866 * @param {Popup} popup
7867 */
7868function addAlternateGreeting(template, greeting, index, getArray, popup) {
7848 const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();7869 const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();
7849 greetingBlock.find('.alternate_greeting_text').on('input', async function () {7870 greetingBlock.find('.alternate_greeting_text').on('input', async function () {
7850 const value = $(this).val();7871 const value = $(this).val();
@@ -7852,11 +7873,16 @@ function addAlternateGreeting(template, greeting, index, getArray) {
7852 array[index] = value;7873 array[index] = value;
7853 }).val(greeting);7874 }).val(greeting);
7854 greetingBlock.find('.greeting_index').text(index + 1);7875 greetingBlock.find('.greeting_index').text(index + 1);
7855 greetingBlock.find('.delete_alternate_greeting').on('click', async function () {7876 greetingBlock.find('.delete_alternate_greeting').on('click', async function (event) {
7877 event.preventDefault();
7878 event.stopPropagation();
7879
7856 if (confirm(t`Are you sure you want to delete this alternate greeting?`)) {7880 if (confirm(t`Are you sure you want to delete this alternate greeting?`)) {
7857 const array = getArray();7881 const array = getArray();
7858 array.splice(index, 1);7882 array.splice(index, 1);
7883
7859 // We need to reopen the popup to update the index numbers7884 // We need to reopen the popup to update the index numbers
7885 await popup.complete(POPUP_RESULT.AFFIRMATIVE);
7860 openAlternateGreetings();7886 openAlternateGreetings();
7861 }7887 }
7862 });7888 });
@@ -9574,9 +9600,6 @@ jQuery(async function () {
9574 }, 2000);9600 }, 2000);
9575 }9601 }
9576 }9602 }
9577 if (popup_type == 'alternate_greeting' && menu_type !== 'create') {
9578 createOrEditCharacter();
9579 }
95809603
9581 if (dialogueResolve) {9604 if (dialogueResolve) {
9582 if (popup_type == 'input') {9605 if (popup_type == 'input') {
public/scripts/extensions-slashcommands.js+320 -0
@@ -0,0 +1,320 @@
1import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js';
2import { SlashCommand } from './slash-commands/SlashCommand.js';
3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
4import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
5import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
6import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
7import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
8import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';
9
10/**
11 * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
12 * @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise<string>}
13 */
14function getExtensionActionCallback(action) {
15 return async (args, extensionName) => {
16 if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
17 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
18 if (!extensionName) {
19 toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
20 return '';
21 }
22
23 const reload = !isFalseBoolean(args?.reload);
24 const internalExtensionName = findExtension(extensionName);
25 if (!internalExtensionName) {
26 toastr.warning(`Extension ${extensionName} does not exist.`);
27 return '';
28 }
29
30 const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
31
32 if (action === 'enable' && isEnabled) {
33 toastr.info(`Extension ${extensionName} is already enabled.`);
34 return internalExtensionName;
35 }
36
37 if (action === 'disable' && !isEnabled) {
38 toastr.info(`Extension ${extensionName} is already disabled.`);
39 return internalExtensionName;
40 }
41
42 if (action === 'toggle') {
43 action = isEnabled ? 'disable' : 'enable';
44 }
45
46 if (reload) {
47 toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
48
49 // Clear input, so it doesn't stay because the command didn't "finish",
50 // and wait for a bit to both show the toast and let the clear bubble through.
51 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
52 await new Promise(resolve => setTimeout(resolve, 100));
53 }
54
55 if (action === 'enable') {
56 await enableExtension(internalExtensionName, reload);
57 } else {
58 await disableExtension(internalExtensionName, reload);
59 }
60
61 toastr.success(`Extension ${extensionName} ${action}d.`);
62
63
64 console.info(`Extension ${action}ed: ${extensionName}`);
65 if (!reload) {
66 console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
67 }
68
69 return internalExtensionName;
70 };
71}
72
73/**
74 * Finds an extension by name, allowing omission of the "third-party/" prefix.
75 *
76 * @param {string} name - The name of the extension to find
77 * @returns {string?} - The matched extension name or undefined if not found
78 */
79function findExtension(name) {
80 return extensionNames.find(extName => {
81 return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
82 });
83}
84
85/**
86 * Provides an array of SlashCommandEnumValue objects based on the extension names.
87 * Each object contains the name of the extension and a description indicating if it is a third-party extension.
88 *
89 * @returns {SlashCommandEnumValue[]} An array of SlashCommandEnumValue objects
90 */
91const extensionNamesEnumProvider = () => extensionNames.map(name => {
92 const isThirdParty = name.startsWith('third-party/');
93 if (isThirdParty) name = name.slice('third-party/'.length);
94
95 const description = isThirdParty ? 'third party extension' : null;
96
97 return new SlashCommandEnumValue(name, description, !isThirdParty ? enumTypes.name : enumTypes.enum);
98});
99
100export function registerExtensionSlashCommands() {
101 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
102 name: 'extension-enable',
103 callback: getExtensionActionCallback('enable'),
104 returns: 'The internal extension name',
105 namedArgumentList: [
106 SlashCommandNamedArgument.fromProps({
107 name: 'reload',
108 description: 'Whether to reload the page after enabling the extension',
109 typeList: [ARGUMENT_TYPE.BOOLEAN],
110 defaultValue: 'true',
111 enumList: commonEnumProviders.boolean('trueFalse')(),
112 }),
113 ],
114 unnamedArgumentList: [
115 SlashCommandArgument.fromProps({
116 description: 'Extension name',
117 typeList: [ARGUMENT_TYPE.STRING],
118 isRequired: true,
119 enumProvider: extensionNamesEnumProvider,
120 forceEnum: true,
121 }),
122 ],
123 helpString: `
124 <div>
125 Enables a specified extension.
126 </div>
127 <div>
128 By default, the page will be reloaded automatically, stopping any further commands.<br />
129 If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay disabled until refreshed.
130 The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
131 </div>
132 <div>
133 <strong>Example:</strong>
134 <ul>
135 <li>
136 <pre><code class="language-stscript">/extension-enable Summarize</code></pre>
137 </li>
138 </ul>
139 </div>
140 `,
141 }));
142 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
143 name: 'extension-disable',
144 callback: getExtensionActionCallback('disable'),
145 returns: 'The internal extension name',
146 namedArgumentList: [
147 SlashCommandNamedArgument.fromProps({
148 name: 'reload',
149 description: 'Whether to reload the page after disabling the extension',
150 typeList: [ARGUMENT_TYPE.BOOLEAN],
151 defaultValue: 'true',
152 enumList: commonEnumProviders.boolean('trueFalse')(),
153 }),
154 ],
155 unnamedArgumentList: [
156 SlashCommandArgument.fromProps({
157 description: 'Extension name',
158 typeList: [ARGUMENT_TYPE.STRING],
159 isRequired: true,
160 enumProvider: extensionNamesEnumProvider,
161 forceEnum: true,
162 }),
163 ],
164 helpString: `
165 <div>
166 Disables a specified extension.
167 </div>
168 <div>
169 By default, the page will be reloaded automatically, stopping any further commands.<br />
170 If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay enabled until refreshed.
171 The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
172 </div>
173 <div>
174 <strong>Example:</strong>
175 <ul>
176 <li>
177 <pre><code class="language-stscript">/extension-disable Summarize</code></pre>
178 </li>
179 </ul>
180 </div>
181 `,
182 }));
183 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
184 name: 'extension-toggle',
185 callback: async (args, extensionName) => {
186 if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
187 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
188
189 const action = isTrueBoolean(args?.state) ? 'enable' :
190 isFalseBoolean(args?.state) ? 'disable' :
191 'toggle';
192
193 return await getExtensionActionCallback(action)(args, extensionName);
194 },
195 returns: 'The internal extension name',
196 namedArgumentList: [
197 SlashCommandNamedArgument.fromProps({
198 name: 'reload',
199 description: 'Whether to reload the page after toggling the extension',
200 typeList: [ARGUMENT_TYPE.BOOLEAN],
201 defaultValue: 'true',
202 enumList: commonEnumProviders.boolean('trueFalse')(),
203 }),
204 SlashCommandNamedArgument.fromProps({
205 name: 'state',
206 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.',
207 typeList: [ARGUMENT_TYPE.BOOLEAN],
208 enumList: commonEnumProviders.boolean('trueFalse')(),
209 }),
210 ],
211 unnamedArgumentList: [
212 SlashCommandArgument.fromProps({
213 description: 'Extension name',
214 typeList: [ARGUMENT_TYPE.STRING],
215 isRequired: true,
216 enumProvider: extensionNamesEnumProvider,
217 forceEnum: true,
218 }),
219 ],
220 helpString: `
221 <div>
222 Toggles the state of a specified extension.
223 </div>
224 <div>
225 By default, the page will be reloaded automatically, stopping any further commands.<br />
226 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.
227 The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
228 </div>
229 <div>
230 <strong>Example:</strong>
231 <ul>
232 <li>
233 <pre><code class="language-stscript">/extension-toggle Summarize</code></pre>
234 </li>
235 <li>
236 <pre><code class="language-stscript">/extension-toggle Summarize state=true</code></pre>
237 </li>
238 </ul>
239 </div>
240 `,
241 }));
242 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
243 name: 'extension-state',
244 callback: async (_, extensionName) => {
245 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
246 const internalExtensionName = findExtension(extensionName);
247 if (!internalExtensionName) {
248 toastr.warning(`Extension ${extensionName} does not exist.`);
249 return '';
250 }
251
252 const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
253 return String(isEnabled);
254 },
255 returns: 'The state of the extension, whether it is enabled.',
256 unnamedArgumentList: [
257 SlashCommandArgument.fromProps({
258 description: 'Extension name',
259 typeList: [ARGUMENT_TYPE.STRING],
260 isRequired: true,
261 enumProvider: extensionNamesEnumProvider,
262 forceEnum: true,
263 }),
264 ],
265 helpString: `
266 <div>
267 Returns the state of a specified extension (true if enabled, false if disabled).
268 </div>
269 <div>
270 <strong>Example:</strong>
271 <ul>
272 <li>
273 <pre><code class="language-stscript">/extension-state Summarize</code></pre>
274 </li>
275 </ul>
276 </div>
277 `,
278 }));
279 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
280 name: 'extension-exists',
281 aliases: ['extension-installed'],
282 callback: async (_, extensionName) => {
283 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
284 const exists = findExtension(extensionName) !== undefined;
285 return exists ? 'true' : 'false';
286 },
287 returns: 'Whether the extension exists and is installed.',
288 unnamedArgumentList: [
289 SlashCommandArgument.fromProps({
290 description: 'Extension name',
291 typeList: [ARGUMENT_TYPE.STRING],
292 isRequired: true,
293 enumProvider: extensionNamesEnumProvider,
294 }),
295 ],
296 helpString: `
297 <div>
298 Checks if a specified extension exists.
299 </div>
300 <div>
301 <strong>Example:</strong>
302 <ul>
303 <li>
304 <pre><code class="language-stscript">/extension-exists SillyTavern-LALib</code></pre>
305 </li>
306 </ul>
307 </div>
308 `,
309 }));
310
311 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
312 name: 'reload-page',
313 callback: async () => {
314 toastr.info('Reloading the page...');
315 location.reload();
316 return '';
317 },
318 helpString: 'Reloads the current page. All further commands will not be processed.',
319 }));
320}
public/scripts/extensions.js+9 -5
@@ -1,5 +1,5 @@
1import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';1import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
2import { hideLoader, showLoader } from './loader.js';2import { showLoader } from './loader.js';
3import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';3import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
4import { renderTemplate, renderTemplateAsync } from './templates.js';4import { renderTemplate, renderTemplateAsync } from './templates.js';
5import { isSubsetOf, setValueByPath } from './utils.js';5import { isSubsetOf, setValueByPath } from './utils.js';
@@ -14,7 +14,9 @@ export {
14 ModuleWorkerWrapper,14 ModuleWorkerWrapper,
15};15};
1616
17/** @type {string[]} */
17export let extensionNames = [];18export let extensionNames = [];
19
18let manifests = {};20let manifests = {};
19const defaultUrl = 'http://localhost:5100';21const defaultUrl = 'http://localhost:5100';
2022
@@ -241,7 +243,7 @@ function onEnableExtensionClick() {
241 enableExtension(name, false);243 enableExtension(name, false);
242}244}
243245
244async function enableExtension(name, reload = true) {246export async function enableExtension(name, reload = true) {
245 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);247 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
246 stateChanged = true;248 stateChanged = true;
247 await saveSettings();249 await saveSettings();
@@ -252,7 +254,7 @@ async function enableExtension(name, reload = true) {
252 }254 }
253}255}
254256
255async function disableExtension(name, reload = true) {257export async function disableExtension(name, reload = true) {
256 extension_settings.disabledExtensions.push(name);258 extension_settings.disabledExtensions.push(name);
257 stateChanged = true;259 stateChanged = true;
258 await saveSettings();260 await saveSettings();
@@ -1041,7 +1043,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
1041 await installExtension(url);1043 await installExtension(url);
1042}1044}
10431045
1044jQuery(async function () {1046
1047
1048export async function initExtensions() {
1045 await addExtensionsButtonAndMenu();1049 await addExtensionsButtonAndMenu();
1046 $('#extensionsMenuButton').css('display', 'flex');1050 $('#extensionsMenuButton').css('display', 'flex');
10471051
@@ -1060,4 +1064,4 @@ jQuery(async function () {
1060 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.1064 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
1061 */1065 */
1062 $('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());1066 $('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
1063});1067}
public/scripts/extensions/connection-manager/edit.html+12 -0
@@ -0,0 +1,12 @@
1<div>
2 <h3>Included settings:</h3>
3 <div class="justifyLeft flex-container flexFlowColumn flexNoGap">
4 {{#each settings}}
5 <label class="checkbox_label">
6 <input type="checkbox" value="{{@key}}" name="exclude"{{#if this}} checked{{/if}}>
7 <span>{{@key}}</span>
8 </label>
9 {{/each}}
10 </div>
11 <h3 data-i18n="Profile name:">Profile name:</h3>
12</div>
public/scripts/extensions/connection-manager/index.js+90 -10
@@ -1,6 +1,6 @@
1import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';1import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
2import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';2import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
3import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';3import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
4import { SlashCommand } from '../../slash-commands/SlashCommand.js';4import { SlashCommand } from '../../slash-commands/SlashCommand.js';
5import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js';5import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js';
6import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';6import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@@ -136,6 +136,7 @@ const profilesProvider = () => [
136 * @property {string} [context] Context Template136 * @property {string} [context] Context Template
137 * @property {string} [instruct-state] Instruct Mode137 * @property {string} [instruct-state] Instruct Mode
138 * @property {string} [tokenizer] Tokenizer138 * @property {string} [tokenizer] Tokenizer
139 * @property {string[]} [exclude] Commands to exclude
139 */140 */
140141
141/**142/**
@@ -172,8 +173,13 @@ function findProfileByName(value) {
172async function readProfileFromCommands(mode, profile, cleanUp = false) {173async function readProfileFromCommands(mode, profile, cleanUp = false) {
173 const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;174 const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
174 const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS;175 const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS;
176 const excludeList = Array.isArray(profile.exclude) ? profile.exclude : [];
175 for (const command of commands) {177 for (const command of commands) {
176 try {178 try {
179 if (excludeList.includes(command)) {
180 continue;
181 }
182
177 const args = getNamedArguments();183 const args = getNamedArguments();
178 const result = await SlashCommandParser.commands[command].callback(args, '');184 const result = await SlashCommandParser.commands[command].callback(args, '');
179 if (result) {185 if (result) {
@@ -209,15 +215,37 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) {
209async function createConnectionProfile(forceName = null) {215async function createConnectionProfile(forceName = null) {
210 const mode = main_api === 'openai' ? 'cc' : 'tc';216 const mode = main_api === 'openai' ? 'cc' : 'tc';
211 const id = uuidv4();217 const id = uuidv4();
218 /** @type {ConnectionProfile} */
212 const profile = {219 const profile = {
213 id,220 id,
214 mode,221 mode,
222 exclude: [],
215 };223 };
216224
217 await readProfileFromCommands(mode, profile);225 await readProfileFromCommands(mode, profile);
218226
219 const profileForDisplay = makeFancyProfile(profile);227 const profileForDisplay = makeFancyProfile(profile);
220 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay });228 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }));
229 template.find('input[name="exclude"]').on('input', function () {
230 const fancyName = String($(this).val());
231 const keyName = Object.entries(FANCY_NAMES).find(x => x[1] === fancyName)?.[0];
232 if (!keyName) {
233 console.warn('Key not found for fancy name:', fancyName);
234 return;
235 }
236
237 if (!Array.isArray(profile.exclude)) {
238 profile.exclude = [];
239 }
240
241 const excludeState = !$(this).prop('checked');
242 if (excludeState) {
243 profile.exclude.push(keyName);
244 } else {
245 const index = profile.exclude.indexOf(keyName);
246 index !== -1 && profile.exclude.splice(index, 1);
247 }
248 });
221 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);249 const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
222 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);250 const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
223 const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });251 const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
@@ -231,7 +259,13 @@ async function createConnectionProfile(forceName = null) {
231 return null;259 return null;
232 }260 }
233261
234 profile.name = name;262 if (Array.isArray(profile.exclude)) {
263 for (const command of profile.exclude) {
264 delete profile[command];
265 }
266 }
267
268 profile.name = String(name);
235 return profile;269 return profile;
236}270}
237271
@@ -358,7 +392,11 @@ async function renderDetailsContent(detailsContent) {
358 const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);392 const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
359 if (profile) {393 if (profile) {
360 const profileForDisplay = makeFancyProfile(profile);394 const profileForDisplay = makeFancyProfile(profile);
361 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', { profile: profileForDisplay });395 const templateParams = { profile: profileForDisplay };
396 if (Array.isArray(profile.exclude) && profile.exclude.length > 0) {
397 templateParams.omitted = profile.exclude.map(e => FANCY_NAMES[e]).join(', ');
398 }
399 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', templateParams);
362 detailsContent.innerHTML = template;400 detailsContent.innerHTML = template;
363 } else {401 } else {
364 detailsContent.textContent = t`No profile selected`;402 detailsContent.textContent = t`No profile selected`;
@@ -473,29 +511,71 @@ async function renderDetailsContent(detailsContent) {
473 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE);511 await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE);
474 });512 });
475513
476 const renameButton = document.getElementById('rename_connection_profile');514 const editButton = document.getElementById('edit_connection_profile');
477 renameButton.addEventListener('click', async () => {515 editButton.addEventListener('click', async () => {
478 const selectedProfile = extension_settings.connectionManager.selectedProfile;516 const selectedProfile = extension_settings.connectionManager.selectedProfile;
479 const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);517 const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
480 if (!profile) {518 if (!profile) {
481 console.log('No profile selected');519 console.log('No profile selected');
482 return;520 return;
483 }521 }
522 if (!Array.isArray(profile.exclude)) {
523 profile.exclude = [];
524 }
525
526 let saveChanges = false;
527 const sortByViewOrder = (a, b) => Object.keys(FANCY_NAMES).indexOf(a) - Object.keys(FANCY_NAMES).indexOf(b);
528 const commands = profile.mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
529 const settings = commands.slice().sort(sortByViewOrder).reduce((acc, command) => {
530 const fancyName = FANCY_NAMES[command];
531 acc[fancyName] = !profile.exclude.includes(command);
532 return acc;
533 }, {});
534 const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
535 const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
536 customButtons: [{
537 text: t`Save and Update`,
538 classes: ['popup-button-ok'],
539 result: POPUP_RESULT.AFFIRMATIVE,
540 action: () => {
541 saveChanges = true;
542 },
543 }],
544 });
484545
485 const newName = await Popup.show.input(t`Enter a new name`, null, profile.name, { rows: 2 });
486 if (!newName) {546 if (!newName) {
487 return;547 return;
488 }548 }
489549
490 if (extension_settings.connectionManager.profiles.some(p => p.name === newName)) {550 if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
491 toastr.error('A profile with the same name already exists.');551 toastr.error('A profile with the same name already exists.');
492 return;552 return;
493 }553 }
494554
495 profile.name = newName;555 const newExcludeList = template.find('input[name="exclude"]:not(:checked)').map(function () {
556 return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
557 }).get();
558
559 if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
560 profile.exclude = newExcludeList;
561 for (const command of newExcludeList) {
562 delete profile[command];
563 }
564 if (saveChanges) {
565 await updateConnectionProfile(profile);
566 } else {
567 toastr.info('Press "Update" to record them into the profile.', 'Included settings list updated');
568 }
569 }
570
571 if (profile.name !== newName) {
572 toastr.success('Connection profile renamed.');
573 profile.name = String(newName);
574 }
575
496 saveSettingsDebounced();576 saveSettingsDebounced();
497 renderConnectionProfiles(profiles);577 renderConnectionProfiles(profiles);
498 toastr.success('Connection profile renamed', '', { timeOut: 1500 });578 await renderDetailsContent(detailsContent);
499 });579 });
500580
501 /** @type {HTMLElement} */581 /** @type {HTMLElement} */
public/scripts/extensions/connection-manager/profile.html+12 -3
@@ -2,11 +2,20 @@
2 <h2 data-i18n="Creating a Connection Profile">2 <h2 data-i18n="Creating a Connection Profile">
3 Creating a Connection Profile3 Creating a Connection Profile
4 </h2>4 </h2>
5 <ul class="justifyLeft">5 <div class="justifyLeft flex-container flexFlowColumn flexNoGap">
6 {{#each profile}}6 {{#each profile}}
7 <li><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</li>7 <label class="checkbox_label">
8 <input type="checkbox" value="{{@key}}" name="exclude" checked>
9 <span><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</span>
10 </label>
8 {{/each}}11 {{/each}}
9 </ul>12 </div>
13 <div class="marginTop5">
14 <small>
15 <b>Hint:</b>
16 <i>Click on the setting name to omit it from the profile.</i>
17 </small>
18 </div>
10 <h3 data-i18n="Enter a name:">19 <h3 data-i18n="Enter a name:">
11 Enter a name:20 Enter a name:
12 </h3>21 </h3>
public/scripts/extensions/connection-manager/settings.html+1 -1
@@ -13,7 +13,7 @@
13 <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>13 <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>
14 <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>14 <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>
15 <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>15 <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>
16 <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>16 <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>
17 <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>17 <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>
18 <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>18 <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>
19 </div>19 </div>
public/scripts/extensions/connection-manager/view.html+5 -0
@@ -3,3 +3,8 @@
3 <li><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</li>3 <li><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</li>
4 {{/each}}4 {{/each}}
5</ul>5</ul>
6{{#if omitted}}
7<div class="margin5">
8 <strong data-i18n="Omitted Settings:">Omitted Settings:</strong>&nbsp;<span>{{omitted}}</span>
9</div>
10{{/if}}
public/scripts/extensions/gallery/index.js+8 -6
@@ -26,9 +26,11 @@ let paginationVisiblePages = 10;
26let paginationMaxLinesPerPage = 2;26let paginationMaxLinesPerPage = 2;
27let galleryMaxRows = 3;27let galleryMaxRows = 3;
2828
29$('body').on('click', '.dragClose', function () {29// Remove all draggables associated with the gallery
30 const relatedId = $(this).data('related-id'); // Get the ID of the related draggable30$('#movingDivs').on('click', '.dragClose', function () {
31 $(`body > .draggable[id="${relatedId}"]`).remove(); // Remove the associated draggable31 const relatedId = $(this).data('related-id');
32 if (!relatedId) return;
33 $(`#movingDivs > .draggable[id="${relatedId}"]`).remove();
32});34});
3335
34const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved';36const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved';
@@ -290,7 +292,7 @@ function makeMovable(id = 'gallery') {
290292
291 $('#dragGallery').css('display', 'block');293 $('#dragGallery').css('display', 'block');
292294
293 $('body').append(newElement);295 $('#movingDivs').append(newElement);
294296
295 loadMovingUIState();297 loadMovingUIState();
296 $(`.draggable[forChar="${id}"]`).css('display', 'block');298 $(`.draggable[forChar="${id}"]`).css('display', 'block');
@@ -362,8 +364,8 @@ function makeDragImg(id, url) {
362 }364 }
363 }365 }
364366
365 // Step 3: Attach it to the body367 // Step 3: Attach it to the movingDivs container
366 document.body.appendChild(newElement);368 document.getElementById('movingDivs').appendChild(newElement);
367369
368 // Step 4: Call dragElement and loadMovingUIState370 // Step 4: Call dragElement and loadMovingUIState
369 const appendedElement = document.getElementById(uniqueId);371 const appendedElement = document.getElementById(uniqueId);
public/scripts/extensions/quick-reply/style.css+1 -0
@@ -350,6 +350,7 @@
350}350}
351.popup:has(#qr--modalEditor) {351.popup:has(#qr--modalEditor) {
352 aspect-ratio: unset;352 aspect-ratio: unset;
353 width: unset;
353}354}
354.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {355.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {
355 min-width: unset;356 min-width: unset;
public/scripts/extensions/quick-reply/style.less+1 -0
@@ -415,6 +415,7 @@
415415
416.popup:has(#qr--modalEditor) {416.popup:has(#qr--modalEditor) {
417 aspect-ratio: unset;417 aspect-ratio: unset;
418 width: unset;
418419
419 &:has(.qr--isExecuting.qr--minimized) {420 &:has(.qr--isExecuting.qr--minimized) {
420 min-width: unset;421 min-width: unset;
public/scripts/extensions/tts/cosyvoice.js+206 -0
@@ -0,0 +1,206 @@
1import { saveTtsProviderSettings } from './index.js';
2
3export { CosyVoiceProvider };
4
5class CosyVoiceProvider {
6 //########//
7 // Config //
8 //########//
9
10 settings;
11 ready = false;
12 voices = [];
13 separator = '. ';
14 audioElement = document.createElement('audio');
15
16 /**
17 * Perform any text processing before passing to TTS engine.
18 * @param {string} text Input text
19 * @returns {string} Processed text
20 */
21 processText(text) {
22 return text;
23 }
24
25 audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
26
27 languageLabels = {
28 'Auto': 'auto',
29 };
30
31 langKey2LangCode = {
32 'zh': 'zh-CN',
33 'en': 'en-US',
34 'ja': 'ja-JP',
35 'ko': 'ko-KR',
36 };
37
38 modelTypes = {
39 CosyVoice: 'CosyVoice',
40 };
41
42 defaultSettings = {
43 provider_endpoint: 'http://localhost:9880',
44 format: 'wav',
45 lang: 'auto',
46 streaming: false,
47
48 };
49
50 get settingsHtml() {
51 let html = `
52
53 <label for="tts_endpoint">Provider Endpoint:</label>
54 <input id="tts_endpoint" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.provider_endpoint}"/>
55 <span>Windows users Use <a target="_blank" href="https://github.com/v3ucn/CosyVoice_For_Windows">CosyVoice_For_Windows</a>(Unofficial).</span><br/>
56 <span>Macos Users Use <a target="_blank" href="https://github.com/v3ucn/CosyVoice_for_MacOs">CosyVoice_for_MacOs</a>(Unofficial).</span><br/>
57 <br/>
58
59 `;
60
61 return html;
62 }
63
64 onSettingsChange() {
65 // Used when provider settings are updated from UI
66 this.settings.provider_endpoint = $('#tts_endpoint').val();
67
68
69 saveTtsProviderSettings();
70 this.changeTTSSettings();
71 }
72
73 async loadSettings(settings) {
74 // Pupulate Provider UI given input settings
75 if (Object.keys(settings).length == 0) {
76 console.info('Using default TTS Provider settings');
77 }
78
79 // Only accept keys defined in defaultSettings
80 this.settings = this.defaultSettings;
81
82 for (const key in settings) {
83 if (key in this.settings) {
84 this.settings[key] = settings[key];
85 } else {
86 console.debug(`Ignoring non-user-configurable setting: ${key}`);
87 }
88 }
89
90 // Set initial values from the settings
91 $('#tts_endpoint').val(this.settings.provider_endpoint);
92
93
94 await this.checkReady();
95
96 console.info('ITS: Settings loaded');
97 }
98
99 // Perform a simple readiness check by trying to fetch voiceIds
100 async checkReady() {
101 await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
102 }
103
104 async onRefreshClick() {
105 return;
106 }
107
108 //#################//
109 // TTS Interfaces //
110 //#################//
111
112 async getVoice(voiceName) {
113
114
115
116 if (this.voices.length == 0) {
117 this.voices = await this.fetchTtsVoiceObjects();
118 }
119
120
121
122 const match = this.voices.filter(
123 v => v.name == voiceName,
124 )[0];
125 console.log(match);
126 if (!match) {
127 throw `TTS Voice name ${voiceName} not found`;
128 }
129 return match;
130 }
131
132
133
134 async generateTts(text, voiceId) {
135 const response = await this.fetchTtsGeneration(text, voiceId);
136 return response;
137 }
138
139 //###########//
140 // API CALLS //
141 //###########//
142 async fetchTtsVoiceObjects() {
143 const response = await fetch(`${this.settings.provider_endpoint}/speakers`);
144 console.info(response);
145
146 if (!response.ok) {
147 throw new Error(`HTTP ${response.status}: ${await response.json()}`);
148 }
149 const responseJson = await response.json();
150
151
152 this.voices = responseJson;
153
154 return responseJson;
155 }
156
157 // Each time a parameter is changed, we change the configuration
158 async changeTTSSettings() {
159 }
160
161 /**
162 * Fetch TTS generation from the API.
163 * @param {string} inputText Text to generate TTS for
164 * @param {string} voiceId Voice ID to use (model_type&speaker_id))
165 * @returns {Promise<Response|string>} Fetch response
166 */
167 async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
168 console.info(`Generating new TTS for voice_id ${voiceId}`);
169
170 const streaming = this.settings.streaming;
171
172 const params = {
173 text: inputText,
174 speaker: voiceId,
175 };
176
177 if (streaming) {
178 params['streaming'] = 1;
179 }
180
181 const url = `${this.settings.provider_endpoint}/`;
182
183 const response = await fetch(
184 url,
185 {
186 method: 'POST',
187 headers: {
188 'Content-Type': 'application/json',
189 },
190 body: JSON.stringify(params), // Convert parameter objects to JSON strings
191 },
192 );
193 if (!response.ok) {
194 toastr.error(response.statusText, 'TTS Generation Failed');
195 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
196 }
197 return response;
198 }
199
200
201
202 // Interface not used
203 async fetchTtsFromHistory(history_item_id) {
204 return Promise.resolve(history_item_id);
205 }
206}
public/scripts/extensions/tts/gpt-sovits-v2.js+226 -0
@@ -0,0 +1,226 @@
1import { saveTtsProviderSettings } from './index.js';
2
3export { GptSovitsV2Provider };
4
5class GptSovitsV2Provider {
6 //########//
7 // Config //
8 //########//
9
10 settings;
11 ready = false;
12 voices = [];
13 separator = '. ';
14 audioElement = document.createElement('audio');
15
16 /**
17 * Perform any text processing before passing to TTS engine.
18 * @param {string} text Input text
19 * @returns {string} Processed text
20 */
21 processText(text) {
22 return text;
23 }
24
25 audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
26
27 languageLabels = {
28 'Auto': 'auto',
29 };
30
31 langKey2LangCode = {
32 'zh': 'zh-CN',
33 'en': 'en-US',
34 'ja': 'ja-JP',
35 'ko': 'ko-KR',
36 };
37
38
39 defaultSettings = {
40 provider_endpoint: 'http://localhost:9880',
41 format: 'wav',
42 lang: 'auto',
43 streaming: false,
44 text_lang: 'zh',
45 prompt_lang: 'zh',
46
47 };
48
49 get settingsHtml() {
50 let html = `
51
52 <label for="tts_endpoint">Provider Endpoint:</label>
53 <input id="tts_endpoint" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.provider_endpoint}"/>
54 <span>Use <a target="_blank" href="https://github.com/v3ucn/GPT-SoVITS-V2">GPT-SoVITS-V2</a>(Unofficial).</span><br/>
55 <label for="text_lang">Text Lang(Inference text language):</label>
56 <input id="text_lang" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.text_lang}"/>
57 <label for="text_lang">Prompt Lang(Reference audio text language):</label>
58 <input id="prompt_lang" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.prompt_lang}"/>
59 <br/>
60
61 `;
62
63 return html;
64 }
65
66 onSettingsChange() {
67 // Used when provider settings are updated from UI
68 this.settings.provider_endpoint = $('#tts_endpoint').val();
69 this.settings.text_lang = $('#text_lang').val();
70 this.settings.prompt_lang = $('#prompt_lang').val();
71
72
73 saveTtsProviderSettings();
74 this.changeTTSSettings();
75 }
76
77 async loadSettings(settings) {
78 // Pupulate Provider UI given input settings
79 if (Object.keys(settings).length == 0) {
80 console.info('Using default TTS Provider settings');
81 }
82
83 // Only accept keys defined in defaultSettings
84 this.settings = this.defaultSettings;
85
86 for (const key in settings) {
87 if (key in this.settings) {
88 this.settings[key] = settings[key];
89 } else {
90 console.debug(`Ignoring non-user-configurable setting: ${key}`);
91 }
92 }
93
94 // Set initial values from the settings
95 $('#tts_endpoint').val(this.settings.provider_endpoint);
96 $('#text_lang').val(this.settings.text_lang);
97 $('#prompt_lang').val(this.settings.prompt_lang);
98
99
100 await this.checkReady();
101
102 console.info('ITS: Settings loaded');
103 }
104
105 // Perform a simple readiness check by trying to fetch voiceIds
106 async checkReady() {
107 await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
108 }
109
110 async onRefreshClick() {
111 return;
112 }
113
114 //#################//
115 // TTS Interfaces //
116 //#################//
117
118 async getVoice(voiceName) {
119
120
121
122 if (this.voices.length == 0) {
123 this.voices = await this.fetchTtsVoiceObjects();
124 }
125
126
127
128 const match = this.voices.filter(
129 v => v.name == voiceName,
130 )[0];
131 console.log(match);
132 if (!match) {
133 throw `TTS Voice name ${voiceName} not found`;
134 }
135 return match;
136 }
137
138
139
140 async generateTts(text, voiceId) {
141 const response = await this.fetchTtsGeneration(text, voiceId);
142 return response;
143 }
144
145 //###########//
146 // API CALLS //
147 //###########//
148 async fetchTtsVoiceObjects() {
149 const response = await fetch(`${this.settings.provider_endpoint}/speakers`);
150 console.info(response);
151
152 if (!response.ok) {
153 throw new Error(`HTTP ${response.status}: ${await response.json()}`);
154 }
155 const responseJson = await response.json();
156
157
158 this.voices = responseJson;
159
160 return responseJson;
161 }
162
163 // Each time a parameter is changed, we change the configuration
164 async changeTTSSettings() {
165 }
166
167 /**
168 * Fetch TTS generation from the API.
169 * @param {string} inputText Text to generate TTS for
170 * @param {string} voiceId Voice ID to use (model_type&speaker_id))
171 * @returns {Promise<Response|string>} Fetch response
172 */
173
174
175
176
177 async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
178 console.info(`Generating new TTS for voice_id ${voiceId}`);
179
180 function replaceSpeaker(text) {
181 return text.replace(/\[.*?\]/gu, '');
182 }
183
184 let prompt_text = replaceSpeaker(voiceId);
185
186 const streaming = this.settings.streaming;
187
188 const params = {
189 text: inputText,
190 prompt_text: prompt_text,
191 ref_audio_path: './参考音频/' + voiceId + '.wav',
192 text_lang: this.settings.text_lang,
193 prompt_lang: this.settings.prompt_lang,
194 text_split_method: 'cut5',
195 batch_size: 1,
196 media_type: 'ogg',
197 streaming_mode: 'true',
198 };
199
200
201 const url = `${this.settings.provider_endpoint}/`;
202
203 const response = await fetch(
204 url,
205 {
206 method: 'POST',
207 headers: {
208 'Content-Type': 'application/json',
209 },
210 body: JSON.stringify(params), // Convert parameter objects to JSON strings
211 },
212 );
213 if (!response.ok) {
214 toastr.error(response.statusText, 'TTS Generation Failed');
215 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
216 }
217 return response;
218 }
219
220
221
222 // Interface not used
223 async fetchTtsFromHistory(history_item_id) {
224 return Promise.resolve(history_item_id);
225 }
226}
public/scripts/extensions/tts/index.js+4 -0
@@ -4,6 +4,7 @@ import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '.
4import { EdgeTtsProvider } from './edge.js';4import { EdgeTtsProvider } from './edge.js';
5import { ElevenLabsTtsProvider } from './elevenlabs.js';5import { ElevenLabsTtsProvider } from './elevenlabs.js';
6import { SileroTtsProvider } from './silerotts.js';6import { SileroTtsProvider } from './silerotts.js';
7import { GptSovitsV2Provider } from './gpt-sovits-v2.js';
7import { CoquiTtsProvider } from './coqui.js';8import { CoquiTtsProvider } from './coqui.js';
8import { SystemTtsProvider } from './system.js';9import { SystemTtsProvider } from './system.js';
9import { NovelTtsProvider } from './novel.js';10import { NovelTtsProvider } from './novel.js';
@@ -15,6 +16,7 @@ import { VITSTtsProvider } from './vits.js';
15import { GSVITtsProvider } from './gsvi.js';16import { GSVITtsProvider } from './gsvi.js';
16import { SBVits2TtsProvider } from './sbvits2.js';17import { SBVits2TtsProvider } from './sbvits2.js';
17import { AllTalkTtsProvider } from './alltalk.js';18import { AllTalkTtsProvider } from './alltalk.js';
19import { CosyVoiceProvider } from './cosyvoice.js';
18import { SpeechT5TtsProvider } from './speecht5.js';20import { SpeechT5TtsProvider } from './speecht5.js';
19import { AzureTtsProvider } from './azure.js';21import { AzureTtsProvider } from './azure.js';
20import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';22import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -86,9 +88,11 @@ const ttsProviders = {
86 AllTalk: AllTalkTtsProvider,88 AllTalk: AllTalkTtsProvider,
87 Azure: AzureTtsProvider,89 Azure: AzureTtsProvider,
88 Coqui: CoquiTtsProvider,90 Coqui: CoquiTtsProvider,
91 'CosyVoice (Unofficial)': CosyVoiceProvider,
89 Edge: EdgeTtsProvider,92 Edge: EdgeTtsProvider,
90 ElevenLabs: ElevenLabsTtsProvider,93 ElevenLabs: ElevenLabsTtsProvider,
91 GSVI: GSVITtsProvider,94 GSVI: GSVITtsProvider,
95 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
92 Novel: NovelTtsProvider,96 Novel: NovelTtsProvider,
93 OpenAI: OpenAITtsProvider,97 OpenAI: OpenAITtsProvider,
94 'OpenAI Compatible': OpenAICompatibleTtsProvider,98 'OpenAI Compatible': OpenAICompatibleTtsProvider,
public/scripts/nai-settings.js+30 -6
@@ -74,7 +74,7 @@ const samplers = {
7474
75let novel_data = null;75let novel_data = null;
76let badWordsCache = {};76let badWordsCache = {};
77const BIAS_KEY = '#novel_api-settings';77const BIAS_KEY = '#range_block_novel';
7878
79export function setNovelData(data) {79export function setNovelData(data) {
80 novel_data = data;80 novel_data = data;
@@ -492,11 +492,37 @@ function getBadWordPermutations(text) {
492492
493export function getNovelGenerationData(finalPrompt, settings, maxLength, isImpersonate, isContinue, _cfgValues, type) {493export function getNovelGenerationData(finalPrompt, settings, maxLength, isImpersonate, isContinue, _cfgValues, type) {
494 console.debug('NovelAI generation data for', type);494 console.debug('NovelAI generation data for', type);
495 const isKayra = nai_settings.model_novel.includes('kayra');
496 const isErato = nai_settings.model_novel.includes('erato');
495497
496 const tokenizerType = getTokenizerTypeForModel(nai_settings.model_novel);498 const tokenizerType = getTokenizerTypeForModel(nai_settings.model_novel);
499 const stoppingStrings = getStoppingStrings(isImpersonate, isContinue);
500
501 // Llama 3 tokenizer, huh?
502 if (isErato) {
503 const additionalStopStrings = [];
504 for (const stoppingString of stoppingStrings) {
505 if (stoppingString.startsWith('\n')) {
506 additionalStopStrings.push('.' + stoppingString);
507 additionalStopStrings.push('!' + stoppingString);
508 additionalStopStrings.push('?' + stoppingString);
509 additionalStopStrings.push('*' + stoppingString);
510 additionalStopStrings.push('"' + stoppingString);
511 additionalStopStrings.push('_' + stoppingString);
512 additionalStopStrings.push('...' + stoppingString);
513 additionalStopStrings.push('."' + stoppingString);
514 additionalStopStrings.push('?"' + stoppingString);
515 additionalStopStrings.push('!"' + stoppingString);
516 additionalStopStrings.push('.*' + stoppingString);
517 additionalStopStrings.push(')' + stoppingString);
518 }
519 }
520 stoppingStrings.push(...additionalStopStrings);
521 }
522
523 const MAX_STOP_SEQUENCES = 1024;
497 const stopSequences = (tokenizerType !== tokenizers.NONE)524 const stopSequences = (tokenizerType !== tokenizers.NONE)
498 ? getStoppingStrings(isImpersonate, isContinue)525 ? stoppingStrings.slice(0, MAX_STOP_SEQUENCES).map(t => getTextTokens(tokenizerType, t))
499 .map(t => getTextTokens(tokenizerType, t))
500 : undefined;526 : undefined;
501527
502 const badWordIds = (tokenizerType !== tokenizers.NONE)528 const badWordIds = (tokenizerType !== tokenizers.NONE)
@@ -515,11 +541,9 @@ export function getNovelGenerationData(finalPrompt, settings, maxLength, isImper
515 console.log(finalPrompt);541 console.log(finalPrompt);
516 }542 }
517543
518 const isKayra = nai_settings.model_novel.includes('kayra');
519 const isErato = nai_settings.model_novel.includes('erato');
520544
521 if (isErato) {545 if (isErato) {
522 finalPrompt = '<|startoftext|>' + finalPrompt;546 finalPrompt = '<|startoftext|><|reserved_special_token81|>' + finalPrompt;
523 }547 }
524548
525 const adjustedMaxLength = (isKayra || isErato) ? getKayraMaxResponseTokens() : maximum_output_length;549 const adjustedMaxLength = (isKayra || isErato) ? getKayraMaxResponseTokens() : maximum_output_length;
public/scripts/slash-commands.js+1 -1
@@ -3469,7 +3469,7 @@ function getModelOptions(quiet) {
3469 case 'openai':3469 case 'openai':
3470 return oai_settings.chat_completion_source;3470 return oai_settings.chat_completion_source;
3471 default:3471 default:
3472 return nullResult;3472 return null;
3473 }3473 }
3474 }3474 }
34753475
public/scripts/sysprompt.js+1 -1
@@ -143,7 +143,7 @@ function selectSystemPromptCallback(args, name) {
143 foundName = result[0].item;143 foundName = result[0].item;
144 }144 }
145145
146 $select.val(foundName).trigger('input');146 $select.val(foundName).trigger('change');
147 !quiet && toastr.success(`System prompt "${foundName}" selected`);147 !quiet && toastr.success(`System prompt "${foundName}" selected`);
148 return foundName;148 return foundName;
149}149}
public/scripts/textgen-settings.js+27 -0
@@ -68,6 +68,10 @@ const LLAMACPP_DEFAULT_ORDER = [
68 'temperature',68 'temperature',
69];69];
70const OOBA_DEFAULT_ORDER = [70const OOBA_DEFAULT_ORDER = [
71 'repetition_penalty',
72 'presence_penalty',
73 'frequency_penalty',
74 'dry',
71 'temperature',75 'temperature',
72 'dynamic_temperature',76 'dynamic_temperature',
73 'quadratic_sampling',77 'quadratic_sampling',
@@ -80,6 +84,9 @@ const OOBA_DEFAULT_ORDER = [
80 'top_a',84 'top_a',
81 'min_p',85 'min_p',
82 'mirostat',86 'mirostat',
87 'xtc',
88 'encoder_repetition_penalty',
89 'no_repeat_ngram',
83];90];
84const BIAS_KEY = '#textgenerationwebui_api-settings';91const BIAS_KEY = '#textgenerationwebui_api-settings';
8592
@@ -798,6 +805,25 @@ function showTypeSpecificControls(type) {
798 });805 });
799}806}
800807
808/**
809 * Inserts missing items from the source array into the target array.
810 * @param {any[]} source - Source array
811 * @param {any[]} target - Target array
812 * @returns {void}
813 */
814function insertMissingArrayItems(source, target) {
815 if (source === target || !Array.isArray(source) || !Array.isArray(target)) {
816 return;
817 }
818
819 for (const item of source) {
820 if (!target.includes(item)) {
821 const index = source.indexOf(item);
822 target.splice(index, 0, item);
823 }
824 }
825}
826
801function setSettingByName(setting, value, trigger) {827function setSettingByName(setting, value, trigger) {
802 if (value === null || value === undefined) {828 if (value === null || value === undefined) {
803 return;829 return;
@@ -812,6 +838,7 @@ function setSettingByName(setting, value, trigger) {
812838
813 if ('sampler_priority' === setting) {839 if ('sampler_priority' === setting) {
814 value = Array.isArray(value) ? value : OOBA_DEFAULT_ORDER;840 value = Array.isArray(value) ? value : OOBA_DEFAULT_ORDER;
841 insertMissingArrayItems(OOBA_DEFAULT_ORDER, value);
815 sortOobaItemsByOrder(value);842 sortOobaItemsByOrder(value);
816 settings.sampler_priority = value;843 settings.sampler_priority = value;
817 return;844 return;
public/scripts/variables.js+208 -124
@@ -1,5 +1,6 @@
1import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';1import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
2import { extension_settings, saveMetadataDebounced } from './extensions.js';2import { extension_settings, saveMetadataDebounced } from './extensions.js';
3import { callGenericPopup, POPUP_TYPE } from './popup.js';
3import { executeSlashCommandsWithOptions } from './slash-commands.js';4import { executeSlashCommandsWithOptions } from './slash-commands.js';
4import { SlashCommand } from './slash-commands/SlashCommand.js';5import { SlashCommand } from './slash-commands/SlashCommand.js';
5import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';6import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
@@ -11,7 +12,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
11import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
12import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
13import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';14import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
14import { isFalseBoolean, convertValueType } from './utils.js';15import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1516
16/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */17/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */
17/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */18/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */
@@ -303,24 +304,48 @@ export function replaceVariableMacros(input) {
303 return lines.join('\n');304 return lines.join('\n');
304}305}
305306
306function listVariablesCallback() {307async function listVariablesCallback(args) {
308 const type = String(args?.format || '').toLowerCase().trim() || 'popup';
309 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
307 if (!chat_metadata.variables) {310 if (!chat_metadata.variables) {
308 chat_metadata.variables = {};311 chat_metadata.variables = {};
309 }312 }
310313
311 const localVariables = Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`);314 const includeLocalVariables = scope === 'all' || scope === 'local';
312 const globalVariables = Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`);315 const includeGlobalVariables = scope === 'all' || scope === 'global';
316
317 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
318 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
319
320 const jsonVariables = [
321 ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
322 ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
323 ];
313324
314 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';325 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
315 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';326 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
316 const chatName = getCurrentChatId();327 const chatName = getCurrentChatId();
317328
318 const converter = new showdown.Converter();329 const converter = new showdown.Converter();
319 const message = `### Local variables (${chatName}):\n${localVariablesString}\n\n### Global variables:\n${globalVariablesString}`;330 const message = [
331 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
332 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
333 ].filter(x => x).join('\n\n');
320 const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));334 const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));
321335
322 sendSystemMessage(system_message_types.GENERIC, htmlMessage);336 switch (type) {
323 return '';337 case 'none':
338 break;
339 case 'chat':
340 sendSystemMessage(system_message_types.GENERIC, htmlMessage);
341 break;
342 case 'popup':
343 default:
344 await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
345 break;
346 }
347
348 return JSON.stringify(jsonVariables);
324}349}
325350
326/**351/**
@@ -463,7 +488,7 @@ function existsGlobalVariable(name) {
463/**488/**
464 * Parses boolean operands from command arguments.489 * Parses boolean operands from command arguments.
465 * @param {object} args Command arguments490 * @param {object} args Command arguments
466 * @returns {{a: string | number, b: string | number, rule: string}} Boolean operands491 * @returns {{a: string | number, b: string | number?, rule: string}} Boolean operands
467 */492 */
468export function parseBooleanOperands(args) {493export function parseBooleanOperands(args) {
469 // Resolution order: numeric literal, local variable, global variable, string literal494 // Resolution order: numeric literal, local variable, global variable, string literal
@@ -472,6 +497,9 @@ export function parseBooleanOperands(args) {
472 */497 */
473 function getOperand(operand) {498 function getOperand(operand) {
474 if (operand === undefined) {499 if (operand === undefined) {
500 return undefined;
501 }
502 if (operand === '') {
475 return '';503 return '';
476 }504 }
477505
@@ -500,8 +528,8 @@ export function parseBooleanOperands(args) {
500 return stringLiteral || '';528 return stringLiteral || '';
501 }529 }
502530
503 const left = getOperand(args.a || args.left || args.first || args.x);531 const left = getOperand(args.a ?? args.left ?? args.first ?? args.x);
504 const right = getOperand(args.b || args.right || args.second || args.y);532 const right = getOperand(args.b ?? args.right ?? args.second ?? args.y);
505 const rule = args.rule;533 const rule = args.rule;
506534
507 return { a: left, b: right, rule };535 return { a: left, b: right, rule };
@@ -509,84 +537,79 @@ export function parseBooleanOperands(args) {
509537
510/**538/**
511 * Evaluates a boolean comparison rule.539 * Evaluates a boolean comparison rule.
512 * @param {string} rule Boolean comparison rule540 *
541 * @param {string?} rule Boolean comparison rule
513 * @param {string|number} a The left operand542 * @param {string|number} a The left operand
514 * @param {string|number} b The right operand543 * @param {string|number?} b The right operand
515 * @returns {boolean} True if the rule yields true, false otherwise544 * @returns {boolean} True if the rule yields true, false otherwise
516 */545 */
517export function evalBoolean(rule, a, b) {546export function evalBoolean(rule, a, b) {
518 if (!rule) {547 if (a === undefined) {
519 toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');548 throw new Error('Left operand is not provided');
520 throw new Error('Invalid command.');549 }
550
551 // If right-hand side was not provided, whe just check if the left side is truthy
552 if (b === undefined) {
553 switch (rule) {
554 case undefined:
555 case 'not': {
556 const resultOnTruthy = rule !== 'not';
557 if (isTrueBoolean(String(a))) return resultOnTruthy;
558 if (isFalseBoolean(String(a))) return !resultOnTruthy;
559 return a ? resultOnTruthy : !resultOnTruthy;
560 }
561 default:
562 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}`);
563 }
521 }564 }
522565
523 let result = false;566 // If no rule was provided, we are implicitly using 'eq', as defined for the slash commands
567 rule ??= 'eq';
568
524 if (typeof a === 'number' && typeof b === 'number') {569 if (typeof a === 'number' && typeof b === 'number') {
525 // only do numeric comparison if both operands are numbers570 // only do numeric comparison if both operands are numbers
526 const aNumber = Number(a);571 const aNumber = Number(a);
527 const bNumber = Number(b);572 const bNumber = Number(b);
528573
529 switch (rule) {574 switch (rule) {
530 case 'not':
531 result = !aNumber;
532 break;
533 case 'gt':575 case 'gt':
534 result = aNumber > bNumber;576 return aNumber > bNumber;
535 break;
536 case 'gte':577 case 'gte':
537 result = aNumber >= bNumber;578 return aNumber >= bNumber;
538 break;
539 case 'lt':579 case 'lt':
540 result = aNumber < bNumber;580 return aNumber < bNumber;
541 break;
542 case 'lte':581 case 'lte':
543 result = aNumber <= bNumber;582 return aNumber <= bNumber;
544 break;
545 case 'eq':583 case 'eq':
546 result = aNumber === bNumber;584 return aNumber === bNumber;
547 break;
548 case 'neq':585 case 'neq':
549 result = aNumber !== bNumber;586 return aNumber !== bNumber;
550 break;
551 default:
552 toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');
553 throw new Error('Invalid command.');
554 }
555 } else {
556 // otherwise do case-insensitive string comparsion, stringify non-strings
557 let aString;
558 let bString;
559 if (typeof a == 'string') {
560 aString = a.toLowerCase();
561 } else {
562 aString = JSON.stringify(a).toLowerCase();
563 }
564 if (typeof b == 'string') {
565 bString = b.toLowerCase();
566 } else {
567 bString = JSON.stringify(b).toLowerCase();
568 }
569
570 switch (rule) {
571 case 'in':587 case 'in':
572 result = aString.includes(bString);
573 break;
574 case 'nin':588 case 'nin':
575 result = !aString.includes(bString);589 // Fall through to string comparison. Otherwise you could not check if 12345 contains 45 for example.
576 break;590 console.debug(`Boolean comparison rule '${rule}' is not supported for type number. Falling back to string comparison.`);
577 case 'eq':
578 result = aString === bString;
579 break;
580 case 'neq':
581 result = aString !== bString;
582 break;591 break;
583 default:592 default:
584 toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');593 throw new Error(`Unknown boolean comparison rule for type number. Accepted: gt, gte, lt, lte, eq, neq. Provided: ${rule}`);
585 throw new Error('Invalid command.');
586 }594 }
587 }595 }
588596
589 return result;597 // otherwise do case-insensitive string comparsion, stringify non-strings
598 let aString = (typeof a === 'string') ? a.toLowerCase() : JSON.stringify(a).toLowerCase();
599 let bString = (typeof b === 'string') ? b.toLowerCase() : JSON.stringify(b).toLowerCase();
600
601 switch (rule) {
602 case 'in':
603 return aString.includes(bString);
604 case 'nin':
605 return !aString.includes(bString);
606 case 'eq':
607 return aString === bString;
608 case 'neq':
609 return aString !== bString;
610 default:
611 throw new Error(`Unknown boolean comparison rule for type number. Accepted: in, nin, eq, neq. Provided: ${rule}`);
612 }
590}613}
591614
592/**615/**
@@ -887,7 +910,35 @@ export function registerVariableCommands() {
887 name: 'listvar',910 name: 'listvar',
888 callback: listVariablesCallback,911 callback: listVariablesCallback,
889 aliases: ['listchatvar'],912 aliases: ['listchatvar'],
890 helpString: 'List registered chat variables.',913 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>format</code> argument to change the output format.',
914 returns: 'JSON list of local variables',
915 namedArgumentList: [
916 SlashCommandNamedArgument.fromProps({
917 name: 'scope',
918 description: 'filter variables by scope',
919 typeList: [ARGUMENT_TYPE.STRING],
920 defaultValue: 'all',
921 isRequired: false,
922 forceEnum: true,
923 enumList: [
924 new SlashCommandEnumValue('all', 'All variables', enumTypes.enum, enumIcons.variable),
925 new SlashCommandEnumValue('local', 'Local variables', enumTypes.enum, enumIcons.localVariable),
926 new SlashCommandEnumValue('global', 'Global variables', enumTypes.enum, enumIcons.globalVariable),
927 ],
928 }),
929 SlashCommandNamedArgument.fromProps({
930 name: 'format',
931 description: 'output format',
932 typeList: [ARGUMENT_TYPE.STRING],
933 isRequired: true,
934 forceEnum: true,
935 enumList: [
936 new SlashCommandEnumValue('popup', 'Show variables in a popup.', enumTypes.enum, enumIcons.default),
937 new SlashCommandEnumValue('chat', 'Post a system message to the chat.', enumTypes.enum, enumIcons.message),
938 new SlashCommandEnumValue('none', 'Just return the variables as a JSON list.', enumTypes.enum, enumIcons.array),
939 ],
940 }),
941 ],
891 }));942 }));
892 SlashCommandParser.addCommandObject(SlashCommand.fromProps({943 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
893 name: 'setvar',944 name: 'setvar',
@@ -1264,32 +1315,36 @@ export function registerVariableCommands() {
1264 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],1315 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1265 isRequired: true,1316 isRequired: true,
1266 enumProvider: commonEnumProviders.variables('all'),1317 enumProvider: commonEnumProviders.variables('all'),
1267 forceEnum: false,
1268 }),1318 }),
1269 SlashCommandNamedArgument.fromProps({1319 SlashCommandNamedArgument.fromProps({
1270 name: 'right',1320 name: 'right',
1271 description: 'right operand',1321 description: 'right operand',
1272 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],1322 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1273 isRequired: true,
1274 enumProvider: commonEnumProviders.variables('all'),1323 enumProvider: commonEnumProviders.variables('all'),
1275 forceEnum: false,
1276 }),1324 }),
1277 new SlashCommandNamedArgument(1325 SlashCommandNamedArgument.fromProps({
1278 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [1326 name: 'rule',
1279 new SlashCommandEnumValue('gt', 'a > b'),1327 description: 'comparison rule',
1280 new SlashCommandEnumValue('gte', 'a >= b'),1328 typeList: [ARGUMENT_TYPE.STRING],
1281 new SlashCommandEnumValue('lt', 'a < b'),1329 defaultValue: 'eq',
1282 new SlashCommandEnumValue('lte', 'a <= b'),1330 enumList: [
1283 new SlashCommandEnumValue('eq', 'a == b'),1331 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
1284 new SlashCommandEnumValue('neq', 'a !== b'),1332 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
1285 new SlashCommandEnumValue('not', '!a'),1333 new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
1286 new SlashCommandEnumValue('in', 'a includes b'),1334 new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
1287 new SlashCommandEnumValue('nin', 'a not includes b'),1335 new SlashCommandEnumValue('gt', 'a > b (numbers)'),
1336 new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1337 new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1338 new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1339 new SlashCommandEnumValue('not', '!a (truthy)'),
1288 ],1340 ],
1289 ),1341 forceEnum: true,
1290 new SlashCommandNamedArgument(1342 }),
1291 'else', 'command to execute if not true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], false,1343 SlashCommandNamedArgument.fromProps({
1292 ),1344 name: 'else',
1345 description: 'command to execute if not true',
1346 typeList: [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND],
1347 }),
1293 ],1348 ],
1294 unnamedArgumentList: [1349 unnamedArgumentList: [
1295 new SlashCommandArgument(1350 new SlashCommandArgument(
@@ -1307,17 +1362,25 @@ export function registerVariableCommands() {
1307 Numeric values and string literals for left and right operands supported.1362 Numeric values and string literals for left and right operands supported.
1308 </div>1363 </div>
1309 <div>1364 <div>
1365 If the rule is not provided, it defaults to <code>eq</code>.
1366 </div>
1367 <div>
1368 If no right operand is provided, it defaults to checking the <code>left</code> value to be truthy.
1369 A non-empty string or non-zero number is considered truthy, as is the value <code>true</code> or <code>on</code>.<br />
1370 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.
1371 </div>
1372 <div>
1310 <strong>Available rules:</strong>1373 <strong>Available rules:</strong>
1311 <ul>1374 <ul>
1312 <li>gt => a > b</li>1375 <li><code>eq</code> => a == b <small>(strings & numbers)</small></li>
1313 <li>gte => a >= b</li>1376 <li><code>neq</code> => a !== b <small>(strings & numbers)</small></li>
1314 <li>lt => a < b</li>1377 <li><code>in</code> => a includes b <small>(strings & numbers as strings)</small></li>
1315 <li>lte => a <= b</li>1378 <li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
1316 <li>eq => a == b</li>1379 <li><code>gt</code> => a > b <small>(numbers)</small></li>
1317 <li>neq => a != b</li>1380 <li><code>gte</code> => a >= b <small>(numbers)</small></li>
1318 <li>not => !a</li>1381 <li><code>lt</code> => a < b <small>(numbers)</small></li>
1319 <li>in (strings) => a includes b</li>1382 <li><code>lte</code> => a <= b <small>(numbers)</small></li>
1320 <li>nin (strings) => a not includes b</li>1383 <li><code>not</code> => !a <small>(truthy)</small></li>
1321 </ul>1384 </ul>
1322 </div>1385 </div>
1323 <div>1386 <div>
@@ -1327,6 +1390,17 @@ export function registerVariableCommands() {
1327 <pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre>1390 <pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre>
1328 triggers a /speak command if the value of "score" is greater or equals 10.1391 triggers a /speak command if the value of "score" is greater or equals 10.
1329 </li>1392 </li>
1393 <li>
1394 <pre><code class="language-stscript">/if left={{lastMessage}} rule=in right=surprise {: /echo SURPISE! :}</code></pre>
1395 executes a subcommand defined as a closure if the given value contains a specified word.
1396 <li>
1397 <pre><code class="language-stscript">/if left=myContent {: /echo My content had some content. :}</code></pre>
1398 executes the defined subcommand, if the provided value of left is truthy (contains some kind of contant that is not empty or false)
1399 </li>
1400 <li>
1401 <pre><code class="language-stscript">/if left=tree right={{getvar::object}} {: /echo The object is a tree! :}</code></pre>
1402 executes the defined subcommand, if the left and right values are equals.
1403 </li>
1330 </ul>1404 </ul>
1331 </div>1405 </div>
1332 `,1406 `,
@@ -1342,32 +1416,38 @@ export function registerVariableCommands() {
1342 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],1416 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1343 isRequired: true,1417 isRequired: true,
1344 enumProvider: commonEnumProviders.variables('all'),1418 enumProvider: commonEnumProviders.variables('all'),
1345 forceEnum: false,
1346 }),1419 }),
1347 SlashCommandNamedArgument.fromProps({1420 SlashCommandNamedArgument.fromProps({
1348 name: 'right',1421 name: 'right',
1349 description: 'right operand',1422 description: 'right operand',
1350 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],1423 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1351 isRequired: true,
1352 enumProvider: commonEnumProviders.variables('all'),1424 enumProvider: commonEnumProviders.variables('all'),
1353 forceEnum: false,
1354 }),1425 }),
1355 new SlashCommandNamedArgument(1426 SlashCommandNamedArgument.fromProps({
1356 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [1427 name: 'rule',
1357 new SlashCommandEnumValue('gt', 'a > b'),1428 description: 'comparison rule',
1358 new SlashCommandEnumValue('gte', 'a >= b'),1429 typeList: [ARGUMENT_TYPE.STRING],
1359 new SlashCommandEnumValue('lt', 'a < b'),1430 defaultValue: 'eq',
1360 new SlashCommandEnumValue('lte', 'a <= b'),1431 enumList: [
1361 new SlashCommandEnumValue('eq', 'a == b'),1432 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
1362 new SlashCommandEnumValue('neq', 'a !== b'),1433 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
1363 new SlashCommandEnumValue('not', '!a'),1434 new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
1364 new SlashCommandEnumValue('in', 'a includes b'),1435 new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
1365 new SlashCommandEnumValue('nin', 'a not includes b'),1436 new SlashCommandEnumValue('gt', 'a > b (numbers)'),
1437 new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1438 new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1439 new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1440 new SlashCommandEnumValue('not', '!a (truthy)'),
1366 ],1441 ],
1367 ),1442 forceEnum: true,
1368 new SlashCommandNamedArgument(1443 }),
1369 'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),1444 SlashCommandNamedArgument.fromProps({
1370 ),1445 name: 'guard',
1446 description: 'disable loop iteration limit',
1447 typeList: [ARGUMENT_TYPE.STRING],
1448 defaultValue: 'off',
1449 enumList: commonEnumProviders.boolean('onOff')(),
1450 }),
1371 ],1451 ],
1372 unnamedArgumentList: [1452 unnamedArgumentList: [
1373 new SlashCommandArgument(1453 new SlashCommandArgument(
@@ -1386,15 +1466,15 @@ export function registerVariableCommands() {
1386 <div>1466 <div>
1387 <strong>Available rules:</strong>1467 <strong>Available rules:</strong>
1388 <ul>1468 <ul>
1389 <li>gt => a > b</li>1469 <li><code>eq</code> => a == b <small>(strings & numbers)</small></li>
1390 <li>gte => a >= b</li>1470 <li><code>neq</code> => a !== b <small>(strings & numbers)</small></li>
1391 <li>lt => a < b</li>1471 <li><code>in</code> => a includes b <small>(strings & numbers as strings)</small></li>
1392 <li>lte => a <= b</li>1472 <li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
1393 <li>eq => a == b</li>1473 <li><code>gt</code> => a > b <small>(numbers)</small></li>
1394 <li>neq => a != b</li>1474 <li><code>gte</code> => a >= b <small>(numbers)</small></li>
1395 <li>not => !a</li>1475 <li><code>lt</code> => a < b <small>(numbers)</small></li>
1396 <li>in (strings) => a includes b</li>1476 <li><code>lte</code> => a <= b <small>(numbers)</small></li>
1397 <li>nin (strings) => a not includes b</li>1477 <li><code>not</code> => !a <small>(truthy)</small></li>
1398 </ul>1478 </ul>
1399 </div>1479 </div>
1400 <div>1480 <div>
@@ -1404,7 +1484,11 @@ export function registerVariableCommands() {
1404 <pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>1484 <pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>
1405 adds 1 to the value of "i" until it reaches 10.1485 adds 1 to the value of "i" until it reaches 10.
1406 </li>1486 </li>
1407 </ul>1487 <li>
1488 <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>
1489 executes the defined subcommand as long as the "currentword" variable is truthy (has any content that is not false/empty)
1490 </ul>
1491 </li>
1408 </div>1492 </div>
1409 <div>1493 <div>
1410 Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.1494 Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.
@@ -1519,7 +1603,7 @@ export function registerVariableCommands() {
1519 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1603 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1520 isRequired: true,1604 isRequired: true,
1521 acceptsMultiple: true,1605 acceptsMultiple: true,
1522 enumProvider: (executor, scope)=>{1606 enumProvider: (executor, scope) => {
1523 const vars = commonEnumProviders.variables('all')(executor, scope);1607 const vars = commonEnumProviders.variables('all')(executor, scope);
1524 vars.push(1608 vars.push(
1525 new SlashCommandEnumValue(1609 new SlashCommandEnumValue(
@@ -1527,16 +1611,16 @@ export function registerVariableCommands() {
1527 null,1611 null,
1528 enumTypes.variable,1612 enumTypes.variable,
1529 enumIcons.variable,1613 enumIcons.variable,
1530 (input)=>/^\w*$/.test(input),1614 (input) => /^\w*$/.test(input),
1531 (input)=>input,1615 (input) => input,
1532 ),1616 ),
1533 new SlashCommandEnumValue(1617 new SlashCommandEnumValue(
1534 'any number',1618 'any number',
1535 null,1619 null,
1536 enumTypes.number,1620 enumTypes.number,
1537 enumIcons.number,1621 enumIcons.number,
1538 (input)=>input == '' || !Number.isNaN(Number(input)),1622 (input) => input == '' || !Number.isNaN(Number(input)),
1539 (input)=>input,1623 (input) => input,
1540 ),1624 ),
1541 );1625 );
1542 return vars;1626 return vars;
public/style.css+23 -9
@@ -3154,6 +3154,26 @@ grammarly-extension {
3154 align-items: center;3154 align-items: center;
3155}3155}
31563156
3157.alternate_greeting details {
3158 padding: 2px;
3159}
3160
3161.alternate_greeting summary {
3162 list-style-position: outside;
3163 margin-left: 1em;
3164 padding-left: 1em;
3165}
3166
3167.alternate_greeting textarea {
3168 field-sizing: content;
3169 max-height: 50dvh;
3170}
3171
3172.alternate_greeting summary::marker,
3173.alternate_greeting summary strong {
3174 cursor: pointer;
3175}
3176
3157#rm_characters_block .form_create_bottom_buttons_block {3177#rm_characters_block .form_create_bottom_buttons_block {
3158 justify-content: space-evenly !important;3178 justify-content: space-evenly !important;
3159 flex-grow: 0;3179 flex-grow: 0;
@@ -3245,8 +3265,9 @@ grammarly-extension {
3245}3265}
32463266
3247.wide_dialogue_popup {3267.wide_dialogue_popup {
3248 aspect-ratio: 1 / 1;3268 /* FIXME: Chrome 129 broke max-height for aspect-ratio sized elements */
3249 width: unset !important;3269 /* aspect-ratio: 1 / 1; */
3270 /* width: unset !important; */
3250 min-width: var(--sheldWidth);3271 min-width: var(--sheldWidth);
3251}3272}
32523273
@@ -5365,13 +5386,6 @@ body:not(.movingUI) .drawer-content.maximized {
53655386
5366/* Jank mobile support for gallery and future draggables */5387/* Jank mobile support for gallery and future draggables */
5367@media screen and (max-width: 1000px) {5388@media screen and (max-width: 1000px) {
5368 #gallery {
5369 display: block;
5370 width: 100vw;
5371 height: 100vh;
5372 z-index: 9999;
5373 }
5374
5375 .draggable {5389 .draggable {
5376 display: block;5390 display: block;
5377 width: 100vw;5391 width: 100vw;
src/users.js+6 -2
@@ -347,6 +347,8 @@ async function migrateSystemPrompts() {
347 if (fs.existsSync(migrateMarker)) {347 if (fs.existsSync(migrateMarker)) {
348 continue;348 continue;
349 }349 }
350 const backupsPath = path.join(directory.backups, '_sysprompt');
351 fs.mkdirSync(backupsPath, { recursive: true });
350 const defaultPrompts = await getDefaultSystemPrompts();352 const defaultPrompts = await getDefaultSystemPrompts();
351 const instucts = fs.readdirSync(directory.instruct);353 const instucts = fs.readdirSync(directory.instruct);
352 let migratedPrompts = [];354 let migratedPrompts = [];
@@ -356,6 +358,8 @@ async function migrateSystemPrompts() {
356 if (path.extname(instruct) === '.json' && !fs.existsSync(sysPromptPath)) {358 if (path.extname(instruct) === '.json' && !fs.existsSync(sysPromptPath)) {
357 const instructData = JSON.parse(fs.readFileSync(instructPath, 'utf8'));359 const instructData = JSON.parse(fs.readFileSync(instructPath, 'utf8'));
358 if ('system_prompt' in instructData && 'name' in instructData) {360 if ('system_prompt' in instructData && 'name' in instructData) {
361 const backupPath = path.join(backupsPath, `${instructData.name}.json`);
362 fs.cpSync(instructPath, backupPath, { force: true });
359 const syspromptData = { name: instructData.name, content: instructData.system_prompt };363 const syspromptData = { name: instructData.name, content: instructData.system_prompt };
360 migratedPrompts.push(syspromptData);364 migratedPrompts.push(syspromptData);
361 delete instructData.system_prompt;365 delete instructData.system_prompt;
@@ -374,8 +378,8 @@ async function migrateSystemPrompts() {
374 console.log(`Migrated system prompt ${sysPromptData.name} for ${directory.root.split(path.sep).pop()}`);378 console.log(`Migrated system prompt ${sysPromptData.name} for ${directory.root.split(path.sep).pop()}`);
375 }379 }
376 writeFileAtomicSync(migrateMarker, '');380 writeFileAtomicSync(migrateMarker, '');
377 } catch {381 } catch (error) {
378 // Ignore errors382 console.error('Error migrating system prompts:', error);
379 }383 }
380 }384 }
381}385}