Resolve conflict
| @@ -9,13 +9,34 @@ | ||
| 9 | 9 | "magical forest" |
| 10 | 10 | ], |
| 11 | 11 | "keysecondary": [], |
| 12 | 12 | "comment": "eldoria", |
| 13 | 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 | 14 | "constant": false, |
| 15 | 15 | "selective": falsetrue, |
| 16 | 16 | "order": 100, |
| 17 | 17 | "position": 0, |
| 18 | 18 | "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 | 41 | "1": { |
| 21 | 42 | "uid": 1, |
| @@ -27,13 +48,34 @@ | ||
| 27 | 48 | "beasts" |
| 28 | 49 | ], |
| 29 | 50 | "keysecondary": [], |
| 30 | 51 | "comment": "shadowfang", |
| 31 | 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 | 53 | "constant": false, |
| 33 | 54 | "selective": falsetrue, |
| 34 | 55 | "order": 100, |
| 35 | 56 | "position": 0, |
| 36 | 57 | "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 | 80 | "2": { |
| 39 | 81 | "uid": 2, |
| @@ -43,13 +85,34 @@ | ||
| 43 | 85 | "refuge" |
| 44 | 86 | ], |
| 45 | 87 | "keysecondary": [], |
| 46 | 88 | "comment": "glade", |
| 47 | 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 | 90 | "constant": false, |
| 49 | 91 | "selective": falsetrue, |
| 50 | 92 | "order": 100, |
| 51 | 93 | "position": 0, |
| 52 | 94 | "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 | 117 | "3": { |
| 55 | 118 | "uid": 3, |
| @@ -59,13 +122,34 @@ | ||
| 59 | 122 | "ability" |
| 60 | 123 | ], |
| 61 | 124 | "keysecondary": [], |
| 62 | 125 | "comment": "power", |
| 63 | 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 | 127 | "constant": false, |
| 65 | 128 | "selective": falsetrue, |
| 66 | 129 | "order": 100, |
| 67 | 130 | "position": 0, |
| 68 | 131 | "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 | } |
Binary file
| @@ -1310,7 +1310,7 @@ | ||
| 1310 | 1310 | </div> |
| 1311 | 1311 | </div> |
| 1312 | 1312 | |
| 1313 | 1313 | <div data-tg-type="koboldcpp, aphrodite, tabby, ooba" id="xtc_block" class="wide100p"> |
| 1314 | 1314 | <h4 class="wide100p textAlignCenter"> |
| 1315 | 1315 | <label data-i18n="Exclude Top Choices (XTC)">Exclude Top Choices (XTC)</label> |
| 1316 | 1316 | <a href="https://github.com/oobabooga/text-generation-webui/pull/6335" target="_blank"> |
| @@ -1679,6 +1679,10 @@ | ||
| 1679 | 1679 | Ooba only. Determines the order of samplers. |
| 1680 | 1680 | </div> |
| 1681 | 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 | 1686 | <div data-name="temperature" draggable="true"><span>Temperature</span><small></small></div> |
| 1683 | 1687 | <div data-name="dynamic_temperature" draggable="true"><span>Dynamic Temperature</span><small></small></div> |
| 1684 | 1688 | <div data-name="quadratic_sampling" draggable="true"><span>Quadratic / Smooth Sampling</span><small></small></div> |
| @@ -1691,6 +1695,9 @@ | ||
| 1691 | 1695 | <div data-name="top_a" draggable="true"><span>Top A</span><small></small></div> |
| 1692 | 1696 | <div data-name="min_p" draggable="true"><span>Min P</span><small></small></div> |
| 1693 | 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 | 1701 | </div> |
| 1695 | 1702 | <div id="textgenerationwebui_default_order" class="menu_button menu_button_icon"> |
| 1696 | 1703 | <span data-i18n="Load default order">Load default order</span> |
| @@ -6090,7 +6097,10 @@ | ||
| 6090 | 6097 | <div class="alternate_grettings flexFlowColumn flex-container"> |
| 6091 | 6098 | <div class="title_restorable"> |
| 6092 | 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 | 6104 | </div> |
| 6095 | 6105 | <small class="justifyLeft" data-i18n="Alternate_Greetings_desc"> |
| 6096 | 6106 | These will be displayed as swipes on the first message when starting a new chat. |
| @@ -6106,11 +6116,18 @@ | ||
| 6106 | 6116 | </div> |
| 6107 | 6117 | <div id="alternate_greeting_form_template" class="template_element"> |
| 6108 | 6118 | <div class="alternate_greeting"> |
| 6109 | 6119 | <divdetails class="title_restorable"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 | 6131 | </div> |
| 6115 | 6132 | </div> |
| 6116 | 6133 | |
| @@ -1719,7 +1719,6 @@ | ||
| 1719 | 1719 | "Model": "Модель", |
| 1720 | 1720 | "Proxy Preset": "Пресет для прокси", |
| 1721 | 1721 | "Enter a name:": "Введите название:", |
| 1722 | - "Enter a new name": "Введите новое название", | |
| 1723 | 1722 | "Are you sure you want to delete the selected profile?": "Вы точно хотите удалить выбранный профиль?", |
| 1724 | 1723 | "instruct_enabled": "Включить Instruct-режим", |
| 1725 | 1724 | "Instruct Template": "Шаблон Instruct-режима", |
| @@ -1746,5 +1745,7 @@ | ||
| 1746 | 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 | 1746 | "None (disabled)": "Нигде (откл.)", |
| 1748 | 1747 | "Markdown Hotkeys": "Горячие клавиши для разметки", |
| 1749 | 1748 | "markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'.", |
| 1749 | + "Save and Update": "Сохранить и обновить", | |
| 1750 | + "Profile name:": "Название профиля:" | |
| 1750 | 1751 | } |
| @@ -158,7 +158,7 @@ import { | ||
| 158 | 158 | } from './scripts/utils.js'; |
| 159 | 159 | import { debounce_timeout } from './scripts/constants.js'; |
| 160 | 160 | |
| 161 | 161 | import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, initExtensions, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js'; |
| 162 | 162 | import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js'; |
| 163 | 163 | import { |
| 164 | 164 | tag_map, |
| @@ -244,6 +244,7 @@ import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCo | ||
| 244 | 244 | import { initInputMarkdown } from './scripts/input-md-formatting.js'; |
| 245 | 245 | import { AbortReason } from './scripts/util/AbortReason.js'; |
| 246 | 246 | import { initSystemPrompts } from './scripts/sysprompt.js'; |
| 247 | +import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js'; | |
| 247 | 248 | |
| 248 | 249 | //exporting functions and vars for mods |
| 249 | 250 | export { |
| @@ -935,6 +936,8 @@ async function firstLoadInit() { | ||
| 935 | 936 | initTextGenModels(); |
| 936 | 937 | initOpenAI(); |
| 937 | 938 | initSystemPrompts(); |
| 939 | + initExtensions(); | |
| 940 | + initExtensionSlashCommands(); | |
| 938 | 941 | await initPresetManager(); |
| 939 | 942 | await getSystemMessages(); |
| 940 | 943 | sendSystemMessage(system_message_types.WELCOME); |
| @@ -2573,7 +2576,7 @@ export function getStoppingStrings(isImpersonate, isContinue) { | ||
| 2573 | 2576 | result.unshift('\n'); |
| 2574 | 2577 | } |
| 2575 | 2578 | |
| 2576 | 2579 | return result.filter(x => x).filter(onlyUnique); |
| 2577 | 2580 | } |
| 2578 | 2581 | |
| 2579 | 2582 | /** |
| @@ -4863,7 +4866,7 @@ export function getMaxContextSize(overrideResponseLength = null) { | ||
| 4863 | 4866 | this_max_context = Math.min(max_context, 8192); |
| 4864 | 4867 | |
| 4865 | 4868 | // Added special tokens and whatnot |
| 4866 | 4869 | this_max_context -= 110; |
| 4867 | 4870 | } |
| 4868 | 4871 | |
| 4869 | 4872 | this_max_context = this_max_context - (overrideResponseLength || amount_gen); |
| @@ -7390,7 +7393,7 @@ function onScenarioOverrideRemoveClick() { | ||
| 7390 | 7393 | */ |
| 7391 | 7394 | export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) { |
| 7392 | 7395 | function getOkButtonText() { |
| 7393 | 7396 | if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) { |
| 7394 | 7397 | $dialoguePopupCancel.css('display', 'none'); |
| 7395 | 7398 | return okButton ?? 'Ok'; |
| 7396 | 7399 | } else if (['delete_extension'].includes(popup_type)) { |
| @@ -7827,24 +7830,42 @@ function openAlternateGreetings() { | ||
| 7827 | 7830 | |
| 7828 | 7831 | const template = $('#alternate_greetings_template .alternate_grettings').clone(); |
| 7829 | 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 | + }); | |
| 7830 | 7843 | |
| 7831 | 7844 | for (let index = 0; index < getArray().length; index++) { |
| 7832 | 7845 | addAlternateGreeting(template, getArray()[index], index, getArray, popup); |
| 7833 | 7846 | } |
| 7834 | 7847 | |
| 7835 | 7848 | template.find('.add_alternate_greeting').on('click', function () { |
| 7836 | 7849 | const array = getArray(); |
| 7837 | 7850 | const index = array.length; |
| 7838 | 7851 | array.push(''); |
| 7839 | 7852 | addAlternateGreeting(template, '', index, getArray, popup); |
| 7840 | 7853 | updateAlternateGreetingsHintVisibility(template); |
| 7841 | 7854 | }); |
| 7842 | 7855 | |
| 7856 | + popup.show(); | |
| 7843 | 7857 | updateAlternateGreetingsHintVisibility(template); |
| 7844 | - callPopup(template, 'alternate_greeting', '', { wide: true, large: true }); | |
| 7845 | 7858 | } |
| 7846 | 7859 | |
| 7847 | -function 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 | + */ | |
| 7868 | +function addAlternateGreeting(template, greeting, index, getArray, popup) { | |
| 7848 | 7869 | const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone(); |
| 7849 | 7870 | greetingBlock.find('.alternate_greeting_text').on('input', async function () { |
| 7850 | 7871 | const value = $(this).val(); |
| @@ -7852,11 +7873,16 @@ function addAlternateGreeting(template, greeting, index, getArray) { | ||
| 7852 | 7873 | array[index] = value; |
| 7853 | 7874 | }).val(greeting); |
| 7854 | 7875 | greetingBlock.find('.greeting_index').text(index + 1); |
| 7855 | 7876 | greetingBlock.find('.delete_alternate_greeting').on('click', async function (event) { |
| 7877 | + event.preventDefault(); | |
| 7878 | + event.stopPropagation(); | |
| 7879 | + | |
| 7856 | 7880 | if (confirm(t`Are you sure you want to delete this alternate greeting?`)) { |
| 7857 | 7881 | const array = getArray(); |
| 7858 | 7882 | array.splice(index, 1); |
| 7883 | + | |
| 7859 | 7884 | // We need to reopen the popup to update the index numbers |
| 7885 | + await popup.complete(POPUP_RESULT.AFFIRMATIVE); | |
| 7860 | 7886 | openAlternateGreetings(); |
| 7861 | 7887 | } |
| 7862 | 7888 | }); |
| @@ -9574,9 +9600,6 @@ jQuery(async function () { | ||
| 9574 | 9600 | }, 2000); |
| 9575 | 9601 | } |
| 9576 | 9602 | } |
| 9577 | - if (popup_type == 'alternate_greeting' && menu_type !== 'create') { | |
| 9578 | - createOrEditCharacter(); | |
| 9579 | - } | |
| 9580 | 9603 | |
| 9581 | 9604 | if (dialogueResolve) { |
| 9582 | 9605 | if (popup_type == 'input') { |
| @@ -0,0 +1,320 @@ | ||
| 1 | +import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js'; | |
| 2 | +import { SlashCommand } from './slash-commands/SlashCommand.js'; | |
| 3 | +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; | |
| 4 | +import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; | |
| 5 | +import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | |
| 6 | +import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; | |
| 7 | +import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | |
| 8 | +import { 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 | + */ | |
| 14 | +function 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 | + */ | |
| 79 | +function 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 | + */ | |
| 91 | +const 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 | + | |
| 100 | +export 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 | +} | |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js'; |
| 2 | 2 | import { hideLoader, showLoader } from './loader.js'; |
| 3 | 3 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 4 | 4 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 5 | 5 | import { isSubsetOf, setValueByPath } from './utils.js'; |
| @@ -14,7 +14,9 @@ export { | ||
| 14 | 14 | ModuleWorkerWrapper, |
| 15 | 15 | }; |
| 16 | 16 | |
| 17 | +/** @type {string[]} */ | |
| 17 | 18 | export let extensionNames = []; |
| 19 | + | |
| 18 | 20 | let manifests = {}; |
| 19 | 21 | const defaultUrl = 'http://localhost:5100'; |
| 20 | 22 | |
| @@ -241,7 +243,7 @@ function onEnableExtensionClick() { | ||
| 241 | 243 | enableExtension(name, false); |
| 242 | 244 | } |
| 243 | 245 | |
| 244 | 246 | export async function enableExtension(name, reload = true) { |
| 245 | 247 | extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name); |
| 246 | 248 | stateChanged = true; |
| 247 | 249 | await saveSettings(); |
| @@ -252,7 +254,7 @@ async function enableExtension(name, reload = true) { | ||
| 252 | 254 | } |
| 253 | 255 | } |
| 254 | 256 | |
| 255 | 257 | export async function disableExtension(name, reload = true) { |
| 256 | 258 | extension_settings.disabledExtensions.push(name); |
| 257 | 259 | stateChanged = true; |
| 258 | 260 | await saveSettings(); |
| @@ -1041,7 +1043,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { | ||
| 1041 | 1043 | await installExtension(url); |
| 1042 | 1044 | } |
| 1043 | 1045 | |
| 1044 | -jQuery(async function () { | |
| 1046 | + | |
| 1047 | + | |
| 1048 | +export async function initExtensions() { | |
| 1045 | 1049 | await addExtensionsButtonAndMenu(); |
| 1046 | 1050 | $('#extensionsMenuButton').css('display', 'flex'); |
| 1047 | 1051 | |
| @@ -1060,4 +1064,4 @@ jQuery(async function () { | ||
| 1060 | 1064 | * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element. |
| 1061 | 1065 | */ |
| 1062 | 1066 | $('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu()); |
| 1063 | 1067 | }); |
| @@ -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> | |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; |
| 2 | 2 | import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 3 | 3 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; |
| 4 | 4 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 5 | 5 | import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js'; |
| 6 | 6 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| @@ -136,6 +136,7 @@ const profilesProvider = () => [ | ||
| 136 | 136 | * @property {string} [context] Context Template |
| 137 | 137 | * @property {string} [instruct-state] Instruct Mode |
| 138 | 138 | * @property {string} [tokenizer] Tokenizer |
| 139 | + * @property {string[]} [exclude] Commands to exclude | |
| 139 | 140 | */ |
| 140 | 141 | |
| 141 | 142 | /** |
| @@ -172,8 +173,13 @@ function findProfileByName(value) { | ||
| 172 | 173 | async function readProfileFromCommands(mode, profile, cleanUp = false) { |
| 173 | 174 | const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; |
| 174 | 175 | const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS; |
| 176 | + const excludeList = Array.isArray(profile.exclude) ? profile.exclude : []; | |
| 175 | 177 | for (const command of commands) { |
| 176 | 178 | try { |
| 179 | + if (excludeList.includes(command)) { | |
| 180 | + continue; | |
| 181 | + } | |
| 182 | + | |
| 177 | 183 | const args = getNamedArguments(); |
| 178 | 184 | const result = await SlashCommandParser.commands[command].callback(args, ''); |
| 179 | 185 | if (result) { |
| @@ -209,15 +215,37 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) { | ||
| 209 | 215 | async function createConnectionProfile(forceName = null) { |
| 210 | 216 | const mode = main_api === 'openai' ? 'cc' : 'tc'; |
| 211 | 217 | const id = uuidv4(); |
| 218 | + /** @type {ConnectionProfile} */ | |
| 212 | 219 | const profile = { |
| 213 | 220 | id, |
| 214 | 221 | mode, |
| 222 | + exclude: [], | |
| 215 | 223 | }; |
| 216 | 224 | |
| 217 | 225 | await readProfileFromCommands(mode, profile); |
| 218 | 226 | |
| 219 | 227 | const profileForDisplay = makeFancyProfile(profile); |
| 220 | 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 | 249 | const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); |
| 222 | 250 | const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken); |
| 223 | 251 | const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); |
| @@ -231,7 +259,13 @@ async function createConnectionProfile(forceName = null) { | ||
| 231 | 259 | return null; |
| 232 | 260 | } |
| 233 | 261 | |
| 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 | 269 | return profile; |
| 236 | 270 | } |
| 237 | 271 | |
| @@ -358,7 +392,11 @@ async function renderDetailsContent(detailsContent) { | ||
| 358 | 392 | const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); |
| 359 | 393 | if (profile) { |
| 360 | 394 | const profileForDisplay = makeFancyProfile(profile); |
| 361 | 395 | const templatetemplateParams = await renderExtensionTemplateAsync(MODULE_NAME, 'view', { 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 | 400 | detailsContent.innerHTML = template; |
| 363 | 401 | } else { |
| 364 | 402 | detailsContent.textContent = t`No profile selected`; |
| @@ -473,29 +511,71 @@ async function renderDetailsContent(detailsContent) { | ||
| 473 | 511 | await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); |
| 474 | 512 | }); |
| 475 | 513 | |
| 476 | 514 | const renameButtoneditButton = document.getElementById('rename_connection_profileedit_connection_profile'); |
| 477 | 515 | renameButtoneditButton.addEventListener('click', async () => { |
| 478 | 516 | const selectedProfile = extension_settings.connectionManager.selectedProfile; |
| 479 | 517 | const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); |
| 480 | 518 | if (!profile) { |
| 481 | 519 | console.log('No profile selected'); |
| 482 | 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 | + }); | |
| 484 | 545 | |
| 485 | - const newName = await Popup.show.input(t`Enter a new name`, null, profile.name, { rows: 2 }); | |
| 486 | 546 | if (!newName) { |
| 487 | 547 | return; |
| 488 | 548 | } |
| 489 | 549 | |
| 490 | 550 | if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) { |
| 491 | 551 | toastr.error('A profile with the same name already exists.'); |
| 492 | 552 | return; |
| 493 | 553 | } |
| 494 | 554 | |
| 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 | 576 | saveSettingsDebounced(); |
| 497 | 577 | renderConnectionProfiles(profiles); |
| 498 | - toastr.success('Connection profile renamed', '', { timeOut: 1500 }); | |
| 578 | + await renderDetailsContent(detailsContent); | |
| 499 | 579 | }); |
| 500 | 580 | |
| 501 | 581 | /** @type {HTMLElement} */ |
| @@ -2,11 +2,20 @@ | ||
| 2 | 2 | <h2 data-i18n="Creating a Connection Profile"> |
| 3 | 3 | Creating a Connection Profile |
| 4 | 4 | </h2> |
| 5 | 5 | <uldiv class="justifyLeft flex-container flexFlowColumn flexNoGap"> |
| 6 | 6 | {{#each profile}} |
| 7 | - <li><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</li> | |
| 7 | + <label class="checkbox_label"> | |
| 8 | + <input type="checkbox" value="{{@key}}" name="exclude" checked> | |
| 9 | + <span><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</span> | |
| 10 | + </label> | |
| 8 | 11 | {{/each}} |
| 9 | 12 | </uldiv> |
| 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 | 19 | <h3 data-i18n="Enter a name:"> |
| 11 | 20 | Enter a name: |
| 12 | 21 | </h3> |
| @@ -13,7 +13,7 @@ | ||
| 13 | 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 | 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 | 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 | 16 | <i id="rename_connection_profileedit_connection_profile" class="menu_button fa-solid fa-pencil" title="RenameEdit a connection profile" data-i18n="[title]RenameEdit a connection profile"></i> |
| 17 | 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 | 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 | 19 | </div> |
| @@ -3,3 +3,8 @@ | ||
| 3 | 3 | <li><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</li> |
| 4 | 4 | {{/each}} |
| 5 | 5 | </ul> |
| 6 | +{{#if omitted}} | |
| 7 | +<div class="margin5"> | |
| 8 | + <strong data-i18n="Omitted Settings:">Omitted Settings:</strong> <span>{{omitted}}</span> | |
| 9 | +</div> | |
| 10 | +{{/if}} | |
| @@ -26,9 +26,11 @@ let paginationVisiblePages = 10; | ||
| 26 | 26 | let paginationMaxLinesPerPage = 2; |
| 27 | 27 | let galleryMaxRows = 3; |
| 28 | 28 | |
| 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 draggable | |
| 30 | +$('#movingDivs').on('click', '.dragClose', function () { | |
| 31 | - $(`body > .draggable[id="${relatedId}"]`).remove(); // Remove the associated draggable | |
| 31 | + const relatedId = $(this).data('related-id'); | |
| 32 | + if (!relatedId) return; | |
| 33 | + $(`#movingDivs > .draggable[id="${relatedId}"]`).remove(); | |
| 32 | 34 | }); |
| 33 | 35 | |
| 34 | 36 | const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved'; |
| @@ -290,7 +292,7 @@ function makeMovable(id = 'gallery') { | ||
| 290 | 292 | |
| 291 | 293 | $('#dragGallery').css('display', 'block'); |
| 292 | 294 | |
| 293 | 295 | $('body#movingDivs').append(newElement); |
| 294 | 296 | |
| 295 | 297 | loadMovingUIState(); |
| 296 | 298 | $(`.draggable[forChar="${id}"]`).css('display', 'block'); |
| @@ -362,8 +364,8 @@ function makeDragImg(id, url) { | ||
| 362 | 364 | } |
| 363 | 365 | } |
| 364 | 366 | |
| 365 | 367 | // Step 3: Attach it to the bodymovingDivs container |
| 366 | 368 | document.bodygetElementById('movingDivs').appendChild(newElement); |
| 367 | 369 | |
| 368 | 370 | // Step 4: Call dragElement and loadMovingUIState |
| 369 | 371 | const appendedElement = document.getElementById(uniqueId); |
| @@ -350,6 +350,7 @@ | ||
| 350 | 350 | } |
| 351 | 351 | .popup:has(#qr--modalEditor) { |
| 352 | 352 | aspect-ratio: unset; |
| 353 | + width: unset; | |
| 353 | 354 | } |
| 354 | 355 | .popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) { |
| 355 | 356 | min-width: unset; |
| @@ -415,6 +415,7 @@ | ||
| 415 | 415 | |
| 416 | 416 | .popup:has(#qr--modalEditor) { |
| 417 | 417 | aspect-ratio: unset; |
| 418 | + width: unset; | |
| 418 | 419 | |
| 419 | 420 | &:has(.qr--isExecuting.qr--minimized) { |
| 420 | 421 | min-width: unset; |
| @@ -0,0 +1,206 @@ | ||
| 1 | +import { saveTtsProviderSettings } from './index.js'; | |
| 2 | + | |
| 3 | +export { CosyVoiceProvider }; | |
| 4 | + | |
| 5 | +class 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 | +} | |
| @@ -0,0 +1,226 @@ | ||
| 1 | +import { saveTtsProviderSettings } from './index.js'; | |
| 2 | + | |
| 3 | +export { GptSovitsV2Provider }; | |
| 4 | + | |
| 5 | +class 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 | +} | |
| @@ -4,6 +4,7 @@ import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '. | ||
| 4 | 4 | import { EdgeTtsProvider } from './edge.js'; |
| 5 | 5 | import { ElevenLabsTtsProvider } from './elevenlabs.js'; |
| 6 | 6 | import { SileroTtsProvider } from './silerotts.js'; |
| 7 | +import { GptSovitsV2Provider } from './gpt-sovits-v2.js'; | |
| 7 | 8 | import { CoquiTtsProvider } from './coqui.js'; |
| 8 | 9 | import { SystemTtsProvider } from './system.js'; |
| 9 | 10 | import { NovelTtsProvider } from './novel.js'; |
| @@ -15,6 +16,7 @@ import { VITSTtsProvider } from './vits.js'; | ||
| 15 | 16 | import { GSVITtsProvider } from './gsvi.js'; |
| 16 | 17 | import { SBVits2TtsProvider } from './sbvits2.js'; |
| 17 | 18 | import { AllTalkTtsProvider } from './alltalk.js'; |
| 19 | +import { CosyVoiceProvider } from './cosyvoice.js'; | |
| 18 | 20 | import { SpeechT5TtsProvider } from './speecht5.js'; |
| 19 | 21 | import { AzureTtsProvider } from './azure.js'; |
| 20 | 22 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| @@ -86,9 +88,11 @@ const ttsProviders = { | ||
| 86 | 88 | AllTalk: AllTalkTtsProvider, |
| 87 | 89 | Azure: AzureTtsProvider, |
| 88 | 90 | Coqui: CoquiTtsProvider, |
| 91 | + 'CosyVoice (Unofficial)': CosyVoiceProvider, | |
| 89 | 92 | Edge: EdgeTtsProvider, |
| 90 | 93 | ElevenLabs: ElevenLabsTtsProvider, |
| 91 | 94 | GSVI: GSVITtsProvider, |
| 95 | + 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider, | |
| 92 | 96 | Novel: NovelTtsProvider, |
| 93 | 97 | OpenAI: OpenAITtsProvider, |
| 94 | 98 | 'OpenAI Compatible': OpenAICompatibleTtsProvider, |
| @@ -74,7 +74,7 @@ const samplers = { | ||
| 74 | 74 | |
| 75 | 75 | let novel_data = null; |
| 76 | 76 | let badWordsCache = {}; |
| 77 | 77 | const BIAS_KEY = '#novel_api-settingsrange_block_novel'; |
| 78 | 78 | |
| 79 | 79 | export function setNovelData(data) { |
| 80 | 80 | novel_data = data; |
| @@ -492,11 +492,37 @@ function getBadWordPermutations(text) { | ||
| 492 | 492 | |
| 493 | 493 | export function getNovelGenerationData(finalPrompt, settings, maxLength, isImpersonate, isContinue, _cfgValues, type) { |
| 494 | 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'); | |
| 495 | 497 | |
| 496 | 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 | 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 | 526 | : undefined; |
| 501 | 527 | |
| 502 | 528 | const badWordIds = (tokenizerType !== tokenizers.NONE) |
| @@ -515,11 +541,9 @@ export function getNovelGenerationData(finalPrompt, settings, maxLength, isImper | ||
| 515 | 541 | console.log(finalPrompt); |
| 516 | 542 | } |
| 517 | 543 | |
| 518 | - const isKayra = nai_settings.model_novel.includes('kayra'); | |
| 519 | - const isErato = nai_settings.model_novel.includes('erato'); | |
| 520 | 544 | |
| 521 | 545 | if (isErato) { |
| 522 | 546 | finalPrompt = '<|startoftext|><|reserved_special_token81|>' + finalPrompt; |
| 523 | 547 | } |
| 524 | 548 | |
| 525 | 549 | const adjustedMaxLength = (isKayra || isErato) ? getKayraMaxResponseTokens() : maximum_output_length; |
| @@ -3469,7 +3469,7 @@ function getModelOptions(quiet) { | ||
| 3469 | 3469 | case 'openai': |
| 3470 | 3470 | return oai_settings.chat_completion_source; |
| 3471 | 3471 | default: |
| 3472 | 3472 | return nullResultnull; |
| 3473 | 3473 | } |
| 3474 | 3474 | } |
| 3475 | 3475 | |
| @@ -143,7 +143,7 @@ function selectSystemPromptCallback(args, name) { | ||
| 143 | 143 | foundName = result[0].item; |
| 144 | 144 | } |
| 145 | 145 | |
| 146 | 146 | $select.val(foundName).trigger('inputchange'); |
| 147 | 147 | !quiet && toastr.success(`System prompt "${foundName}" selected`); |
| 148 | 148 | return foundName; |
| 149 | 149 | } |
| @@ -68,6 +68,10 @@ const LLAMACPP_DEFAULT_ORDER = [ | ||
| 68 | 68 | 'temperature', |
| 69 | 69 | ]; |
| 70 | 70 | const OOBA_DEFAULT_ORDER = [ |
| 71 | + 'repetition_penalty', | |
| 72 | + 'presence_penalty', | |
| 73 | + 'frequency_penalty', | |
| 74 | + 'dry', | |
| 71 | 75 | 'temperature', |
| 72 | 76 | 'dynamic_temperature', |
| 73 | 77 | 'quadratic_sampling', |
| @@ -80,6 +84,9 @@ const OOBA_DEFAULT_ORDER = [ | ||
| 80 | 84 | 'top_a', |
| 81 | 85 | 'min_p', |
| 82 | 86 | 'mirostat', |
| 87 | + 'xtc', | |
| 88 | + 'encoder_repetition_penalty', | |
| 89 | + 'no_repeat_ngram', | |
| 83 | 90 | ]; |
| 84 | 91 | const BIAS_KEY = '#textgenerationwebui_api-settings'; |
| 85 | 92 | |
| @@ -798,6 +805,25 @@ function showTypeSpecificControls(type) { | ||
| 798 | 805 | }); |
| 799 | 806 | } |
| 800 | 807 | |
| 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 | + */ | |
| 814 | +function 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 | + | |
| 801 | 827 | function setSettingByName(setting, value, trigger) { |
| 802 | 828 | if (value === null || value === undefined) { |
| 803 | 829 | return; |
| @@ -812,6 +838,7 @@ function setSettingByName(setting, value, trigger) { | ||
| 812 | 838 | |
| 813 | 839 | if ('sampler_priority' === setting) { |
| 814 | 840 | value = Array.isArray(value) ? value : OOBA_DEFAULT_ORDER; |
| 841 | + insertMissingArrayItems(OOBA_DEFAULT_ORDER, value); | |
| 815 | 842 | sortOobaItemsByOrder(value); |
| 816 | 843 | settings.sampler_priority = value; |
| 817 | 844 | return; |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js'; |
| 2 | 2 | import { extension_settings, saveMetadataDebounced } from './extensions.js'; |
| 3 | +import { callGenericPopup, POPUP_TYPE } from './popup.js'; | |
| 3 | 4 | import { executeSlashCommandsWithOptions } from './slash-commands.js'; |
| 4 | 5 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | 6 | import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; |
| @@ -11,7 +12,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom | ||
| 11 | 12 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 12 | 13 | import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 13 | 14 | import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; |
| 14 | 15 | import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js'; |
| 15 | 16 | |
| 16 | 17 | /** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */ |
| 17 | 18 | /** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */ |
| @@ -303,24 +304,48 @@ export function replaceVariableMacros(input) { | ||
| 303 | 304 | return lines.join('\n'); |
| 304 | 305 | } |
| 305 | 306 | |
| 306 | 307 | async function listVariablesCallback(args) { |
| 308 | + const type = String(args?.format || '').toLowerCase().trim() || 'popup'; | |
| 309 | + const scope = String(args?.scope || '').toLowerCase().trim() || 'all'; | |
| 307 | 310 | if (!chat_metadata.variables) { |
| 308 | 311 | chat_metadata.variables = {}; |
| 309 | 312 | } |
| 310 | 313 | |
| 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 | + ]; | |
| 313 | 324 | |
| 314 | 325 | const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables'; |
| 315 | 326 | const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables'; |
| 316 | 327 | const chatName = getCurrentChatId(); |
| 317 | 328 | |
| 318 | 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 | 334 | const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message)); |
| 321 | 335 | |
| 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 | } |
| 325 | 350 | |
| 326 | 351 | /** |
| @@ -463,7 +488,7 @@ function existsGlobalVariable(name) { | ||
| 463 | 488 | /** |
| 464 | 489 | * Parses boolean operands from command arguments. |
| 465 | 490 | * @param {object} args Command arguments |
| 466 | 491 | * @returns {{a: string | number, b: string | number?, rule: string}} Boolean operands |
| 467 | 492 | */ |
| 468 | 493 | export function parseBooleanOperands(args) { |
| 469 | 494 | // Resolution order: numeric literal, local variable, global variable, string literal |
| @@ -472,6 +497,9 @@ export function parseBooleanOperands(args) { | ||
| 472 | 497 | */ |
| 473 | 498 | function getOperand(operand) { |
| 474 | 499 | if (operand === undefined) { |
| 500 | + return undefined; | |
| 501 | + } | |
| 502 | + if (operand === '') { | |
| 475 | 503 | return ''; |
| 476 | 504 | } |
| 477 | 505 | |
| @@ -500,8 +528,8 @@ export function parseBooleanOperands(args) { | ||
| 500 | 528 | return stringLiteral || ''; |
| 501 | 529 | } |
| 502 | 530 | |
| 503 | 531 | const left = getOperand(args.a ||?? args.left ||?? args.first ||?? args.x); |
| 504 | 532 | const right = getOperand(args.b ||?? args.right ||?? args.second ||?? args.y); |
| 505 | 533 | const rule = args.rule; |
| 506 | 534 | |
| 507 | 535 | return { a: left, b: right, rule }; |
| @@ -509,84 +537,79 @@ export function parseBooleanOperands(args) { | ||
| 509 | 537 | |
| 510 | 538 | /** |
| 511 | 539 | * Evaluates a boolean comparison rule. |
| 512 | - * @param {string} rule Boolean comparison rule | |
| 540 | + * | |
| 541 | + * @param {string?} rule Boolean comparison rule | |
| 513 | 542 | * @param {string|number} a The left operand |
| 514 | 543 | * @param {string|number?} b The right operand |
| 515 | 544 | * @returns {boolean} True if the rule yields true, false otherwise |
| 516 | 545 | */ |
| 517 | 546 | export function evalBoolean(rule, a, b) { |
| 518 | 547 | if (!rulea === 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 | } |
| 522 | 565 | |
| 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 | 569 | if (typeof a === 'number' && typeof b === 'number') { |
| 525 | 570 | // only do numeric comparison if both operands are numbers |
| 526 | 571 | const aNumber = Number(a); |
| 527 | 572 | const bNumber = Number(b); |
| 528 | 573 | |
| 529 | 574 | switch (rule) { |
| 530 | - case 'not': | |
| 531 | - result = !aNumber; | |
| 532 | - break; | |
| 533 | 575 | case 'gt': |
| 534 | 576 | result =return aNumber > bNumber; |
| 535 | - break; | |
| 536 | 577 | case 'gte': |
| 537 | 578 | result =return aNumber >= bNumber; |
| 538 | - break; | |
| 539 | 579 | case 'lt': |
| 540 | 580 | result =return aNumber < bNumber; |
| 541 | - break; | |
| 542 | 581 | case 'lte': |
| 543 | 582 | result =return aNumber <= bNumber; |
| 544 | - break; | |
| 545 | 583 | case 'eq': |
| 546 | 584 | result =return aNumber === bNumber; |
| 547 | - break; | |
| 548 | 585 | case 'neq': |
| 549 | 586 | result =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 | 587 | case 'in': |
| 572 | - result = aString.includes(bString); | |
| 573 | - break; | |
| 574 | 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 | 591 | break; |
| 583 | 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 | } |
| 588 | 596 | |
| 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 | } |
| 591 | 614 | |
| 592 | 615 | /** |
| @@ -887,7 +910,35 @@ export function registerVariableCommands() { | ||
| 887 | 910 | name: 'listvar', |
| 888 | 911 | callback: listVariablesCallback, |
| 889 | 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 | 943 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 893 | 944 | name: 'setvar', |
| @@ -1264,32 +1315,36 @@ export function registerVariableCommands() { | ||
| 1264 | 1315 | typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], |
| 1265 | 1316 | isRequired: true, |
| 1266 | 1317 | enumProvider: commonEnumProviders.variables('all'), |
| 1267 | - forceEnum: false, | |
| 1268 | 1318 | }), |
| 1269 | 1319 | SlashCommandNamedArgument.fromProps({ |
| 1270 | 1320 | name: 'right', |
| 1271 | 1321 | description: 'right operand', |
| 1272 | 1322 | typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], |
| 1273 | - isRequired: true, | |
| 1274 | 1323 | enumProvider: commonEnumProviders.variables('all'), |
| 1275 | - forceEnum: false, | |
| 1276 | 1324 | }), |
| 1277 | 1325 | new 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 | 1331 | new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'), |
| 1284 | 1332 | new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'), |
| 1285 | 1333 | new SlashCommandEnumValue('notin', '!a includes b (strings & numbers as strings)'), |
| 1286 | 1334 | new SlashCommandEnumValue('innin', 'a not includes b (strings & numbers as strings)'), |
| 1287 | 1335 | new SlashCommandEnumValue('ningt', 'a not includes> 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 | 1349 | unnamedArgumentList: [ |
| 1295 | 1350 | new SlashCommandArgument( |
| @@ -1307,17 +1362,25 @@ export function registerVariableCommands() { | ||
| 1307 | 1362 | Numeric values and string literals for left and right operands supported. |
| 1308 | 1363 | </div> |
| 1309 | 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 | 1373 | <strong>Available rules:</strong> |
| 1311 | 1374 | <ul> |
| 1312 | 1375 | <li>gt<code>eq</code> => a >== b <small>(strings & numbers)</small></li> |
| 1313 | 1376 | <li>gte<code>neq</code> => a >!== b <small>(strings & numbers)</small></li> |
| 1314 | 1377 | <li>lt<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 | 1379 | <li>eq<code>gt</code> => a ==> b <small>(numbers)</small></li> |
| 1317 | 1380 | <li>neq<code>gte</code> => a !>= b <small>(numbers)</small></li> |
| 1318 | 1381 | <li>not<code>lt</code> => !a < b <small>(numbers)</small></li> |
| 1319 | 1382 | <li>in (strings)<code>lte</code> => a includes<= 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 | 1384 | </ul> |
| 1322 | 1385 | </div> |
| 1323 | 1386 | <div> |
| @@ -1327,6 +1390,17 @@ export function registerVariableCommands() { | ||
| 1327 | 1390 | <pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre> |
| 1328 | 1391 | triggers a /speak command if the value of "score" is greater or equals 10. |
| 1329 | 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 | 1404 | </ul> |
| 1331 | 1405 | </div> |
| 1332 | 1406 | `, |
| @@ -1342,32 +1416,38 @@ export function registerVariableCommands() { | ||
| 1342 | 1416 | typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], |
| 1343 | 1417 | isRequired: true, |
| 1344 | 1418 | enumProvider: commonEnumProviders.variables('all'), |
| 1345 | - forceEnum: false, | |
| 1346 | 1419 | }), |
| 1347 | 1420 | SlashCommandNamedArgument.fromProps({ |
| 1348 | 1421 | name: 'right', |
| 1349 | 1422 | description: 'right operand', |
| 1350 | 1423 | typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER], |
| 1351 | - isRequired: true, | |
| 1352 | 1424 | enumProvider: commonEnumProviders.variables('all'), |
| 1353 | - forceEnum: false, | |
| 1354 | 1425 | }), |
| 1355 | 1426 | new 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 | 1432 | new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'), |
| 1362 | 1433 | new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'), |
| 1363 | 1434 | new SlashCommandEnumValue('notin', '!a includes b (strings & numbers as strings)'), |
| 1364 | 1435 | new SlashCommandEnumValue('innin', 'a not includes b (strings & numbers as strings)'), |
| 1365 | 1436 | new SlashCommandEnumValue('ningt', 'a not includes> 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 | 1452 | unnamedArgumentList: [ |
| 1373 | 1453 | new SlashCommandArgument( |
| @@ -1386,15 +1466,15 @@ export function registerVariableCommands() { | ||
| 1386 | 1466 | <div> |
| 1387 | 1467 | <strong>Available rules:</strong> |
| 1388 | 1468 | <ul> |
| 1389 | 1469 | <li>gt<code>eq</code> => a >== b <small>(strings & numbers)</small></li> |
| 1390 | 1470 | <li>gte<code>neq</code> => a >!== b <small>(strings & numbers)</small></li> |
| 1391 | 1471 | <li>lt<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 | 1473 | <li>eq<code>gt</code> => a ==> b <small>(numbers)</small></li> |
| 1394 | 1474 | <li>neq<code>gte</code> => a !>= b <small>(numbers)</small></li> |
| 1395 | 1475 | <li>not<code>lt</code> => !a < b <small>(numbers)</small></li> |
| 1396 | 1476 | <li>in (strings)<code>lte</code> => a includes<= 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 | 1478 | </ul> |
| 1399 | 1479 | </div> |
| 1400 | 1480 | <div> |
| @@ -1404,7 +1484,11 @@ export function registerVariableCommands() { | ||
| 1404 | 1484 | <pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre> |
| 1405 | 1485 | adds 1 to the value of "i" until it reaches 10. |
| 1406 | 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 | 1492 | </div> |
| 1409 | 1493 | <div> |
| 1410 | 1494 | Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable. |
| @@ -1519,7 +1603,7 @@ export function registerVariableCommands() { | ||
| 1519 | 1603 | typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], |
| 1520 | 1604 | isRequired: true, |
| 1521 | 1605 | acceptsMultiple: true, |
| 1522 | 1606 | enumProvider: (executor, scope) => { |
| 1523 | 1607 | const vars = commonEnumProviders.variables('all')(executor, scope); |
| 1524 | 1608 | vars.push( |
| 1525 | 1609 | new SlashCommandEnumValue( |
| @@ -1527,16 +1611,16 @@ export function registerVariableCommands() { | ||
| 1527 | 1611 | null, |
| 1528 | 1612 | enumTypes.variable, |
| 1529 | 1613 | enumIcons.variable, |
| 1530 | 1614 | (input) => /^\w*$/.test(input), |
| 1531 | 1615 | (input) => input, |
| 1532 | 1616 | ), |
| 1533 | 1617 | new SlashCommandEnumValue( |
| 1534 | 1618 | 'any number', |
| 1535 | 1619 | null, |
| 1536 | 1620 | enumTypes.number, |
| 1537 | 1621 | enumIcons.number, |
| 1538 | 1622 | (input) => input == '' || !Number.isNaN(Number(input)), |
| 1539 | 1623 | (input) => input, |
| 1540 | 1624 | ), |
| 1541 | 1625 | ); |
| 1542 | 1626 | return vars; |
| @@ -3154,6 +3154,26 @@ grammarly-extension { | ||
| 3154 | 3154 | align-items: center; |
| 3155 | 3155 | } |
| 3156 | 3156 | |
| 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 | 3177 | #rm_characters_block .form_create_bottom_buttons_block { |
| 3158 | 3178 | justify-content: space-evenly !important; |
| 3159 | 3179 | flex-grow: 0; |
| @@ -3245,8 +3265,9 @@ grammarly-extension { | ||
| 3245 | 3265 | } |
| 3246 | 3266 | |
| 3247 | 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 | 3271 | min-width: var(--sheldWidth); |
| 3251 | 3272 | } |
| 3252 | 3273 | |
| @@ -5365,13 +5386,6 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 5365 | 5386 | |
| 5366 | 5387 | /* Jank mobile support for gallery and future draggables */ |
| 5367 | 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 | 5389 | .draggable { |
| 5376 | 5390 | display: block; |
| 5377 | 5391 | width: 100vw; |
| @@ -347,6 +347,8 @@ async function migrateSystemPrompts() { | ||
| 347 | 347 | if (fs.existsSync(migrateMarker)) { |
| 348 | 348 | continue; |
| 349 | 349 | } |
| 350 | + const backupsPath = path.join(directory.backups, '_sysprompt'); | |
| 351 | + fs.mkdirSync(backupsPath, { recursive: true }); | |
| 350 | 352 | const defaultPrompts = await getDefaultSystemPrompts(); |
| 351 | 353 | const instucts = fs.readdirSync(directory.instruct); |
| 352 | 354 | let migratedPrompts = []; |
| @@ -356,6 +358,8 @@ async function migrateSystemPrompts() { | ||
| 356 | 358 | if (path.extname(instruct) === '.json' && !fs.existsSync(sysPromptPath)) { |
| 357 | 359 | const instructData = JSON.parse(fs.readFileSync(instructPath, 'utf8')); |
| 358 | 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 | 363 | const syspromptData = { name: instructData.name, content: instructData.system_prompt }; |
| 360 | 364 | migratedPrompts.push(syspromptData); |
| 361 | 365 | delete instructData.system_prompt; |
| @@ -374,8 +378,8 @@ async function migrateSystemPrompts() { | ||
| 374 | 378 | console.log(`Migrated system prompt ${sysPromptData.name} for ${directory.root.split(path.sep).pop()}`); |
| 375 | 379 | } |
| 376 | 380 | writeFileAtomicSync(migrateMarker, ''); |
| 377 | 381 | } catch (error) { |
| 378 | - // Ignore errors | |
| 382 | + console.error('Error migrating system prompts:', error); | |
| 379 | 383 | } |
| 380 | 384 | } |
| 381 | 385 | } |