Merge branch 'SillyTavern:staging' into staging

This commit is contained in:
Tony Ribeiro 2023-10-24 14:23:45 +02:00 committed by GitHub
commit 141850eda5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1765 additions and 1194 deletions

View File

@ -26,6 +26,8 @@ const extras = {
captioningModel: 'Xenova/vit-gpt2-image-captioning',
// Feature extraction model. HuggingFace ID of a model in ONNX format.
embeddingModel: 'Xenova/all-mpnet-base-v2',
// GPT-2 text generation model. HuggingFace ID of a model in ONNX format.
promptExpansionModel: 'Cohee/fooocus_expansion-onnx',
};
// Request overrides for additional headers

View File

@ -1,6 +1,11 @@
{
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"chat_start": "",
"example_separator": "",
"chat_start": "",
"always_force_name2": false,
"trim_sentences": false,
"include_newline": false,
"custom_stopping_strings": "[\"\\n\"]",
"custom_stopping_strings_macro": true,
"name": "Adventure"
}

View File

@ -293,7 +293,7 @@
display: none;
}
#bg_menu_content {
.bg_list {
width: unset;
}
}
@ -445,4 +445,4 @@
#horde_model {
height: unset;
}
}
}

View File

@ -6,6 +6,12 @@
color: var(--fullred);
}
.highlighted {
color: black;
background-color: yellow;
text-shadow: none !important;
}
.m-t-0 {
margin-top: 0;
}

View File

@ -138,8 +138,7 @@
filter: brightness(1);
}
.tags_view,
.open_alternate_greetings {
.tags_view {
margin: 0;
aspect-ratio: 1 / 1;
}
@ -171,4 +170,4 @@
-1px 1px 0px black,
1px -1px 0px black;
opacity: 1;
}
}

View File

@ -86,6 +86,9 @@
<script type="module" src="scripts/filters.js"></script>
<script type="module" src="scripts/personas.js"></script>
<script type="module" src="scripts/server-history.js"></script>
<script type="module" src="scripts/setting-search.js"></script>
<script type="module" src="scripts/bulk-edit.js"></script>
<script type="module" src="scripts/cfg-scale.js"></script>
<title>SillyTavern</title>
</head>
@ -127,8 +130,8 @@
<span class="note-link-span">?</span>
</a>
</h4>
<div class="preset_buttons">
<select id="settings_perset" data-preset-manager-for="kobold">
<div class="flex-container">
<select id="settings_perset" data-preset-manager-for="kobold" class="flex1 text_pole">
<option value="gui" data-i18n="guikoboldaisettings">GUI KoboldAI Settings</option>
</select>
<input type="file" hidden data-preset-manager-file="kobold" accept=".json, .settings">
@ -146,8 +149,8 @@
<span class="note-link-span">?</span>
</a>
</h4>
<div class="preset_buttons">
<select id="settings_perset_novel" data-preset-manager-for="novel">
<div class="flex-container">
<select id="settings_perset_novel" class="flex1 text_pole" data-preset-manager-for="novel">
<option value="gui" data-i18n="default">Default</option>
</select>
<input type="file" hidden data-preset-manager-file="novel" accept=".json, .settings">
@ -161,8 +164,8 @@
<div id="openai_api-presets">
<div>
<h4 class="margin0"><span data-i18n="openaipresets">Chat Completion Presets</span></h4>
<div class="openai_preset_buttons">
<select id="settings_perset_openai">
<div class="flex-container">
<select id="settings_perset_openai" class="flex1 text_pole">
<option value="gui" data-i18n="default">Default</option>
</select>
<i id="update_oai_preset" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
@ -176,8 +179,8 @@
</div>
<div id="textgenerationwebui_api-presets">
<h4 class="margin0"><span data-i18n="Text Gen WebUI (ooba/Mancer) presets">Text Gen WebUI (ooba/Mancer) presets</span></h4>
<div class="preset_buttons">
<select id="settings_preset_textgenerationwebui" data-preset-manager-for="textgenerationwebui">
<div class="flex-container">
<select id="settings_preset_textgenerationwebui" class="flex1 text_pole" data-preset-manager-for="textgenerationwebui">
</select>
<input type="file" hidden data-preset-manager-file="textgenerationwebui" accept=".json, .settings">
<i data-newbie-hidden data-preset-manager-update="textgenerationwebui" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
@ -232,7 +235,7 @@
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="max_context" name="volume" min="512" max="4096" step="1">
<input type="range" id="max_context" name="volume" min="512" max="4096" step="512">
</div>
<div class="range-block-counter" data-randomization-disabled="true">
<div contenteditable="true" data-for="max_context" id="max_context_counter">
@ -1750,6 +1753,15 @@
Add bias entry
</div>
<div class="openai_logit_bias_list"></div>
<div class="m-t-1">
<small>
<i class="fa-solid fa-lightbulb"></i>
&nbsp;
<span data-i18n="Most tokens have a leading space.">
Most tokens have a leading space.
</span>
</small>
</div>
</div>
</div>
</div>
@ -2261,8 +2273,8 @@
<h4 data-i18n="Context Template">
Context Template
</h4>
<div class="preset_buttons flex-container">
<select id="context_presets" data-preset-manager-for="context" class="flex1"></select>
<div class="flex-container">
<select id="context_presets" data-preset-manager-for="context" class="flex1 text_pole"></select>
<input type="file" hidden data-preset-manager-file="context" accept=".json, .settings">
<i id="context_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset for Instruct Mode."></i>
<i data-newbie-hidden data-preset-manager-update="context" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
@ -2349,8 +2361,8 @@
<label for="instruct_presets">
<span data-i18n="Presets">Presets</span>
</label>
<div class="preset_buttons">
<select id="instruct_presets" data-preset-manager-for="instruct" class="flex1"></select>
<div class="flex-container">
<select id="instruct_presets" data-preset-manager-for="instruct" class="flex1 text_pole"></select>
<input type="file" hidden data-preset-manager-file="instruct" accept=".json, .settings">
<i id="instruct_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset on API connection."></i>
<i data-newbie-hidden data-preset-manager-update="instruct" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
@ -2512,18 +2524,6 @@
Always add character's name to prompt
</span>
</label>
<label data-newbie-hidden class="checkbox_label" for="pin-examples-checkbox">
<input id="pin-examples-checkbox" type="checkbox" />
<span data-i18n="Keep Example Messages in Prompt">
Keep Example Messages in Prompt
</span>
</label>
<label data-newbie-hidden class="checkbox_label" for="remove-examples-checkbox">
<input id="remove-examples-checkbox" type="checkbox" />
<span data-i18n="Strip Example Messages from Prompt">
Strip Example Messages from Prompt
</span>
</label>
<label class="checkbox_label" for="collapse-newlines-checkbox"><input id="collapse-newlines-checkbox" type="checkbox" />
<span data-i18n="Remove Empty New Lines from Output">
Remove Empty New Lines from Output
@ -3076,7 +3076,19 @@
<div name="UserSettingsThirdColumn" id="power-user-options-block" class="flex-container wide100p">
<div id="power-user-option-checkboxes">
<div data-newbie-hidden name="CharacterHandlingToggles">
<h4>Character Handling</h4>
<h4 data-i18n="Character Handling">
Character Handling
</h4>
<div data-newbie-hidden id="examples-behavior-block">
<label data-i18n="Example Messages Behavior">
Example Messages Behavior:
</label>
<select id="example_messages_behavior">
<option value="normal">Gradual push-out</option>
<option value="keep">Always include examples</option>
<option value="strip">Never include examples</option>
</select>
</div>
<label data-newbie-hidden class="checkbox_label" for="fuzzy_search_checkbox">
<input id="fuzzy_search_checkbox" type="checkbox" />
<span data-i18n="Advanced Character Search">Advanced Character Search</span>
@ -3131,10 +3143,16 @@
Quick "Continue" button
</span>
</label>
<label data-newbie-hidden class="checkbox_label" for="swipes-checkbox">
<input id="swipes-checkbox" type="checkbox" />
<span data-i18n="Swipes">Swipes</span>
</label>
<div class="checkbox-container flex-container">
<label data-newbie-hidden class="checkbox_label" for="swipes-checkbox">
<input id="swipes-checkbox" type="checkbox" />
<span data-i18n="Swipes">Swipes</span>
</label>
<label data-newbie-hidden class="checkbox_label" for="gestures-checkbox">
<input id="gestures-checkbox" type="checkbox" />
<span data-i18n="Gestures">Gestures</span>
</label>
</div>
<label class="checkbox_label" for="auto-load-chat-checkbox">
<input id="auto-load-chat-checkbox" type="checkbox" />
<span data-i18n="Auto-load Last Chat">Auto-load Last Chat</span>
@ -3221,14 +3239,23 @@
</div>
</div>
<div id="logo_block" class="drawer" title="Change Background Image" data-i18n="[title]Change Background Image">
<div id="site_logo" class="drawer-toggle drawer-header">
<div id="logo_block" class="drawer">
<div id="site_logo" class="drawer-toggle drawer-header" title="Change Background Image" data-i18n="[title]Change Background Image">
<div class="drawer-icon fa-solid fa-panorama closedIcon"></div>
</div>
<div class="drawer-content closedDrawer">
<div id="Backgrounds" class="drawer-content closedDrawer">
<div class="flex-container">
<input id="bg-filter" placeholder="Filter" class="text_pole" type="search" />
<div id="bg_menu_content">
<div class="flex-container wide100p">
<input id="bg-filter" placeholder="Filter" class="text_pole flex1" type="search" />
<div id="auto_background" class="menu_button menu_button_icon" title="Automatically select a background based on the chat context.">
<i class="fa-solid fa-wand-magic"></i>
Auto-select
</div>
</div>
<h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter">
System Backgrounds
</h3>
<div id="bg_menu_content" class="bg_list">
<form id="form_bg_download" class="bg_example no-border no-shadow" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<label class="input-file">
<input type="file" id="add_bg_button" name="avatar" accept="image/png, image/jpeg, image/jpg, image/gif, image/bmp">
@ -3236,6 +3263,14 @@
</label>
</form>
</div>
<h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter">
Chat Backgrounds
</h3>
<div id="bg_chat_hint" class="wide100p textAlignCenter">
Chat backgrounds generated with the <code><i class="fa-solid fa-paintbrush"></i>&nbsp;Stable Diffusion</code> extension will appear here.
</div>
<div id="bg_custom_content" class="bg_list">
</div>
</div>
</div>
</div>
@ -3247,28 +3282,42 @@
<div id="rm_extensions_block" class="drawer-content closedDrawer">
<div class="extensions_block flex-container">
<div class="alignitemscenter flex-container justifyCenter wide100p" style="justify-content: space-between;">
<h3 class="margin0" data-i18n="Extensions API:">Extensions API:
<a target="_blank" href="https://github.com/SillyTavern/SillyTavern-extras">
SillyTavern-extras
<h3 class="margin0" data-i18n="Extras API:">Extras API:
<a target="_blank" href="https://github.com/SillyTavern/SillyTavern-Extras">
SillyTavern-Extras
</a>
</h3>
<div class="flex-container">
<div id="extensions_status" data-i18n="Not connected...">Not connected...</div>
<label for="extensions_autoconnect">
<label for="extensions_autoconnect" class="checkbox_label flexNoGap">
<input id="extensions_autoconnect" type="checkbox">
<span data-i18n="Auto-connect">Auto-connect</span>
</label>
</div>
</div>
<div class="alignitemsflexstart flex-container wide100p">
<input id="extensions_url" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]Extensions URL" placeholder="Extensions URL">
<input id="extensions_api_key" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]API key" placeholder="Extras API key">
<input id="extensions_url" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]Extras API URL" placeholder="Extras API URL">
<input id="extensions_api_key" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]Extras API key (optional)" placeholder="Extras API key (optional)">
<div class="extensions_url_block">
<div id="extensions_connect" class="menu_button" data-i18n="Connect">Connect</div>
<div id="extensions_details" class="menu_button_icon menu_button">
Manage extensions
</div>
<div id="third_party_extension_button" title="Import Extension From Git Repo" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
</div>
</div>
<hr class="wide100p margin0">
<div class="alignitemscenter flex-container wide100p">
<h3 class="margin0 flex1" data-i18n="Extensions">
Extensions
</h3>
<label for="extensions_notify_updates" class="checkbox_label flexNoGap">
<input id="extensions_notify_updates" type="checkbox">
<span data-i18n="Notify on extension updates">Notify on extension updates</span>
</label>
<div id="extensions_details" class="menu_button_icon menu_button">
<i class="fa-solid fa-cubes"></i>
Manage extensions
</div>
<div id="third_party_extension_button" title="Import Extension From Git Repo" class="menu_button menu_button_icon">
<i class="fa-solid fa-cloud-arrow-down"></i>
Install extension
</div>
</div>
<div id="extensions_settings" class="flex1 wide50p">
@ -3501,8 +3550,10 @@
<span class="note-link-span">?</span>
</a>
</span>
<div class="menu_button open_alternate_greetings" title="Click to set additional greeting messages" data-i18n="[title]Click to set additional greeting messages">
<i class="fa-solid fa-comment-dots"></i>
<div class="menu_button menu_button_icon open_alternate_greetings margin0" title="Click to set additional greeting messages" data-i18n="[title]Click to set additional greeting messages">
<span data-i18n="Alt. Greetings">
Alt. Greetings
</span>
</div>
</div>
<textarea id="firstmessage_textarea" 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="marginBot5" name="first_mes" placeholder=""></textarea>
@ -3679,6 +3730,8 @@
<div id="rm_print_characters_pagination">
<i id="charListGridToggle" class="fa-solid fa-table-cells-large menu_button" title="Toggle character grid view"></i>
<i id="bulkEditButton" class="fa-solid fa-edit menu_button bulkEditButton" title="Bulk edit characters"></i>
<i id="bulkDeleteButton" class="fa-solid fa-trash menu_button bulkDeleteButton" title="Bulk delete characters" style="display: none;"></i>
</div>
<div id="rm_print_characters_block" class="flexFlowColumn"></div>
</div>
@ -3874,8 +3927,11 @@
<div id="background_template" class="template_element">
<div class="bg_example flex-container" bgfile="" class="bg_example_img" title="">
<div title="Rename background" bgfile="" class="bg_button bg_example_edit fa-solid fa-pencil"></div>
<div title="Delete background" bgfile="" class="bg_button bg_example_cross fa-solid fa-circle-xmark"></div>
<div title="Copy to system backgrounds" class="bg_button bg_example_copy fa-solid fa-file-arrow-up"></div>
<div title="Rename background" class="bg_button bg_example_edit fa-solid fa-pencil"></div>
<div title="Lock" class="bg_button bg_example_lock fa-solid fa-lock"></div>
<div title="Unlock" class="bg_button bg_example_unlock fa-solid fa-lock-open"></div>
<div title="Delete background" class="bg_button bg_example_cross fa-solid fa-circle-xmark"></div>
<div class="BGSampleTitle">
</div>
</div>
@ -4316,7 +4372,7 @@
<div id="openai_logit_bias_template" class="template_element">
<div class="openai_logit_bias_form">
<input class="openai_logit_bias_text text_pole" data-i18n="[placeholder]Type here..." placeholder="type here..." />
<input class="openai_logit_bias_text text_pole" data-i18n="[placeholder]Text or token ids" placeholder="Text or [token ids]" />
<input class="openai_logit_bias_value text_pole" type="number" min="-100" value="0" max="100" />
<i class="menu_button fa-solid fa-xmark openai_logit_bias_remove"></i>
</form>
@ -4716,6 +4772,178 @@
</div>
</div>
</div>
<div id="cfgConfig" class="drawer-content flexGap5">
<div class="panelControlBar flex-container">
<div id="cfgConfigHeader" class="fa-solid fa-grip drag-grabber"></div>
<div id="CFGClose" class="fa-solid fa-circle-xmark"></div>
</div>
<div name="cfgConfigHolder" class="scrollY">
<div id="chat_cfg_container">
<div class="inline-drawer">
<div id="CFGBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Chat CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small>
<b>Unique to this chat.</b><br>
</small>
<label for="chat_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="chat_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="chat_cfg_guidance_scale" id="chat_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="chat_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="chat_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="chat_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="chat_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
<div id="groupchat_cfg_use_chara_container">
<label class="checkbox_label" for="groupchat_cfg_use_chara">
<input type="checkbox" id="groupchat_cfg_use_chara" />
<span data-i18n="Use character CFG scales">Use character CFG scales</span>
</label>
</div>
</div>
</div>
</div>
<div id="chara_cfg_container" style="display: none;">
<hr class="sysHR">
<div class="inline-drawer">
<div id="charaANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Character CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><b>Will be automatically added as the CFG for this character.</b></small>
<br />
<label for="chara_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="chara_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="chara_cfg_guidance_scale" id="chara_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="chara_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="chara_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="chara_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="chara_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
</div>
</div>
</div>
<div id="global_cfg_container">
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Global CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><b>Will be used as the default CFG options for every chat unless overridden.</b></small>
<br />
<label for="global_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="global_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="global_cfg_guidance_scale" id="global_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="global_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="global_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="global_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="global_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
</div>
</div>
</div>
<div id="cfg_prompt_combine_container">
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>CFG Prompt Cascading</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="flex-container flexFlowColumn">
<small>
<b>Combine positive/negative prompts from other boxes.</b>
<br />
For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.
</small>
</div>
<br />
<div class="flex-container flexFlowColumn">
<label for="cfg_prompt_combine">
<span data-i18n="Scale">Always Include</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="0" />
<span data-i18n="Chat Negatives">Chat Negatives</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="1" />
<span data-i18n="Character Negatives">Character Negatives</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="2" />
<span data-i18n="Global Negatives">Global Negatives</span>
</label>
</div>
<div class="flex-container flexFlowColumn">
<label>
Custom Separator: <input id="cfg_prompt_separator" class="text_pole textarea_compact widthUnset" placeholder="&quot;\n&quot;" type="text" />
</label>
<label>
Insertion Depth: <input id="cfg_prompt_insertion_depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="sheld">
<div id="sheldheader" class="fa-solid fa-grip drag-grabber"></div>
@ -4755,9 +4983,10 @@
<i class="fa-lg fa-solid fa-note-sticky"></i>
<span data-i18n="Author's Note">Author's Note</span>
</a>
<div data-newbie-hidden id="options_advanced">
</div>
<a data-newbie-hidden id="option_toggle_CFG">
<i class="fa-lg fa-solid fa-scale-balanced"></i>
<span data-i18n="CFG Scale">CFG Scale</span>
</a>
<a id="option_back_to_main">
<i class="fa-lg fa-solid fa-left-long"></i>
<span data-i18n="Back to parent chat">Back to parent chat</span>

View File

@ -1,5 +1,5 @@
import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods, } from "./scripts/RossAscends-mods.js";
import { userStatsHandler, statMesProcess } from './scripts/stats.js';
import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js';
import {
generateKoboldWithStreaming,
kai_settings,
@ -142,7 +142,7 @@ import {
onlyUnique,
} from "./scripts/utils.js";
import { extension_settings, getContext, installExtension, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js";
import { extension_settings, getContext, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js";
import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js";
import {
tag_map,
@ -169,7 +169,7 @@ import { getDeviceInfo } from "./scripts/RossAscends-mods.js";
import { registerPromptManagerMigration } from "./scripts/PromptManager.js";
import { getRegexedString, regex_placement } from "./scripts/extensions/regex/engine.js";
import { FILTER_TYPES, FilterHelper } from "./scripts/filters.js";
import { getCfgPrompt, getGuidanceScale } from "./scripts/extensions/cfg/util.js";
import { getCfgPrompt, getGuidanceScale, initCfg } from "./scripts/cfg-scale.js";
import {
force_output_sequence,
formatInstructModeChat,
@ -182,6 +182,7 @@ import {
import { applyLocale } from "./scripts/i18n.js";
import { getTokenCount, getTokenizerModel, saveTokenCache } from "./scripts/tokenizers.js";
import { initPersonas, selectCurrentPersona, setPersonaDescription } from "./scripts/personas.js";
import { getBackgrounds, initBackgrounds } from "./scripts/backgrounds.js";
//exporting functions and vars for mods
export {
@ -319,7 +320,6 @@ reloadMarkdownProcessor();
console.debug('initializing Prompt Itemization Array on Startup');
let itemizedPrompts = [];
/* let bg_menu_toggle = false; */
export const systemUserName = "SillyTavern System";
let default_user_name = "User";
let name1 = default_user_name;
@ -348,7 +348,6 @@ let generation_started = new Date();
let characters = [];
let this_chid;
let saveCharactersPage = 0;
let backgrounds = [];
const default_avatar = "img/ai4.png";
export const system_avatar = "img/five.png";
export const comment_avatar = "img/quill.png";
@ -632,7 +631,6 @@ let create_save = {
let animation_duration = 125;
let animation_easing = "ease-in-out";
let popup_type = "";
let bg_file_for_del = "";
let chat_file_for_del = "";
let online_status = "no_connection";
@ -721,9 +719,12 @@ async function firstLoadInit() {
await getUserAvatars();
await getCharacters();
await getBackgrounds();
initBackgrounds();
initAuthorsNote();
initPersonas();
initRossMods();
initStats();
initCfg();
}
function checkOnlineStatus() {
@ -1033,73 +1034,6 @@ async function getCharacters() {
}
}
async function getBackgrounds() {
const response = await fetch("/getbackgrounds", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
"": "",
}),
});
if (response.ok === true) {
const getData = await response.json();
//background = getData;
//console.log(getData.length);
$("#bg_menu_content").children('div').remove();
for (const bg of getData) {
const template = getBackgroundFromTemplate(bg);
$("#bg_menu_content").append(template);
}
}
}
function getBackgroundFromTemplate(bg) {
const thumbPath = getThumbnailUrl('bg', bg);
const template = $('#background_template .bg_example').clone();
template.attr('bgfile', bg);
template.attr('title', bg);
template.find('.bg_button').attr('bgfile', bg);
template.css('background-image', `url('${thumbPath}')`);
template.find('.BGSampleTitle').text(bg.slice(0, bg.lastIndexOf('.')));
return template;
}
async function setBackground(bg) {
jQuery.ajax({
type: "POST", //
url: "/setbackground", //
data: JSON.stringify({
bg: bg,
}),
beforeSend: function () {
},
cache: false,
dataType: "json",
contentType: "application/json",
//processData: false,
success: function (html) { },
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
}
async function delBackground(bg) {
const response = await fetch("/delbackground", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
bg: bg,
}),
});
if (response.ok === true) {
}
}
async function delChat(chatfile) {
const response = await fetch("/delchat", {
method: "POST",
@ -1742,7 +1676,7 @@ function substituteParams(content, _name1, _name2, _original, _group) {
if (typeof _original === 'string') {
content = content.replace(/{{original}}/i, _original);
}
content = content.replace(/{{input}}/gi, $('#send_textarea').val());
content = content.replace(/{{input}}/gi, String($('#send_textarea').val()));
content = content.replace(/{{user}}/gi, _name1);
content = content.replace(/{{char}}/gi, _name2);
content = content.replace(/{{charIfNotGroup}}/gi, _group);
@ -1753,11 +1687,14 @@ function substituteParams(content, _name1, _name2, _original, _group) {
content = content.replace(/<CHARIFNOTGROUP>/gi, _group);
content = content.replace(/<GROUP>/gi, _group);
content = content.replace(/\{\{\/\/(.*?)\}\}/g, "");
content = content.replace(/{{time}}/gi, moment().format('LT'));
content = content.replace(/{{date}}/gi, moment().format('LL'));
content = content.replace(/{{weekday}}/gi, moment().format('dddd'));
content = content.replace(/{{isotime}}/gi, moment().format('HH:mm'));
content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD'));
content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => {
const formattedTime = moment().format(format);
return formattedTime;
@ -2185,11 +2122,12 @@ class StreamingProcessor {
let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal);
// Predict unbalanced asterisks / quotes during streaming
const charsToBalance = ['*', '"'];
const charsToBalance = ['*', '"', '```'];
for (const char of charsToBalance) {
if (!isFinal && isOdd(countOccurrences(processedText, char))) {
// Add character at the end to balance it
processedText = processedText.trimEnd() + char;
const separator = char.length > 1 ? '\n' : '';
processedText = processedText.trimEnd() + separator + char;
}
}
@ -3080,7 +3018,9 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
: ` ${cfgPrompt.value}`;
} else {
// TODO: Make all extension prompts use an array/splice method
finalMesSend[mesSend.length - cfgPrompt.depth].extensionPrompts.push(`${cfgPrompt.value}\n`);
const lengthDiff = mesSend.length - cfgPrompt.depth;
const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
}
}
@ -4819,8 +4759,6 @@ export async function getUserAvatars() {
});
if (response.ok === true) {
const getData = await response.json();
//background = getData;
//console.log(getData.length);
$("#user_avatar_block").html(""); //RossAscends: necessary to avoid doubling avatars each refresh.
$("#user_avatar_block").append('<div class="avatar_upload">+</div>');
@ -4883,9 +4821,9 @@ export function setUserName(value) {
function setUserAvatar() {
user_avatar = $(this).attr("imgfile");
reloadUserAvatar();
saveSettingsDebounced();
highlightSelectedAvatar();
selectCurrentPersona();
saveSettingsDebounced();
$('.zoomed_avatar[forchar]').remove();
}
@ -5932,48 +5870,6 @@ function callPopup(text, type, inputValue = '', { okButton, rows, wide, large }
});
}
function read_bg_load(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
$("#bg_load_preview")
.attr("src", e.target.result)
.width(103)
.height(83);
var formData = new FormData($("#form_bg_download").get(0));
//console.log(formData);
jQuery.ajax({
type: "POST",
url: "/downloadbackground",
data: formData,
beforeSend: function () {
},
cache: false,
contentType: false,
processData: false,
success: function (html) {
setBackground(html);
$("#bg1").css(
"background-image",
`url("${e.target.result}")`
);
$("#form_bg_download").after(getBackgroundFromTemplate(html));
},
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
};
reader.readAsDataURL(input.files[0]);
}
}
function showSwipeButtons() {
if (chat.length === 0) {
return;
@ -6187,6 +6083,7 @@ function enlargeMessageImage() {
const mesId = mesBlock.attr('mesid');
const message = chat[mesId];
const imgSrc = message?.extra?.image;
const title = message?.extra?.title;
if (!imgSrc) {
return;
@ -6195,7 +6092,12 @@ function enlargeMessageImage() {
const img = document.createElement('img');
img.classList.add('img_enlarged');
img.src = imgSrc;
callPopup(img.outerHTML, 'text', '', { wide: true, large: true });
const imgContainer = $('<div><pre><code></code></pre></div>');
imgContainer.prepend(img);
imgContainer.addClass('img_enlarged_container');
imgContainer.find('code').addClass('txt').text(title);
addCopyToCodeBlocks(imgContainer);
callPopup(imgContainer, 'text', '', { wide: true, large: true });
}
function updateAlternateGreetingsHintVisibility(root) {
@ -7340,86 +7242,6 @@ jQuery(async function () {
});
$("#avatar_upload_file").on("change", uploadUserAvatar);
$(document).on("click", ".bg_example", async function () {
//when user clicks on a BG thumbnail...
const this_bgfile = $(this).attr("bgfile"); // this_bgfile = whatever they clicked
const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage;
// custom background is set. Do not override the layer below
if (customBg !== 'none') {
return;
}
// if clicked on upload button
if (!this_bgfile) {
return;
}
const backgroundUrl = `backgrounds/${this_bgfile}`;
// fetching to browser memory to reduce flicker
fetch(backgroundUrl).then(() => {
$("#bg1").css(
"background-image",
`url("${backgroundUrl}")`
);
setBackground(this_bgfile);
}).catch(() => {
console.log('Background could not be set: ' + backgroundUrl);
});
});
$(document).on('click', '.bg_example_edit', async function (e) {
e.stopPropagation();
const old_bg = $(this).attr('bgfile');
if (!old_bg) {
console.debug('no bgfile');
return;
}
const fileExtension = old_bg.split('.').pop();
const old_bg_extensionless = old_bg.replace(`.${fileExtension}`, '');
const new_bg_extensionless = await callPopup('<h3>Enter new background name:</h3>', 'input', old_bg_extensionless);
if (!new_bg_extensionless) {
console.debug('no new_bg_extensionless');
return;
}
const new_bg = `${new_bg_extensionless}.${fileExtension}`;
if (old_bg_extensionless === new_bg_extensionless) {
console.debug('new_bg === old_bg');
return;
}
const data = { old_bg, new_bg };
const response = await fetch('/renamebackground', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(data),
cache: 'no-cache',
});
if (response.ok) {
await getBackgrounds();
} else {
toastr.warning('Failed to rename background');
}
});
$(document).on("click", ".bg_example_cross", function (e) {
e.stopPropagation();
bg_file_for_del = $(this);
//$(this).parent().remove();
//delBackground(this_bgfile);
popup_type = "del_bg";
callPopup("<h3>Delete the background?</h3>");
});
$(document).on("click", ".PastChat_cross", function () {
chat_file_for_del = $(this).attr('file_name');
console.debug('detected cross click for' + chat_file_for_del);
@ -7476,10 +7298,6 @@ jQuery(async function () {
dialogueResolve($("#avatarToCrop").data('cropper').getCroppedCanvas().toDataURL('image/jpeg'));
};
if (popup_type == "del_bg") {
delBackground(bg_file_for_del.attr("bgfile"));
bg_file_for_del.parent().remove();
}
if (popup_type == "del_chat") {
//close past chat popup
$("#select_chat_cross").click();
@ -7574,10 +7392,6 @@ jQuery(async function () {
});
$("#add_bg_button").change(function () {
read_bg_load(this);
});
$("#add_avatar_button").change(function () {
read_avatar_load(this);
});
@ -7917,6 +7731,7 @@ jQuery(async function () {
$("#rm_button_selected_ch").children("h2").text('');
select_rm_characters();
sendSystemMessage(system_message_types.WELCOME);
eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
} else {
toastr.info("Please stop the message generation first.");
}
@ -8268,6 +8083,33 @@ jQuery(async function () {
}, 150);
})
$(document).on("click", function (e) {
// Expanded options don't need to be closed
if (power_user.expand_message_actions) {
return;
}
// Check if the click was outside the relevant elements
if (!$(e.target).closest('.extraMesButtons, .extraMesButtonsHint').length) {
// Transition out the .extraMesButtons first
$('.extraMesButtons:visible').transition({
opacity: 0,
duration: 150,
easing: 'ease-in-out',
complete: function () {
$(this).hide(); // Hide the .extraMesButtons after the transition
// Transition the .extraMesButtonsHint back in
$('.extraMesButtonsHint:not(:visible)').show().transition({
opacity: .2,
duration: 150,
easing: 'ease-in-out'
});
}
});
}
});
$(document).on("click", ".mes_edit_cancel", function () {
let text = chat[this_edit_mes_id]["mes"];
@ -8818,18 +8660,6 @@ jQuery(async function () {
}
});
$("#bg-filter").on("input", function () {
const filterValue = String($(this).val()).toLowerCase();
$("#bg_menu_content > div").each(function () {
const $bgContent = $(this);
if ($bgContent.attr("title").toLowerCase().includes(filterValue)) {
$bgContent.show();
} else {
$bgContent.hide();
}
});
});
$("#char-management-dropdown").on('change', async (e) => {
let target = $(e.target.selectedOptions).attr('id');
switch (target) {
@ -8974,34 +8804,6 @@ jQuery(async function () {
}
});
/**
* Handles the click event for the third-party extension import button.
* Prompts the user to enter the Git URL of the extension to import.
* After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
* If the extension is imported successfully, a success message is displayed.
* If the extension import fails, an error message is displayed and the error is logged to the console.
* After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
*
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', async () => {
const html = `<h3>Enter the Git URL of the extension to import</h3>
<br>
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
<br>
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Extension import cancelled');
return;
}
const url = input.trim();
await installExtension(url);
});
const $dropzone = $(document.body);
$dropzone.on('dragover', (event) => {

View File

@ -18,6 +18,8 @@ import {
getThumbnailUrl,
selectCharacterById,
eventSource,
menu_type,
substituteParams,
} from "../script.js";
import {
@ -234,7 +236,9 @@ export function RA_CountCharTokens() {
total_tokens += Number(counter.text());
permanent_tokens += isPermanent ? Number(counter.text()) : 0;
} else {
const tokens = getTokenCount(value);
// We substitute macro for existing characters, but not for the character being created
const valueToCount = menu_type === 'create' ? value : substituteParams(value);
const tokens = getTokenCount(valueToCount);
counter.text(tokens);
total_tokens += tokens;
permanent_tokens += isPermanent ? tokens : 0;
@ -897,6 +901,9 @@ export function initRossMods() {
//Regenerate if user swipes on the last mesage in chat
document.addEventListener('swiped-left', function (e) {
if (power_user.gestures === false) {
return
}
var SwipeButR = $('.swipe_right:last');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) {
@ -906,6 +913,9 @@ export function initRossMods() {
}
});
document.addEventListener('swiped-right', function (e) {
if (power_user.gestures === false) {
return
}
var SwipeButL = $('.swipe_left:last');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) {

View File

@ -0,0 +1,488 @@
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl } from "../script.js";
import { saveMetadataDebounced } from "./extensions.js";
import { registerSlashCommand } from "./slash-commands.js";
import { stringFormat } from "./utils.js";
const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
/**
* Sets the background for the current chat and adds it to the list of custom backgrounds.
* @param {{url: string, path:string}} backgroundInfo
*/
function forceSetBackground(backgroundInfo) {
saveBackgroundMetadata(backgroundInfo.url);
setCustomBackground();
const list = chat_metadata[LIST_METADATA_KEY] || [];
const bg = backgroundInfo.path;
list.push(bg);
chat_metadata[LIST_METADATA_KEY] = list;
saveMetadataDebounced();
getChatBackgroundsList();
highlightNewBackground(bg);
highlightLockedBackground();
}
async function onChatChanged() {
if (hasCustomBackground()) {
setCustomBackground();
}
else {
unsetCustomBackground();
}
getChatBackgroundsList();
highlightLockedBackground();
}
function getChatBackgroundsList() {
const list = chat_metadata[LIST_METADATA_KEY];
const listEmpty = !Array.isArray(list) || list.length === 0;
$('#bg_custom_content').empty();
$('#bg_chat_hint').toggle(listEmpty);
if (listEmpty) {
return;
}
for (const bg of list) {
const template = getBackgroundFromTemplate(bg, true);
$('#bg_custom_content').append(template);
}
}
function getBackgroundPath(fileUrl) {
return `backgrounds/${fileUrl}`;
}
function highlightLockedBackground() {
$('.bg_example').removeClass('locked');
const lockedBackground = chat_metadata[BG_METADATA_KEY];
if (!lockedBackground) {
return;
}
$(`.bg_example`).each(function () {
const url = $(this).data('url');
if (url === lockedBackground) {
$(this).addClass('locked');
}
});
}
function onLockBackgroundClick(e) {
e.stopPropagation();
const chatName = getCurrentChatId();
if (!chatName) {
toastr.warning('Select a chat to lock the background for it');
return;
}
const relativeBgImage = getUrlParameter(this);
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
highlightLockedBackground();
}
function onUnlockBackgroundClick(e) {
e.stopPropagation();
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
}
function hasCustomBackground() {
return chat_metadata[BG_METADATA_KEY];
}
function saveBackgroundMetadata(file) {
chat_metadata[BG_METADATA_KEY] = file;
saveMetadataDebounced();
}
function removeBackgroundMetadata() {
delete chat_metadata[BG_METADATA_KEY];
saveMetadataDebounced();
}
function setCustomBackground() {
const file = chat_metadata[BG_METADATA_KEY];
// bg already set
if (document.getElementById("bg_custom").style.backgroundImage == file) {
return;
}
$("#bg_custom").css("background-image", file);
}
function unsetCustomBackground() {
$("#bg_custom").css("background-image", 'none');
}
function onSelectBackgroundClick() {
const isCustom = $(this).attr('custom') === 'true';
const relativeBgImage = getUrlParameter(this);
// if clicked on upload button
if (!relativeBgImage) {
return;
}
// Automatically lock the background if it's custom or other background is locked
if (hasCustomBackground() || isCustom) {
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
highlightLockedBackground();
} else {
highlightLockedBackground();
}
const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage;
// Custom background is set. Do not override the layer below
if (customBg !== 'none') {
return;
}
const bgFile = $(this).attr("bgfile");
const backgroundUrl = getBackgroundPath(bgFile);
// Fetching to browser memory to reduce flicker
fetch(backgroundUrl).then(() => {
$("#bg1").css("background-image", relativeBgImage);
setBackground(bgFile);
}).catch(() => {
console.log('Background could not be set: ' + backgroundUrl);
});
}
async function onCopyToSystemBackgroundClick(e) {
e.stopPropagation();
const bgNames = await getNewBackgroundName(this);
if (!bgNames) {
return;
}
const bgFile = await fetch(bgNames.oldBg);
if (!bgFile.ok) {
toastr.warning('Failed to copy background');
return;
}
const blob = await bgFile.blob();
const file = new File([blob], bgNames.newBg);
const formData = new FormData();
formData.set('avatar', file);
uploadBackground(formData);
const list = chat_metadata[LIST_METADATA_KEY] || [];
const index = list.indexOf(bgNames.oldBg);
list.splice(index, 1);
saveMetadataDebounced();
getChatBackgroundsList();
}
/**
* Gets the new background name from the user.
* @param {Element} referenceElement
* @returns {Promise<{oldBg: string, newBg: string}>}
* */
async function getNewBackgroundName(referenceElement) {
const exampleBlock = $(referenceElement).closest('.bg_example');
const isCustom = exampleBlock.attr('custom') === 'true';
const oldBg = exampleBlock.attr('bgfile');
if (!oldBg) {
console.debug('no bgfile');
return;
}
const fileExtension = oldBg.split('.').pop();
const fileNameBase = isCustom ? oldBg.split('/').pop() : oldBg;
const oldBgExtensionless = fileNameBase.replace(`.${fileExtension}`, '');
const newBgExtensionless = await callPopup('<h3>Enter new background name:</h3>', 'input', oldBgExtensionless);
if (!newBgExtensionless) {
console.debug('no new_bg_extensionless');
return;
}
const newBg = `${newBgExtensionless}.${fileExtension}`;
if (oldBgExtensionless === newBgExtensionless) {
console.debug('new_bg === old_bg');
return;
}
return { oldBg, newBg };
}
async function onRenameBackgroundClick(e) {
e.stopPropagation();
const bgNames = await getNewBackgroundName(this);
if (!bgNames) {
return;
}
const data = { old_bg: bgNames.oldBg, new_bg: bgNames.newBg };
const response = await fetch('/renamebackground', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(data),
cache: 'no-cache',
});
if (response.ok) {
await getBackgrounds();
highlightNewBackground(bgNames.newBg);
} else {
toastr.warning('Failed to rename background');
}
}
async function onDeleteBackgroundClick(e) {
e.stopPropagation();
const bgToDelete = $(this).closest('.bg_example');
const url = bgToDelete.data('url');
const isCustom = bgToDelete.attr('custom') === 'true';
const confirm = await callPopup("<h3>Delete the background?</h3>", 'confirm');
const bg = bgToDelete.attr('bgfile');
if (confirm) {
// If it's not custom, it's a built-in background. Delete it from the server
if (!isCustom) {
delBackground(bg);
} else {
const list = chat_metadata[LIST_METADATA_KEY] || [];
const index = list.indexOf(bg);
list.splice(index, 1);
}
const siblingSelector = '.bg_example:not(#form_bg_download)';
const nextBg = bgToDelete.next(siblingSelector);
const prevBg = bgToDelete.prev(siblingSelector);
const anyBg = $(siblingSelector);
if (nextBg.length > 0) {
nextBg.trigger('click');
} else if (prevBg.length > 0) {
prevBg.trigger('click');
} else {
$(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click');
}
bgToDelete.remove();
if (url === chat_metadata[BG_METADATA_KEY]) {
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
}
if (isCustom) {
getChatBackgroundsList();
saveMetadataDebounced();
}
}
}
const autoBgPrompt = `Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}`;
async function autoBackgroundCommand() {
/** @type {HTMLElement[]} */
const bgTitles = Array.from(document.querySelectorAll('#bg_menu_content .BGSampleTitle'));
const options = bgTitles.map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return;
}
const list = options.map(option => `- ${option.text}`).join('\n');
const prompt = stringFormat(autoBgPrompt, list);
const reply = await generateQuietPrompt(prompt, false, false);
const fuse = new Fuse(options, { keys: ['text'] });
const bestMatch = fuse.search(reply, { limit: 1 });
if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.');
return;
}
console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click();
}
export async function getBackgrounds() {
const response = await fetch("/getbackgrounds", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
"": "",
}),
});
if (response.ok === true) {
const getData = await response.json();
//background = getData;
//console.log(getData.length);
$("#bg_menu_content").children('div').remove();
for (const bg of getData) {
const template = getBackgroundFromTemplate(bg, false);
$("#bg_menu_content").append(template);
}
}
}
/**
* Gets the URL of the background
* @param {Element} block
* @returns {string} URL of the background
*/
function getUrlParameter(block) {
return $(block).closest(".bg_example").data("url");
}
/**
* Instantiates a background template
* @param {string} bg Path to background
* @param {boolean} isCustom Whether the background is custom
* @returns {JQuery<HTMLElement>} Background template
*/
function getBackgroundFromTemplate(bg, isCustom) {
const template = $('#background_template .bg_example').clone();
const thumbPath = isCustom ? bg : getThumbnailUrl('bg', bg);
const url = isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;
const title = isCustom ? bg.split('/').pop() : bg;
const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
template.attr('title', title);
template.attr('bgfile', bg);
template.attr('custom', String(isCustom));
template.data('url', url);
template.css('background-image', `url('${thumbPath}')`);
template.find('.BGSampleTitle').text(friendlyTitle);
return template;
}
async function setBackground(bg) {
jQuery.ajax({
type: "POST", //
url: "/setbackground", //
data: JSON.stringify({
bg: bg,
}),
beforeSend: function () {
},
cache: false,
dataType: "json",
contentType: "application/json",
//processData: false,
success: function (html) { },
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
}
async function delBackground(bg) {
const response = await fetch("/delbackground", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
bg: bg,
}),
});
}
function onBackgroundUploadSelected() {
const form = $("#form_bg_download").get(0);
if (!(form instanceof HTMLFormElement)) {
console.error('form_bg_download is not a form');
return;
}
const formData = new FormData(form);
uploadBackground(formData);
form.reset();
}
/**
* Uploads a background to the server
* @param {FormData} formData
*/
function uploadBackground(formData) {
jQuery.ajax({
type: "POST",
url: "/downloadbackground",
data: formData,
beforeSend: function () {
},
cache: false,
contentType: false,
processData: false,
success: async function (bg) {
setBackground(bg);
$("#bg1").css("background-image", `url("${getBackgroundPath(bg)}"`);
await getBackgrounds();
highlightNewBackground(bg);
},
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
}
/**
* @param {string} bg
*/
function highlightNewBackground(bg) {
const newBg = $(`.bg_example[bgfile="${bg}"]`);
const scrollOffset = newBg.offset().top - newBg.parent().offset().top;
$('#Backgrounds').scrollTop(scrollOffset);
newBg.addClass('flash animated');
setTimeout(() => newBg.removeClass('flash animated'), 2000);
}
function onBackgroundFilterInput() {
const filterValue = String($(this).val()).toLowerCase();
$("#bg_menu_content > div").each(function () {
const $bgContent = $(this);
if ($bgContent.attr("title").toLowerCase().includes(filterValue)) {
$bgContent.show();
} else {
$bgContent.hide();
}
});
}
export function initBackgrounds() {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
$(document).on("click", '.bg_example', onSelectBackgroundClick);
$(document).on('click', '.bg_example_lock', onLockBackgroundClick);
$(document).on('click', '.bg_example_unlock', onUnlockBackgroundClick);
$(document).on('click', '.bg_example_edit', onRenameBackgroundClick);
$(document).on("click", '.bg_example_cross', onDeleteBackgroundClick);
$(document).on("click", '.bg_example_copy', onCopyToSystemBackgroundClick);
$('#auto_background').on("click", autoBackgroundCommand);
$("#add_bg_button").on('change', onBackgroundUploadSelected);
$("#bg-filter").on("input", onBackgroundFilterInput);
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], " locks a background for the currently selected chat", true, true);
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' unlocks a background for the currently selected chat', true, true);
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' automatically changes the background based on the chat context using the AI request prompt', true, true);
}

View File

@ -1,4 +1,4 @@
import { characters, getCharacters, handleDeleteCharacter, callPopup } from "../../../script.js";
import { characters, getCharacters, handleDeleteCharacter, callPopup } from "../script.js";
let is_bulk_edit = false;
@ -64,23 +64,6 @@ async function onDeleteButtonClick() {
}
}
/**
* Adds the bulk edit and delete buttons to the UI.
*/
function addButtons() {
const editButton = $(
"<i id='bulkEditButton' class='fa-solid fa-edit menu_button bulkEditButton' title='Bulk edit characters'></i>"
);
const deleteButton = $(
"<i id='bulkDeleteButton' class='fa-solid fa-trash menu_button bulkDeleteButton' title='Bulk delete characters' style='display: none;'></i>"
);
$("#charListGridToggle").after(editButton, deleteButton);
$("#bulkEditButton").on("click", onEditButtonClick);
$("#bulkDeleteButton").on("click", onDeleteButtonClick);
}
/**
* Enables bulk selection by adding a checkbox next to each character.
*/
@ -111,7 +94,7 @@ function disableBulkSelect() {
/**
* Entry point that runs on page load.
*/
jQuery(async () => {
addButtons();
// loadSettings();
jQuery(() => {
$("#bulkEditButton").on("click", onEditButtonClick);
$("#bulkDeleteButton").on("click", onDeleteButtonClick);
});

View File

@ -1,19 +1,17 @@
import {
chat_metadata,
substituteParams,
this_chid,
eventSource,
event_types,
saveSettingsDebounced,
this_chid,
} from "../../../script.js";
import { selected_group } from "../../group-chats.js";
import { extension_settings, saveMetadataDebounced } from "../../extensions.js";
import { getCharaFilename, delay } from "../../utils.js";
import { power_user } from "../../power-user.js";
import { metadataKeys } from "./util.js";
} from "../script.js";
import { extension_settings, saveMetadataDebounced } from "./extensions.js"
import { selected_group } from "./group-chats.js";
import { getCharaFilename, delay } from "./utils.js";
import { power_user } from "./power-user.js";
// Keep track of where your extension is located, name should match repo name
const extensionName = "cfg";
const extensionFolderPath = `scripts/extensions/${extensionName}`;
const extensionName = 'cfg';
const defaultSettings = {
global: {
"guidance_scale": 1,
@ -199,7 +197,7 @@ function loadSettings() {
if (!promptSeparator.startsWith(`"`)) {
promptSeparatorDisplay.unshift(`"`);
}
if (!promptSeparator.endsWith(`"`)) {
promptSeparatorDisplay.push(`"`);
}
@ -279,14 +277,8 @@ function migrateSettings() {
}
// This function is called when the extension is loaded
jQuery(async () => {
// This is an example of loading HTML from a file
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
// Append settingsHtml to extensions_settings
// extension_settings and extensions_settings2 are the left and right columns of the settings menu
// Left should be extensions that deal with system functions and right should be visual/UI related
windowHtml.find('#CFGClose').on('click', function () {
export function initCfg() {
$('#CFGClose').on('click', function () {
$("#cfgConfig").transition({
opacity: 0,
duration: 200,
@ -295,7 +287,7 @@ jQuery(async () => {
setTimeout(function () { $('#cfgConfig').hide() }, 200);
});
windowHtml.find('#chat_cfg_guidance_scale').on('input', function() {
$('#chat_cfg_guidance_scale').on('input', function() {
const numberValue = Number($(this).val());
const success = setChatCfg(numberValue, settingType.guidance_scale);
if (success) {
@ -303,15 +295,15 @@ jQuery(async () => {
}
});
windowHtml.find('#chat_cfg_negative_prompt').on('input', function() {
$('#chat_cfg_negative_prompt').on('input', function() {
setChatCfg($(this).val(), settingType.negative_prompt);
});
windowHtml.find('#chat_cfg_positive_prompt').on('input', function() {
$('#chat_cfg_positive_prompt').on('input', function() {
setChatCfg($(this).val(), settingType.positive_prompt);
});
windowHtml.find('#chara_cfg_guidance_scale').on('input', function() {
$('#chara_cfg_guidance_scale').on('input', function() {
const value = $(this).val();
const success = setCharCfg(value, settingType.guidance_scale);
if (success) {
@ -319,34 +311,34 @@ jQuery(async () => {
}
});
windowHtml.find('#chara_cfg_negative_prompt').on('input', function() {
$('#chara_cfg_negative_prompt').on('input', function() {
setCharCfg($(this).val(), settingType.negative_prompt);
});
windowHtml.find('#chara_cfg_positive_prompt').on('input', function() {
$('#chara_cfg_positive_prompt').on('input', function() {
setCharCfg($(this).val(), settingType.positive_prompt);
});
windowHtml.find('#global_cfg_guidance_scale').on('input', function() {
$('#global_cfg_guidance_scale').on('input', function() {
extension_settings.cfg.global.guidance_scale = Number($(this).val());
$('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2));
saveSettingsDebounced();
});
windowHtml.find('#global_cfg_negative_prompt').on('input', function() {
$('#global_cfg_negative_prompt').on('input', function() {
extension_settings.cfg.global.negative_prompt = $(this).val();
saveSettingsDebounced();
});
windowHtml.find('#global_cfg_positive_prompt').on('input', function() {
$('#global_cfg_positive_prompt').on('input', function() {
extension_settings.cfg.global.positive_prompt = $(this).val();
saveSettingsDebounced();
});
windowHtml.find(`input[name="cfg_prompt_combine"]`).on('input', function() {
const values = windowHtml.find(`input[name="cfg_prompt_combine"]`)
$(`input[name="cfg_prompt_combine"]`).on('input', function() {
const values = $('#cfgConfig').find(`input[name="cfg_prompt_combine"]`)
.filter(":checked")
.map(function() { return parseInt($(this).val()) })
.map(function() { return Number($(this).val()) })
.get()
.filter((e) => !Number.isNaN(e)) || [];
@ -354,17 +346,17 @@ jQuery(async () => {
saveMetadataDebounced();
});
windowHtml.find(`#cfg_prompt_insertion_depth`).on('input', function() {
$(`#cfg_prompt_insertion_depth`).on('input', function() {
chat_metadata[metadataKeys.prompt_insertion_depth] = Number($(this).val());
saveMetadataDebounced();
});
windowHtml.find(`#cfg_prompt_separator`).on('input', function() {
$(`#cfg_prompt_separator`).on('input', function() {
chat_metadata[metadataKeys.prompt_separator] = $(this).val();
saveMetadataDebounced();
});
windowHtml.find('#groupchat_cfg_use_chara').on('input', function() {
$('#groupchat_cfg_use_chara').on('input', function() {
const checked = !!$(this).prop('checked');
chat_metadata[metadataKeys.groupchat_individual_chars] = checked
@ -375,20 +367,126 @@ jQuery(async () => {
saveMetadataDebounced();
});
$("#movingDivs").append(windowHtml);
initialLoadSettings();
if (extension_settings.cfg) {
migrateSettings();
}
const buttonHtml = $(await $.get(`${extensionFolderPath}/menuButton.html`));
buttonHtml.on('click', onCfgMenuItemClick)
buttonHtml.appendTo("#options_advanced");
$('#option_toggle_CFG').on('click', onCfgMenuItemClick);
// Hook events
eventSource.on(event_types.CHAT_CHANGED, async () => {
await onChatChanged();
});
});
}
export const cfgType = {
chat: 0,
chara: 1,
global: 2
}
export const metadataKeys = {
guidance_scale: "cfg_guidance_scale",
negative_prompt: "cfg_negative_prompt",
positive_prompt: "cfg_positive_prompt",
prompt_combine: "cfg_prompt_combine",
groupchat_individual_chars: "cfg_groupchat_individual_chars",
prompt_insertion_depth: "cfg_prompt_insertion_depth",
prompt_separator: "cfg_prompt_separator"
}
// Gets the CFG guidance scale
// If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
export function getGuidanceScale() {
if (!extension_settings.cfg) {
console.warn("CFG extension is not enabled. Skipping CFG guidance.");
return;
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) {
return {
type: cfgType.chat,
value: chatGuidanceScale
};
}
if ((!selected_group && charaCfg || groupchatCharOverride) && charaCfg?.guidance_scale !== 1) {
return {
type: cfgType.chara,
value: charaCfg.guidance_scale
};
}
if (extension_settings.cfg.global && extension_settings.cfg.global?.guidance_scale !== 1) {
return {
type: cfgType.global,
value: extension_settings.cfg.global.guidance_scale
};
}
}
/**
* Gets the CFG prompt separator.
* @returns {string} The CFG prompt separator
*/
function getCustomSeparator() {
const defaultSeparator = "\n";
try {
if (chat_metadata[metadataKeys.prompt_separator]) {
return JSON.parse(chat_metadata[metadataKeys.prompt_separator]);
}
return defaultSeparator;
} catch {
console.warn("Invalid JSON detected for prompt separator. Using default separator.");
return defaultSeparator;
}
}
// Gets the CFG prompt
export function getCfgPrompt(guidanceScale, isNegative) {
let splitCfgPrompt = [];
const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? [];
if (guidanceScale.type === cfgType.chat || cfgPromptCombine.includes(cfgType.chat)) {
splitCfgPrompt.unshift(
substituteParams(
chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt]
)
);
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
if (guidanceScale.type === cfgType.chara || cfgPromptCombine.includes(cfgType.chara)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt
)
);
}
if (guidanceScale.type === cfgType.global || cfgPromptCombine.includes(cfgType.global)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt
)
);
}
const customSeparator = getCustomSeparator();
const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator);
const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1;
console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
return {
value: combinedCfgPrompt,
depth: insertionDepth
};
}

View File

@ -123,6 +123,7 @@ const extension_settings = {
apiUrl: defaultUrl,
apiKey: '',
autoConnect: false,
notifyUpdates: false,
disabledExtensions: [],
expressionOverrides: [],
memory: {},
@ -367,6 +368,15 @@ function addExtensionsButtonAndMenu() {
});
}
function notifyUpdatesInputHandler() {
extension_settings.notifyUpdates = !!$('#extensions_notify_updates').prop('checked');
saveSettingsDebounced();
if (extension_settings.notifyUpdates) {
checkForExtensionUpdates(true);
}
}
/* $(document).on('click', function (e) {
const target = $(e.target);
if (target.is(dropdown)) return;
@ -582,16 +592,25 @@ async function showExtensionsDetails() {
let htmlExternal = '<h3>External Extensions:</h3>';
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
const promises = [];
for (const extension of extensions) {
const { isExternal, extensionHtml } = await getExtensionData(extension);
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
promises.push(getExtensionData(extension));
}
const settledPromises = await Promise.allSettled(promises);
settledPromises.forEach(promise => {
if (promise.status === 'fulfilled') {
const { isExternal, extensionHtml } = promise.value;
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
}
});
const html = `
${getModuleInformation()}
${htmlDefault}
@ -703,7 +722,9 @@ async function getExtensionVersion(extensionName) {
* @returns {Promise<void>}
*/
export async function installExtension(url) {
console.debug('Extension import started', url);
console.debug('Extension installation started', url);
toastr.info('Please wait...', 'Installing extension');
const request = await fetch('/api/extensions/install', {
method: 'POST',
@ -712,14 +733,14 @@ export async function installExtension(url) {
});
if (!request.ok) {
toastr.info(request.statusText, 'Extension import failed');
console.error('Extension import failed', request.status, request.statusText);
toastr.info(request.statusText, 'Extension installation failed');
console.error('Extension installation failed', request.status, request.statusText);
return;
}
const response = await request.json();
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been imported successfully!`, 'Extension import successful');
console.debug(`Extension "${response.display_name}" has been imported successfully at ${response.extensionPath}`);
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful');
console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
await loadExtensionSettings({}, false);
eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
}
@ -737,6 +758,7 @@ async function loadExtensionSettings(settings, versionChanged) {
$("#extensions_url").val(extension_settings.apiUrl);
$("#extensions_api_key").val(extension_settings.apiKey);
$("#extensions_autoconnect").prop('checked', extension_settings.autoConnect);
$("#extensions_notify_updates").prop('checked', extension_settings.notifyUpdates);
// Activate offline extensions
eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD);
@ -752,6 +774,55 @@ async function loadExtensionSettings(settings, versionChanged) {
connectToApi(extension_settings.apiUrl);
}
if (extension_settings.notifyUpdates) {
checkForExtensionUpdates(false);
}
}
/**
* Checks if there are updates available for 3rd-party extensions.
* @param {boolean} force Skip nag check
* @returns {Promise<any>}
*/
async function checkForExtensionUpdates(force) {
if (!force) {
const STORAGE_NAG_KEY = 'extension_update_nag';
const currentDate = new Date().toDateString();
// Don't nag more than once a day
if (localStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
return;
}
localStorage.setItem(STORAGE_NAG_KEY, currentDate);
}
const updatesAvailable = [];
const promises = [];
for (const [id, manifest] of Object.entries(manifests)) {
if (manifest.auto_update && id.startsWith('third-party')) {
const promise = new Promise(async (resolve, reject) => {
try {
const data = await getExtensionVersion(id.replace('third-party', ''));
if (data.isUpToDate === false) {
updatesAvailable.push(manifest.display_name);
}
resolve();
} catch (error) {
console.error('Error checking for extension updates', error);
reject();
}
});
promises.push(promise);
}
}
await Promise.allSettled(promises);
if (updatesAvailable.length > 0) {
toastr.info(`${updatesAvailable.map(x => `${x}`).join('\n')}`, 'Extension updates available');
}
}
async function autoUpdateExtensions() {
@ -783,8 +854,36 @@ jQuery(function () {
$("#extensions_connect").on('click', connectClickHandler);
$("#extensions_autoconnect").on('input', autoConnectInputHandler);
$("#extensions_details").on('click', showExtensionsDetails);
$("#extensions_notify_updates").on('input', notifyUpdatesInputHandler);
$(document).on('click', '.toggle_disable', onDisableExtensionClick);
$(document).on('click', '.toggle_enable', onEnableExtensionClick);
$(document).on('click', '.btn_update', onUpdateClick);
$(document).on('click', '.btn_delete', onDeleteClick);
/**
* Handles the click event for the third-party extension import button.
* Prompts the user to enter the Git URL of the extension to import.
* After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
* If the extension is imported successfully, a success message is displayed.
* If the extension import fails, an error message is displayed and the error is logged to the console.
* After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
*
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', async () => {
const html = `<h3>Enter the Git URL of the extension to install</h3>
<br>
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
<br>
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Extension install cancelled');
return;
}
const url = input.trim();
await installExtension(url);
});
});

View File

@ -1,178 +0,0 @@
import { eventSource, event_types, generateQuietPrompt } from "../../../script.js";
import { getContext, saveMetadataDebounced } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { stringFormat } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'backgrounds';
const METADATA_KEY = 'custom_background';
/**
* @param {string} background
*/
function forceSetBackground(background) {
saveBackgroundMetadata(background);
setCustomBackground();
}
async function moduleWorker() {
if (hasCustomBackground()) {
$('#unlock_background').show();
$('#lock_background').hide();
setCustomBackground();
}
else {
$('#unlock_background').hide();
$('#lock_background').show();
unsetCustomBackground();
}
}
function onLockBackgroundClick() {
const bgImage = window.getComputedStyle(document.getElementById('bg1')).backgroundImage;
// Extract the URL from the CSS string
const urlRegex = /url\((['"])?(.*?)\1\)/;
const matches = bgImage.match(urlRegex);
const url = matches[2];
// Remove the protocol and host, leaving the relative URL
const relativeUrl = new URL(url).pathname;
const relativeBgImage = `url("${relativeUrl}")`
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
$('#unlock_background').show();
$('#lock_background').hide();
}
function onUnlockBackgroundClick() {
removeBackgroundMetadata();
unsetCustomBackground();
$('#unlock_background').hide();
$('#lock_background').show();
}
function hasCustomBackground() {
const context = getContext();
return !!context.chatMetadata[METADATA_KEY];
}
function saveBackgroundMetadata(file) {
const context = getContext();
context.chatMetadata[METADATA_KEY] = file;
saveMetadataDebounced();
}
function removeBackgroundMetadata() {
const context = getContext();
delete context.chatMetadata[METADATA_KEY];
saveMetadataDebounced();
}
function setCustomBackground() {
const context = getContext();
const file = context.chatMetadata[METADATA_KEY];
// bg already set
if (document.getElementById("bg_custom").style.backgroundImage == file) {
return;
}
$("#bg_custom").css("background-image", file);
$("#custom_bg_preview").css("background-image", file);
}
function unsetCustomBackground() {
$("#bg_custom").css("background-image", 'none');
$("#custom_bg_preview").css("background-image", 'none');
}
function onSelectBackgroundClick() {
const bgfile = $(this).attr("bgfile");
if (hasCustomBackground()) {
saveBackgroundMetadata(`url("backgrounds/${bgfile}")`);
setCustomBackground();
}
}
const autoBgPrompt = `Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}`;
async function autoBackgroundCommand() {
const options = Array.from(document.querySelectorAll('.BGSampleTitle')).map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return;
}
const list = options.map(option => `- ${option.text}`).join('\n');
const prompt = stringFormat(autoBgPrompt, list);
const reply = await generateQuietPrompt(prompt);
const fuse = new Fuse(options, { keys: ['text'] });
const bestMatch = fuse.search(reply, { limit: 1 });
if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.');
return;
}
console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click();
}
$(document).ready(function () {
function addSettings() {
const html = `
<div class="background_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Chat Backgrounds</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="background_controls">
<div id="lock_background" class="menu_button">
<i class="fa-solid fa-lock"></i>
Lock
</div>
<div id="unlock_background" class="menu_button">
<i class="fa-solid fa-unlock"></i>
Unlock
</div>
<small>
Press "Lock" to assign a currently selected background to a character or group chat.<br>
Any background image selected while lock is engaged will be saved automatically.
</small>
</div>
<div class="background_controls">
<div id="auto_background" class="menu_button">
<i class="fa-solid fa-wand-magic"></i>
Auto
</div>
<small>
Automatically select a background based on the chat context.<br>
Respects the "Lock" setting state.
</small>
</div>
<div>Preview</div>
<div id="custom_bg_preview">
</div>
</div>
</div>
</div>
`;
$('#extensions_settings').append(html);
$('#lock_background').on('click', onLockBackgroundClick);
$('#unlock_background').on('click', onUnlockBackgroundClick);
$(document).on("click", ".bg_example", onSelectBackgroundClick);
$('#auto_background').on("click", autoBackgroundCommand);
}
addSettings();
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], " locks a background for the currently selected chat", true, true);
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' unlocks a background for the currently selected chat', true, true);
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' automatically changes the background based on the chat context using the AI request prompt', true, true);
eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
eventSource.on(event_types.CHAT_CHANGED, moduleWorker);
});

View File

@ -1,11 +0,0 @@
{
"display_name": "Chat Backgrounds",
"loading_order": 7,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,45 +0,0 @@
#custom_bg_preview {
width: 160px;
height: 90px;
background-color: var(--grey30a);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
border-radius: 20px;
border: 1px solid var(--SmartThemeBorderColor);
box-shadow: 0 0 7px var(--black50a);
margin: 5px;
}
#custom_bg_preview::before {
content: 'No Background';
color: white;
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#custom_bg_preview:not([style*="background-image: none"])::before {
display: none;
}
.background_controls .menu_button {
display: flex;
flex-direction: row;
align-items: center;
column-gap: 10px;
}
.background_controls {
display: flex;
flex-direction: row;
align-items: center;
column-gap: 10px;
}
.background_controls small {
flex-grow: 1;
}

View File

@ -1,11 +0,0 @@
{
"display_name": "Bulk Card Editor",
"loading_order": 9,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "city-unit",
"version": "1.0.0",
"homePage": "https://github.com/city-unit"
}

View File

@ -1,7 +0,0 @@
.bulk_select_checkbox {
align-self: center;
}
#rm_print_characters_block.bulk_select .wide100pLess70px {
width: calc(100% - 85px);
}

View File

@ -1,11 +0,0 @@
{
"display_name": "CFG",
"loading_order": 1,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "kingbri",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,4 +0,0 @@
<a id="option_toggle_CFG">
<i class="fa-lg fa-solid fa-scale-balanced"></i>
<span data-i18n="CFG Scale">CFG Scale</span>
</a>

View File

@ -1,95 +0,0 @@
import { chat_metadata, substituteParams, this_chid } from "../../../script.js";
import { extension_settings, getContext } from "../../extensions.js"
import { selected_group } from "../../group-chats.js";
import { getCharaFilename } from "../../utils.js";
export const cfgType = {
chat: 0,
chara: 1,
global: 2
}
export const metadataKeys = {
guidance_scale: "cfg_guidance_scale",
negative_prompt: "cfg_negative_prompt",
positive_prompt: "cfg_positive_prompt",
prompt_combine: "cfg_prompt_combine",
groupchat_individual_chars: "cfg_groupchat_individual_chars",
prompt_insertion_depth: "cfg_prompt_insertion_depth",
prompt_separator: "cfg_prompt_separator"
}
// Gets the CFG guidance scale
// If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
export function getGuidanceScale() {
if (!extension_settings.cfg) {
console.warn("CFG extension is not enabled. Skipping CFG guidance.");
return;
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) {
return {
type: cfgType.chat,
value: chatGuidanceScale
};
}
if ((!selected_group && charaCfg || groupchatCharOverride) && charaCfg?.guidance_scale !== 1) {
return {
type: cfgType.chara,
value: charaCfg.guidance_scale
};
}
if (extension_settings.cfg.global && extension_settings.cfg.global?.guidance_scale !== 1) {
return {
type: cfgType.global,
value: extension_settings.cfg.global.guidance_scale
};
}
}
// Gets the CFG prompt
export function getCfgPrompt(guidanceScale, isNegative) {
let splitCfgPrompt = [];
const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? [];
if (guidanceScale.type === cfgType.chat || cfgPromptCombine.includes(cfgType.chat)) {
splitCfgPrompt.unshift(
substituteParams(
chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt]
)
);
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
if (guidanceScale.type === cfgType.chara || cfgPromptCombine.includes(cfgType.chara)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt
)
);
}
if (guidanceScale.type === cfgType.global || cfgPromptCombine.includes(cfgType.global)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt
)
);
}
// This line is a bit hacky with a JSON.stringify and JSON.parse. Fix this if possible.
const customSeparator = JSON.parse(chat_metadata[metadataKeys.prompt_separator] || JSON.stringify("\n")) ?? "\n";
const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator);
const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1;
console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
return {
value: combinedCfgPrompt,
depth: insertionDepth
};
}

View File

@ -1,172 +0,0 @@
<div id="cfgConfig" class="drawer-content flexGap5">
<div class="panelControlBar flex-container">
<div id="cfgConfigHeader" class="fa-solid fa-grip drag-grabber"></div>
<div id="CFGClose" class="fa-solid fa-circle-xmark"></div>
</div>
<div name="cfgConfigHolder" class="scrollY">
<div id="chat_cfg_container">
<div class="inline-drawer">
<div id="CFGBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Chat CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small>
<b>Unique to this chat.</b><br>
</small>
<label for="chat_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="chat_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="chat_cfg_guidance_scale" id="chat_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="chat_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="chat_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="chat_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="chat_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
<div id="groupchat_cfg_use_chara_container">
<label class="checkbox_label" for="groupchat_cfg_use_chara">
<input type="checkbox" id="groupchat_cfg_use_chara" />
<span data-i18n="Use character CFG scales">Use character CFG scales</span>
</label>
</div>
</div>
</div>
</div>
<div id="chara_cfg_container" style="display: none;">
<hr class="sysHR">
<div class="inline-drawer">
<div id="charaANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Character CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><b>Will be automatically added as the CFG for this character.</b></small>
<br />
<label for="chara_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="chara_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="chara_cfg_guidance_scale" id="chara_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="chara_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="chara_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="chara_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="chara_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
</div>
</div>
</div>
<div id="global_cfg_container">
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Global CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><b>Will be used as the default CFG options for every chat unless overridden.</b></small>
<br />
<label for="global_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="global_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="global_cfg_guidance_scale" id="global_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="global_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="global_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="global_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="global_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
</div>
</div>
</div>
<div id="cfg_prompt_combine_container">
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>CFG Prompt Cascading</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="flex-container flexFlowColumn">
<small>
<b>Combine positive/negative prompts from other boxes.</b>
<br />
For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.
</small>
</div>
<br />
<div class="flex-container flexFlowColumn">
<label for="cfg_prompt_combine">
<span data-i18n="Scale">Always Include</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="0" />
<span data-i18n="Chat Negatives">Chat Negatives</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="1" />
<span data-i18n="Character Negatives">Character Negatives</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="2" />
<span data-i18n="Global Negatives">Global Negatives</span>
</label>
</div>
<div class="flex-container flexFlowColumn">
<label>
Custom Separator: <input id="cfg_prompt_separator" class="text_pole textarea_compact widthUnset" placeholder="&quot;\n&quot;" type="text" />
</label>
<label>
Insertion Depth: <input id="cfg_prompt_insertion_depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -764,7 +764,7 @@ function sampleClassifyText(text) {
// Remove asterisks and quotes
let result = text.replace(/[\*\"]/g, '');
const SAMPLE_THRESHOLD = 300;
const SAMPLE_THRESHOLD = 500;
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
if (text.length < SAMPLE_THRESHOLD) {
@ -1498,6 +1498,8 @@ function setExpressionOverrideHtml(forceClear = false) {
if (isVisualNovelMode()) {
$('#visual-novel-wrapper').empty();
}
updateFunction();
});
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);

View File

@ -1,5 +1,5 @@
{
"display_name": "Memory",
"display_name": "Summarize",
"loading_order": 9,
"requires": [],
"optional": [

View File

@ -1,8 +1,7 @@
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams } from "../../../script.js";
import { getContext, extension_settings } from "../../extensions.js";
import { initScrollHeight, resetScrollHeight } from "../../utils.js";
import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "../../slash-commands.js";
import { registerSlashCommand } from "../../slash-commands.js";
export { MODULE_NAME };
@ -15,8 +14,9 @@ const defaultSettings = {
quickReplyEnabled: false,
numberOfSlots: 5,
quickReplySlots: [],
placeBeforePromptEnabled: false,
placeBeforeInputEnabled: false,
quickActionEnabled: false,
AutoInputInject: true,
}
//method from worldinfo
@ -35,8 +35,12 @@ async function updateQuickReplyPresetList() {
if (presets !== undefined) {
presets.forEach((item, i) => {
$("#quickReplyPresets").append(`<option value='${item.name}'${selected_preset.includes(item.name) ? ' selected' : ''}>${item.name}</option>`);
presets.forEach((item) => {
const option = document.createElement('option');
option.value = item.name;
option.innerText = item.name;
option.selected = selected_preset.includes(item.name);
$("#quickReplyPresets").append(option);
});
}
}
@ -50,6 +54,10 @@ async function loadSettings(type) {
Object.assign(extension_settings.quickReply, defaultSettings);
}
if (extension_settings.quickReply.AutoInputInject === undefined) {
extension_settings.quickReply.AutoInputInject = true;
}
// If the user has an old version of the extension, update it
if (!Array.isArray(extension_settings.quickReply.quickReplySlots)) {
extension_settings.quickReply.quickReplySlots = [];
@ -77,20 +85,21 @@ async function loadSettings(type) {
$('#quickReplyEnabled').prop('checked', extension_settings.quickReply.quickReplyEnabled);
$('#quickReplyNumberOfSlots').val(extension_settings.quickReply.numberOfSlots);
$('#placeBeforePromptEnabled').prop('checked', extension_settings.quickReply.placeBeforePromptEnabled);
$('#placeBeforeInputEnabled').prop('checked', extension_settings.quickReply.placeBeforeInputEnabled);
$('#quickActionEnabled').prop('checked', extension_settings.quickReply.quickActionEnabled);
$('#AutoInputInject').prop('checked', extension_settings.quickReply.AutoInputInject);
}
function onQuickReplyInput(id) {
extension_settings.quickReply.quickReplySlots[id - 1].mes = $(`#quickReply${id}Mes`).val();
$(`#quickReply${id}`).attr('title', ($(`#quickReply${id}Mes`).val()));
$(`#quickReply${id}`).attr('title', String($(`#quickReply${id}Mes`).val()));
resetScrollHeight($(`#quickReply${id}Mes`));
saveSettingsDebounced();
}
function onQuickReplyLabelInput(id) {
extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val();
$(`#quickReply${id}`).text($(`#quickReply${id}Label`).val());
$(`#quickReply${id}`).text(String($(`#quickReply${id}Label`).val()));
saveSettingsDebounced();
}
@ -109,8 +118,13 @@ async function onQuickActionEnabledInput() {
saveSettingsDebounced();
}
async function onPlaceBeforePromptEnabledInput() {
extension_settings.quickReply.placeBeforePromptEnabled = !!$(this).prop('checked');
async function onPlaceBeforeInputEnabledInput() {
extension_settings.quickReply.placeBeforeInputEnabled = !!$(this).prop('checked');
saveSettingsDebounced();
}
async function onAutoInputInject() {
extension_settings.quickReply.AutoInputInject = !!$(this).prop('checked');
saveSettingsDebounced();
}
@ -125,16 +139,15 @@ async function sendQuickReply(index) {
let newText;
if (existingText) {
// If existing text, add space after prompt
if (extension_settings.quickReply.placeBeforePromptEnabled) {
if (existingText && extension_settings.quickReply.AutoInputInject) {
if (extension_settings.quickReply.placeBeforeInputEnabled) {
newText = `${prompt} ${existingText} `;
} else {
newText = `${existingText} ${prompt} `;
}
} else {
// If no existing text, add prompt only (with a trailing space)
newText = prompt + ' ';
// If no existing text and placeBeforeInputEnabled false, add prompt only (with a trailing space)
newText = `${prompt} `;
}
newText = substituteParams(newText);
@ -142,9 +155,9 @@ async function sendQuickReply(index) {
$("#send_textarea").val(newText);
// Set the focus back to the textarea
$("#send_textarea").focus();
$("#send_textarea").trigger('focus');
// Only trigger send button if quickActionEnabled is not checked or
// Only trigger send button if quickActionEnabled is not checked or
// the prompt starts with '/'
if (!extension_settings.quickReply.quickActionEnabled || prompt.startsWith('/')) {
$("#send_but").trigger('click');
@ -221,7 +234,7 @@ async function saveQuickReplyPreset() {
}
else {
presets[quickReplyPresetIndex] = quickReplyPreset;
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true);
$(`#quickReplyPresets option[value="${name}"]`).prop('selected', true);
}
saveSettingsDebounced();
} else {
@ -274,8 +287,8 @@ function generateQuickReplyElements() {
for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) {
quickReplyHtml += `
<div class="flex-container alignitemsflexstart">
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Add a button label)">
<textarea id="quickReply${i}Mes" placeholder="(custom message here)" class="text_pole widthUnset flex1" rows="2"></textarea>
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Button label)">
<textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1" rows="2"></textarea>
</div>
`;
}
@ -309,7 +322,7 @@ async function applyQuickReplyPreset(name) {
addQuickReplyBar();
moduleWorker();
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true);
$(`#quickReplyPresets option[value="${name}"]`).prop('selected', true);
console.debug('QR Preset applied: ' + name);
}
@ -334,7 +347,6 @@ async function doQR(_, text) {
}
jQuery(async () => {
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
const settingsHtml = `
@ -348,20 +360,28 @@ jQuery(async () => {
<div>
<label class="checkbox_label">
<input id="quickReplyEnabled" type="checkbox" />
Enable Quick Replies
Enable Quick Replies
</label>
<label class="checkbox_label">
<input id="quickActionEnabled" type="checkbox" />
Disable Send / Insert In User Input
Disable Send / Insert In User Input
</label>
<label class="checkbox_label marginBot10">
<input id="placeBeforePromptEnabled" type="checkbox" />
Place Quick-reply before the Prompt
<input id="placeBeforeInputEnabled" type="checkbox" />
Place Quick-reply before the Input
</label>
<label class="checkbox_label marginBot10">
<input id="AutoInputInject" type="checkbox" />
Inject user input automatically<br>(If disabled, use {{input}} macro for manual injection)
</label>
<label for="quickReplyPresets">Quick Reply presets:</label>
<div class="flex-container flexnowrap wide100p">
<select id="quickReplyPresets" name="quickreply-preset">
<select id="quickReplyPresets" name="quickreply-preset" class="flex1 text_pole">
</select>
<i id="quickReplyPresetSaveButton" class="fa-solid fa-save"></i>
<div id="quickReplyPresetSaveButton" class="menu_button menu_button_icon">
<div class="fa-solid fa-save"></div>
<span>Save</span>
</div>
</div>
<label for="quickReplyNumberOfSlots">Number of slots:</label>
</div>
@ -379,10 +399,11 @@ jQuery(async () => {
</div>`;
$('#extensions_settings2').append(settingsHtml);
// Add event handler for quickActionEnabled
$('#quickActionEnabled').on('input', onQuickActionEnabledInput);
$('#placeBeforePromptEnabled').on('input', onPlaceBeforePromptEnabledInput);
$('#placeBeforeInputEnabled').on('input', onPlaceBeforeInputEnabledInput);
$('#AutoInputInject').on('input', onAutoInputInject);
$('#quickReplyEnabled').on('input', onQuickReplyEnabledInput);
$('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput);
$("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset);
@ -392,16 +413,13 @@ jQuery(async () => {
extension_settings.quickReplyPreset = quickReplyPresetSelected;
applyQuickReplyPreset(quickReplyPresetSelected);
saveSettingsDebounced();
});
await loadSettings('init');
addQuickReplyBar();
});
$(document).ready(() => {
jQuery(() => {
registerSlashCommand('qr', doQR, [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true);
registerSlashCommand('qrset', doQRPresetSwitch, [], '<span class="monospace">(name)</span> swaps to the specified Quick Reply Preset', true, true);
})

View File

@ -1,11 +0,0 @@
{
"display_name": "Settings Search",
"loading_order": 15,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "RossAscends",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@ -1,5 +0,0 @@
.highlighted {
color: black;
background-color: yellow;
text-shadow: none !important;
}

View File

@ -121,6 +121,17 @@ const helpString = [
example: '/sd apple tree' would generate a picture of an apple tree.`,
].join('<br>');
const defaultPrefix = 'best quality, absurdres, aesthetic,';
const defaultNegative = 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry';
const defaultStyles = [
{
name: 'Default',
negative: defaultNegative,
prefix: defaultPrefix,
},
];
const defaultSettings = {
source: sources.extras,
@ -143,8 +154,8 @@ const defaultSettings = {
width: 512,
height: 512,
prompt_prefix: 'best quality, absurdres, masterpiece,',
negative_prompt: 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry',
prompt_prefix: defaultPrefix,
negative_prompt: defaultNegative,
sampler: 'DDIM',
model: '',
@ -160,6 +171,7 @@ const defaultSettings = {
// Refine mode
refine_mode: false,
expand: false,
prompts: promptTemplates,
@ -190,6 +202,9 @@ const defaultSettings = {
novel_upscale_ratio_step: 0.1,
novel_upscale_ratio: 1.0,
novel_anlas_guard: false,
style: 'Default',
styles: defaultStyles,
}
function getSdRequestBody() {
@ -238,6 +253,10 @@ async function loadSettings() {
extension_settings.sd.character_prompts = {};
}
if (!Array.isArray(extension_settings.sd.styles)) {
extension_settings.sd.styles = defaultStyles;
}
$('#sd_source').val(extension_settings.sd.source);
$('#sd_scale').val(extension_settings.sd.scale).trigger('input');
$('#sd_steps').val(extension_settings.sd.steps).trigger('input');
@ -257,11 +276,20 @@ async function loadSettings() {
$('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
$('#sd_expand').prop('checked', extension_settings.sd.expand);
$('#sd_auto_url').val(extension_settings.sd.auto_url);
$('#sd_auto_auth').val(extension_settings.sd.auto_auth);
$('#sd_vlad_url').val(extension_settings.sd.vlad_url);
$('#sd_vlad_auth').val(extension_settings.sd.vlad_auth);
for (const style of extension_settings.sd.styles) {
const option = document.createElement('option');
option.value = style.name;
option.text = style.name;
option.selected = style.name === extension_settings.sd.style;
$('#sd_style').append(option);
}
toggleSourceControls();
addPromptTemplates();
@ -300,7 +328,88 @@ function addPromptTemplates() {
}
}
async function refinePrompt(prompt) {
function onStyleSelect() {
const selectedStyle = String($('#sd_style').find(':selected').val());
const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
if (!styleObject) {
console.warn(`Could not find style object for ${selectedStyle}`);
return;
}
$('#sd_prompt_prefix').val(styleObject.prefix).trigger('input');
$('#sd_negative_prompt').val(styleObject.negative).trigger('input');
extension_settings.sd.style = selectedStyle;
saveSettingsDebounced();
}
async function onSaveStyleClick() {
const userInput = await callPopup('Enter style name:', 'input', '', { okButton: 'Save' });
if (!userInput) {
return;
}
const name = String(userInput).trim();
const prefix = String($('#sd_prompt_prefix').val());
const negative = String($('#sd_negative_prompt').val());
const existingStyle = extension_settings.sd.styles.find(x => x.name === name);
if (existingStyle) {
existingStyle.prefix = prefix;
existingStyle.negative = negative;
$('#sd_style').val(name);
saveSettingsDebounced();
return;
}
const styleObject = {
name: name,
prefix: prefix,
negative: negative,
};
extension_settings.sd.styles.push(styleObject);
const option = document.createElement('option');
option.value = styleObject.name;
option.text = styleObject.name;
option.selected = true;
$('#sd_style').append(option);
$('#sd_style').val(styleObject.name);
saveSettingsDebounced();
}
async function expandPrompt(prompt) {
try {
const response = await fetch('/api/sd/expand', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ prompt: prompt }),
});
if (!response.ok) {
throw new Error('API returned an error.');
}
const data = await response.json();
return data.prompt;
} catch {
return prompt;
}
}
/**
* Modifies prompt based on auto-expansion and user inputs.
* @param {string} prompt Prompt to refine
* @param {boolean} allowExpand Whether to allow auto-expansion
* @returns {Promise<string>} Refined prompt
*/
async function refinePrompt(prompt, allowExpand) {
if (allowExpand && extension_settings.sd.expand) {
prompt = await expandPrompt(prompt);
}
if (extension_settings.sd.refine_mode) {
const refinedPrompt = await callPopup('<h3>Review and edit the prompt:</h3>Press "Cancel" to abort the image generation.', 'input', prompt.trim(), { rows: 5, okButton: 'Generate' });
@ -346,7 +455,14 @@ function getCharacterPrefix() {
return '';
}
function combinePrefixes(str1, str2) {
/**
* Combines two prompt prefixes into one.
* @param {string} str1 Base string
* @param {string} str2 Secondary string
* @param {string} macro Macro to replace with the secondary string
* @returns {string} Combined string with a comma between them
*/
function combinePrefixes(str1, str2, macro = '') {
if (!str2) {
return str1;
}
@ -355,12 +471,16 @@ function combinePrefixes(str1, str2) {
str1 = str1.trim().replace(/^,|,$/g, '');
str2 = str2.trim().replace(/^,|,$/g, '');
// Combine the strings with a comma between them
var result = `${str1}, ${str2},`;
// Combine the strings with a comma between them)
const result = macro && str1.includes(macro) ? str1.replace(macro, str2) : `${str1}, ${str2},`;
return result;
}
function onExpandInput() {
extension_settings.sd.expand = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onRefineModeInput() {
extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked');
saveSettingsDebounced();
@ -961,17 +1081,21 @@ async function loadNovelModels() {
}
return [
{
value: 'nai-diffusion-2',
text: 'NAI Diffusion Anime V2',
},
{
value: 'nai-diffusion',
text: 'Full',
text: 'NAI Diffusion Anime V1 (Full)',
},
{
value: 'safe-diffusion',
text: 'Safe',
text: 'NAI Diffusion Anime V1 (Curated)',
},
{
value: 'nai-diffusion-furry',
text: 'Furry',
text: 'NAI Diffusion Furry',
},
];
}
@ -1080,10 +1204,9 @@ async function generatePicture(_, trigger, message, callback) {
extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
}
const callbackOriginal = callback;
callback = async function (prompt, base64Image) {
const imagePath = base64Image;
const imgUrl = `url("${encodeURI(base64Image)}")`;
eventSource.emit(event_types.FORCE_SET_BACKGROUND, imgUrl);
callback = async function (prompt, imagePath) {
const imgUrl = `url("${encodeURI(imagePath)}")`;
eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
if (typeof callbackOriginal === 'function') {
callbackOriginal(prompt, imagePath);
@ -1129,14 +1252,14 @@ async function getPrompt(generationType, message, trigger, quiet_prompt) {
}
if (generationType !== generationMode.FREE) {
prompt = await refinePrompt(prompt);
prompt = await refinePrompt(prompt, true);
}
return prompt;
}
async function generatePrompt(quiet_prompt) {
const reply = await generateQuietPrompt(quiet_prompt, false);
const reply = await generateQuietPrompt(quiet_prompt, false, false);
return processReply(reply);
}
@ -1145,7 +1268,7 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix())
: extension_settings.sd.prompt_prefix;
const prefixedPrompt = combinePrefixes(prefix, prompt);
const prefixedPrompt = combinePrefixes(prefix, prompt, '{prompt}');
let result = { format: '', data: '' };
const currentChatId = getCurrentChatId();
@ -1345,13 +1468,13 @@ function getNovelParams() {
let width = extension_settings.sd.width;
let height = extension_settings.sd.height;
// Don't apply Anlas guard if it's disabled.d
// Don't apply Anlas guard if it's disabled.
if (!extension_settings.sd.novel_anlas_guard) {
return { steps, width, height };
}
const MAX_STEPS = 28;
const MAX_PIXELS = 409600;
const MAX_PIXELS = 1024 * 1024;
if (width * height > MAX_PIXELS) {
const ratio = Math.sqrt(MAX_PIXELS / (width * height));
@ -1523,7 +1646,7 @@ async function sdMessageButton(e) {
try {
setBusyIcon(true);
if (hasSavedImage) {
const prompt = await refinePrompt(message.extra.title);
const prompt = await refinePrompt(message.extra.title, false);
message.extra.title = prompt;
console.log('Regenerating an image, using existing prompt:', prompt);
@ -1610,6 +1733,9 @@ jQuery(async () => {
$('#sd_novel_upscale_ratio').on('input', onNovelUpscaleRatioInput);
$('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput);
$('#sd_novel_view_anlas').on('click', onViewAnlasClick);
$('#sd_expand').on('input', onExpandInput);
$('#sd_style').on('change', onStyleSelect);
$('#sd_save_style').on('click', onSaveStyleClick);
$('#sd_character_prompt_block').hide();
$('.sd_settings .inline-drawer-toggle').on('click', function () {

View File

@ -12,6 +12,10 @@
<input id="sd_refine_mode" type="checkbox" />
Edit prompts before generation
</label>
<label for="sd_expand" class="checkbox_label" title="Automatically extend prompts using text generation model">
<input id="sd_expand" type="checkbox" />
Auto-enhance prompts
</label>
<label for="sd_source">Source</label>
<select id="sd_source">
<option value="extras">Extras API (local / remote)</option>
@ -122,15 +126,25 @@
<label for="sd_novel_upscale_ratio">Upscale by (<span id="sd_novel_upscale_ratio_value"></span>)</label>
<input id="sd_novel_upscale_ratio" type="range" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" />
</div>
<hr>
<h4 title="Preset for prompt prefix and negative prompt">
Style
</h4>
<div class="flex-container">
<select id="sd_style" class="flex1 text_pole"></select>
<div id="sd_save_style" title="Save style" class="menu_button">
<i class="fa-solid fa-save"></i>
</div>
</div>
<label for="sd_prompt_prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3"></textarea>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
<label for="sd_negative_prompt">Negative prompt</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea>
<div id="sd_character_prompt_block">
<label for="sd_character_prompt">Character-specific prompt prefix</label>
<small>Won't be used in groups.</small>
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea>
</div>
<label for="sd_negative_prompt">Negative prompt</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea>
</div>
</div>
<div class="inline-drawer">

View File

@ -1,6 +1,7 @@
import { callPopup, main_api } from "../../../script.js";
import { getContext } from "../../extensions.js";
import { getTokenizerModel } from "../../tokenizers.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { getTokenCount, getTokenizerModel } from "../../tokenizers.js";
async function doTokenCounter() {
const selectedTokenizer = main_api == 'openai'
@ -29,6 +30,20 @@ async function doTokenCounter() {
callPopup(dialog, 'text');
}
function doCount() {
// get all of the messages in the chat
const context = getContext();
const messages = context.chat.filter(x => x.mes && !x.is_system).map(x => x.mes);
//concat all the messages into a single string
const allMessages = messages.join(' ');
console.debug('All messages:', allMessages);
//toastr success with the token count of the chat
toastr.success(`Token count: ${getTokenCount(allMessages)}`);
}
jQuery(() => {
const buttonHtml = `
<div id="token_counter" class="list-group-item flex-container flexGap5">
@ -37,4 +52,5 @@ jQuery(() => {
</div>`;
$('#extensionsMenu').prepend(buttonHtml);
$('#token_counter').on('click', doTokenCounter);
registerSlashCommand('count', doCount, [], ' counts the number of tokens in the current chat', true, false);
});

View File

@ -10,15 +10,12 @@ class ElevenLabsTtsProvider {
voices = []
separator = ' ... ... ... '
get settings() {
return this.settings
}
defaultSettings = {
stability: 0.75,
similarity_boost: 0.75,
apiKey: "",
multilingual: false,
model: 'eleven_monolingual_v1',
voiceMap: {}
}
@ -27,15 +24,17 @@ class ElevenLabsTtsProvider {
<div class="elevenlabs_tts_settings">
<label for="elevenlabs_tts_api_key">API Key</label>
<input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>
<label for="elevenlabs_tts_model">Model</label>
<select id="elevenlabs_tts_model" class="text_pole">
<option value="eleven_monolingual_v1">Monolingual</option>
<option value="eleven_multilingual_v1">Multilingual v1</option>
<option value="eleven_multilingual_v2">Multilingual v2</option>
</select>
<input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
<label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
<input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.05" />
<label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
<input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.05" />
<label class="checkbox_label" for="elevenlabs_tts_multilingual">
<input id="elevenlabs_tts_multilingual" type="checkbox" value="${this.defaultSettings.multilingual}" />
Enable Multilingual
</label>
</div>
`
return html
@ -45,11 +44,10 @@ class ElevenLabsTtsProvider {
// Update dynamically
this.settings.stability = $('#elevenlabs_tts_stability').val()
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val()
this.settings.multilingual = $('#elevenlabs_tts_multilingual').prop('checked')
this.settings.model = $('#elevenlabs_tts_model').find(':selected').val()
saveTtsProviderSettings()
}
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
@ -59,26 +57,39 @@ class ElevenLabsTtsProvider {
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
// Migrate old settings
if (settings['multilingual'] !== undefined) {
settings.model = settings.multilingual ? 'eleven_multilingual_v1' : 'eleven_monolingual_v1';
delete settings['multilingual'];
}
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to TTS Provider: ${key}`
}
}
$('#elevenlabs_tts_stability').val(this.settings.stability)
$('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost)
$('#elevenlabs_tts_api_key').val(this.settings.apiKey)
$('#tts_auto_generation').prop('checked', this.settings.multilingual)
$('#eleven_labs_connect').on('click', () => {this.onConnectClick()})
$('#elevenlabs_tts_settings').on('input',this.onSettingsChange)
$('#elevenlabs_tts_model').val(this.settings.model);
$('#eleven_labs_connect').on('click', () => { this.onConnectClick() })
$('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this))
await this.checkReady()
console.debug("ElevenLabs: Settings loaded")
try {
await this.checkReady()
console.debug("ElevenLabs: Settings loaded")
} catch {
console.debug("ElevenLabs: Settings loaded, but not ready")
}
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
async checkReady() {
await this.fetchTtsVoiceObjects()
}
@ -87,7 +98,7 @@ class ElevenLabsTtsProvider {
async onConnectClick() {
// Update on Apply click
return await this.updateApiKey().catch( (error) => {
return await this.updateApiKey().catch((error) => {
toastr.error(`ElevenLabs: ${error}`)
})
}
@ -102,6 +113,7 @@ class ElevenLabsTtsProvider {
})
this.settings.apiKey = this.settings.apiKey
console.debug(`Saved new API_KEY: ${this.settings.apiKey}`)
$('#tts_status').text('')
this.onSettingsChange()
}
@ -123,7 +135,7 @@ class ElevenLabsTtsProvider {
}
async generateTts(text, voiceId){
async generateTts(text, voiceId) {
const historyId = await this.findTtsGenerationInHistory(text, voiceId)
let response
@ -189,11 +201,8 @@ class ElevenLabsTtsProvider {
}
async fetchTtsGeneration(text, voiceId) {
let model = "eleven_monolingual_v1"
if (this.settings.multilingual == true) {
model = "eleven_multilingual_v1"
}
console.info(`Generating new TTS for voice_id ${voiceId}`)
let model = this.settings.model ?? "eleven_monolingual_v1";
console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`)
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{
@ -203,9 +212,12 @@ class ElevenLabsTtsProvider {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: model,
model_id: model,
text: text,
voice_settings: this.settings
voice_settings: {
stability: Number(this.settings.stability),
similarity_boost: Number(this.settings.similarity_boost),
},
})
}
)

View File

@ -429,7 +429,7 @@ async function processTtsQueue() {
console.debug('New message found, running TTS')
currentTtsJob = ttsJobQueue.shift()
let text = extension_settings.tts.narrate_translated_only ? currentTtsJob?.extra?.display_text : currentTtsJob.mes
let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes
text = extension_settings.tts.narrate_dialogues_only
? text.replace(/\*[^\*]*?(\*|$)/g, '').trim() // remove asterisks content
: text.replaceAll('*', '').trim() // remove just the asterisks
@ -614,8 +614,8 @@ function onTtsProviderChange() {
// Ensure that TTS provider settings are saved to extension settings.
export function saveTtsProviderSettings() {
updateVoiceMap()
extension_settings.tts[ttsProviderName] = ttsProvider.settings
updateVoiceMap()
saveSettingsDebounced()
console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`)
}
@ -695,6 +695,9 @@ function updateVoiceMap() {
voiceMap = tempVoiceMap
console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`)
}
if (!extension_settings.tts[ttsProviderName].voiceMap) {
extension_settings.tts[ttsProviderName].voiceMap = {}
}
Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap)
saveSettingsDebounced()
}
@ -748,10 +751,6 @@ class VoiceMapEntry {
*
*/
export async function initVoiceMap(){
// Clear existing voiceMap state
$('#tts_voicemap_block').empty()
voiceMapEntries = []
// Gate initialization if not enabled or TTS Provider not ready. Prevents error popups.
const enabled = $('#tts_enabled').is(':checked')
if (!enabled){
@ -769,6 +768,10 @@ export async function initVoiceMap(){
setTtsStatus("TTS Provider Loaded", true)
// Clear existing voiceMap state
$('#tts_voicemap_block').empty()
voiceMapEntries = []
// Get characters in current chat
const characters = getCharacters()

View File

@ -5,7 +5,7 @@ import {
saveSettingsDebounced,
setGenerationParamsFromPreset
} from "../script.js";
import { getCfgPrompt } from "./extensions/cfg/util.js";
import { getCfgPrompt } from "./cfg-scale.js";
import { MAX_CONTEXT_DEFAULT } from "./power-user.js";
import { getTextTokens, tokenizers } from "./tokenizers.js";
import {

View File

@ -181,7 +181,7 @@ async function bindUserNameToPersona() {
export function selectCurrentPersona() {
const personaName = power_user.personas[user_avatar];
if (personaName && name1 !== personaName) {
if (personaName) {
const lockedPersona = chat_metadata['persona'];
if (lockedPersona && lockedPersona !== user_avatar && power_user.persona_show_notifications) {
toastr.info(
@ -191,7 +191,10 @@ export function selectCurrentPersona() {
);
}
setUserName(personaName);
if (personaName !== name1) {
console.log(`Auto-updating user name to ${personaName}`);
setUserName(personaName);
}
const descriptor = power_user.persona_descriptions[user_avatar];

View File

@ -41,6 +41,7 @@ export {
fixMarkdown,
power_user,
send_on_enter_options,
getContextSettings,
};
export const MAX_CONTEXT_DEFAULT = 4096;
@ -131,7 +132,6 @@ let power_user = {
custom_css: '',
waifuMode: false,
movingUI: false,
movingUIState: {},
@ -139,7 +139,7 @@ let power_user = {
noShadows: false,
theme: 'Default (Dark) 1.7.1',
gestures: true,
auto_swipe: false,
auto_swipe_minimum_length: 0,
auto_swipe_blacklist: [],
@ -249,6 +249,20 @@ const storage_keys = {
expand_message_actions: 'ExpandMessageActions',
};
const contextControls = [
// Power user context scoped settings
{ id: "context_story_string", property: "story_string", isCheckbox: false, isGlobalSetting: false },
{ id: "context_example_separator", property: "example_separator", isCheckbox: false, isGlobalSetting: false },
{ id: "context_chat_start", property: "chat_start", isCheckbox: false, isGlobalSetting: false },
// Existing power user settings
{ id: "always-force-name2-checkbox", property: "always_force_name2", isCheckbox: true, isGlobalSetting: true },
{ id: "trim_sentences_checkbox", property: "trim_sentences", isCheckbox: true, isGlobalSetting: true },
{ id: "include_newline_checkbox", property: "include_newline", isCheckbox: true, isGlobalSetting: true },
{ id: "custom_stopping_strings", property: "custom_stopping_strings", isCheckbox: false, isGlobalSetting: true },
{ id: "custom_stopping_strings_macro", property: "custom_stopping_strings_macro", isCheckbox: true, isGlobalSetting: true }
];
let browser_has_focus = true;
const debug_functions = [];
@ -399,6 +413,7 @@ function switchMessageActions() {
power_user.expand_message_actions = value === null ? false : value == "true";
$("body").toggleClass("expandMessageActions", power_user.expand_message_actions);
$("#expandMessageActions").prop("checked", power_user.expand_message_actions);
$('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
}
function switchUiMode() {
@ -836,6 +851,18 @@ switchMesIDDisplay();
switchTokenCount();
switchMessageActions();
function getExampleMessagesBehavior() {
if (power_user.strip_examples) {
return 'strip';
}
if (power_user.pin_examples) {
return 'keep';
}
return 'normal';
}
function loadPowerUserSettings(settings, data) {
// Load from settings.json
if (settings.power_user !== undefined) {
@ -864,7 +891,6 @@ function loadPowerUserSettings(settings, data) {
const timestamps = localStorage.getItem(storage_keys.timestamps_enabled);
const mesIDDisplay = localStorage.getItem(storage_keys.mesIDDisplay_enabled);
const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions);
console.log(expandMessageActions)
power_user.fast_ui_mode = fastUi === null ? true : fastUi == "true";
power_user.movingUI = movingUI === null ? false : movingUI == "true";
power_user.noShadows = noShadows === null ? false : noShadows == "true";
@ -873,7 +899,6 @@ function loadPowerUserSettings(settings, data) {
power_user.timestamps_enabled = timestamps === null ? true : timestamps == "true";
power_user.mesIDDisplay_enabled = mesIDDisplay === null ? true : mesIDDisplay == "true";
power_user.expand_message_actions = expandMessageActions === null ? true : expandMessageActions == "true";
console.log(power_user.expand_message_actions)
power_user.avatar_style = Number(localStorage.getItem(storage_keys.avatar_style) ?? avatar_styles.ROUND);
//power_user.chat_display = Number(localStorage.getItem(storage_keys.chat_display) ?? chat_styles.DEFAULT);
power_user.chat_width = Number(localStorage.getItem(storage_keys.chat_width) ?? 50);
@ -902,6 +927,7 @@ function loadPowerUserSettings(settings, data) {
$('#continue_on_send').prop("checked", power_user.continue_on_send);
$('#quick_continue').prop("checked", power_user.quick_continue);
$('#mes_continue').css('display', power_user.quick_continue ? '' : 'none');
$('#gestures-checkbox').prop("checked", power_user.gestures);
$('#auto_swipe').prop("checked", power_user.auto_swipe);
$('#auto_swipe_minimum_length').val(power_user.auto_swipe_minimum_length);
$('#auto_swipe_blacklist').val(power_user.auto_swipe_blacklist.join(", "));
@ -911,6 +937,8 @@ function loadPowerUserSettings(settings, data) {
$('#fuzzy_search_checkbox').prop("checked", power_user.fuzzy_search);
$('#persona_show_notifications').prop("checked", power_user.persona_show_notifications);
$('#encode_tags').prop("checked", power_user.encode_tags);
$('#example_messages_behavior').val(getExampleMessagesBehavior());
$(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop("selected", true);
$("#console_log_prompts").prop("checked", power_user.console_log_prompts);
$('#auto_fix_generated_markdown').prop("checked", power_user.auto_fix_generated_markdown);
@ -921,8 +949,6 @@ function loadPowerUserSettings(settings, data) {
$("#confirm_message_delete").prop("checked", power_user.confirm_message_delete !== undefined ? !!power_user.confirm_message_delete : true);
$("#spoiler_free_mode").prop("checked", power_user.spoiler_free_mode);
$("#collapse-newlines-checkbox").prop("checked", power_user.collapse_newlines);
$("#pin-examples-checkbox").prop("checked", power_user.pin_examples);
$("#remove-examples-checkbox").prop("checked", power_user.strip_examples);
$("#always-force-name2-checkbox").prop("checked", power_user.always_force_name2);
$("#trim_sentences_checkbox").prop("checked", power_user.trim_sentences);
$("#include_newline_checkbox").prop("checked", power_user.include_newline);
@ -1070,14 +1096,28 @@ function switchMaxContextSize() {
}
}
function loadContextSettings() {
const controls = [
{ id: "context_story_string", property: "story_string", isCheckbox: false },
{ id: "context_example_separator", property: "example_separator", isCheckbox: false },
{ id: "context_chat_start", property: "chat_start", isCheckbox: false },
];
// Fetch a compiled object of all preset settings
function getContextSettings() {
let compiledSettings = {};
controls.forEach(control => {
contextControls.forEach((control) => {
let value = control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property];
// Force to a boolean if the setting is a checkbox
if (control.isCheckbox) {
value = !!value;
}
compiledSettings[control.property] = value;
});
return compiledSettings;
}
// TODO: Maybe add a refresh button to reset settings to preset
// TODO: Add "global state" if a preset doesn't set the power_user checkboxes
function loadContextSettings() {
contextControls.forEach(control => {
const $element = $(`#${control.id}`);
if (control.isCheckbox) {
@ -1086,8 +1126,16 @@ function loadContextSettings() {
$element.val(power_user.context[control.property]);
}
// If the setting already exists, no need to duplicate it
// TODO: Maybe check the power_user object for the setting instead of a flag?
$element.on('input', function () {
power_user.context[control.property] = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
const value = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
if (control.isGlobalSetting) {
power_user[control.property] = value;
} else {
power_user.context[control.property] = value;
}
saveSettingsDebounced();
if (!control.isCheckbox) {
resetScrollHeight($element);
@ -1113,15 +1161,24 @@ function loadContextSettings() {
}
power_user.context.preset = name;
controls.forEach(control => {
contextControls.forEach(control => {
if (preset[control.property] !== undefined) {
power_user.context[control.property] = preset[control.property];
if (control.isGlobalSetting) {
power_user[control.property] = preset[control.property];
} else {
power_user.context[control.property] = preset[control.property];
}
const $element = $(`#${control.id}`);
if (control.isCheckbox) {
$element.prop('checked', power_user.context[control.property]).trigger('input');
$element
.prop('checked', control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property])
.trigger('input');
} else {
$element.val(power_user.context[control.property]).trigger('input');
$element
.val(control.isGlobalSetting ? power_user[control.property] : power_user.context[control.property])
.trigger('input');
}
}
});
@ -1164,7 +1221,7 @@ function highlightDefaultContext() {
export function fuzzySearchCharacters(searchValue) {
const fuse = new Fuse(characters, {
keys: [
{ name: 'data.name', weight: 5 },
{ name: 'data.name', weight: 8 },
{ name: 'data.description', weight: 3 },
{ name: 'data.mes_example', weight: 3 },
{ name: 'data.scenario', weight: 2 },
@ -1849,28 +1906,6 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$("#pin-examples-checkbox").change(function () {
if ($(this).prop("checked")) {
$("#remove-examples-checkbox").prop("checked", false).prop("disabled", true);
power_user.strip_examples = false;
} else {
$("#remove-examples-checkbox").prop("disabled", false);
}
power_user.pin_examples = !!$(this).prop("checked");
saveSettingsDebounced();
});
$("#remove-examples-checkbox").change(function () {
if ($(this).prop("checked")) {
$("#pin-examples-checkbox").prop("checked", false).prop("disabled", true);
power_user.pin_examples = false;
} else {
$("#pin-examples-checkbox").prop("disabled", false);
}
power_user.strip_examples = !!$(this).prop("checked");
saveSettingsDebounced();
});
// include newline is the child of trim sentences
// if include newline is checked, trim sentences must be checked
// if trim sentences is unchecked, include newline must be unchecked
@ -1929,6 +1964,31 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#example_messages_behavior').on('change', function () {
const selectedOption = String($(this).find(':selected').val());
console.log('Setting example messages behavior to', selectedOption);
switch (selectedOption) {
case 'normal':
power_user.pin_examples = false;
power_user.strip_examples = false;
break;
case 'keep':
power_user.pin_examples = true;
power_user.strip_examples = false;
break;
case 'strip':
power_user.pin_examples = false;
power_user.strip_examples = true;
break;
}
console.debug('power_user.pin_examples', power_user.pin_examples);
console.debug('power_user.strip_examples', power_user.strip_examples);
saveSettingsDebounced();
});
// Settings that go to local storage
$("#fast_ui_mode").change(function () {
power_user.fast_ui_mode = $(this).prop("checked");
@ -2122,6 +2182,11 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#gestures-checkbox').on('change', function () {
power_user.gestures = !!$('#gestures-checkbox').prop('checked');
saveSettingsDebounced();
});
$('#auto_swipe').on('input', function () {
power_user.auto_swipe = !!$(this).prop('checked');
saveSettingsDebounced();

View File

@ -18,7 +18,7 @@ import {
import { groups, selected_group } from "./group-chats.js";
import { instruct_presets } from "./instruct-mode.js";
import { kai_settings } from "./kai-settings.js";
import { context_presets, power_user } from "./power-user.js";
import { context_presets, getContextSettings, power_user } from "./power-user.js";
import {
textgenerationwebui_preset_names,
textgenerationwebui_presets,
@ -104,6 +104,7 @@ class PresetManager {
async updatePreset() {
const selected = $(this.select).find("option:selected");
console.log(selected)
if (selected.val() == 'gui') {
toastr.info('Cannot update GUI preset');
@ -236,7 +237,7 @@ class PresetManager {
case "textgenerationwebui":
return textgenerationwebui_settings;
case "context":
const context_preset = structuredClone(power_user.context);
const context_preset = getContextSettings();
context_preset['name'] = name || power_user.context.preset;
return context_preset;
case "instruct":

View File

@ -1,29 +1,29 @@
export { MODULE_NAME };
const MODULE_NAME = 'settingsSearch';
async function addSettingsSearchHTML() {
const html = `
<div class="wide100p">
<div class="justifyLeft">
<textarea id="settingsSearch" class="wide100p textarea_compact" rows="1" placeholder="Search Settings"></textarea>
</div>
</div>`
$("#user-settings-block").prepend(html);
}
/**
* Search for settings that match the search string and highlight them.
*/
async function searchSettings() {
removeHighlighting(); // Remove previous highlights
let searchString = $("#settingsSearch").val();
let searchableText = $("#user-settings-block-content"); // Get the HTML block
const searchString = String($("#settingsSearch").val());
const searchableText = $("#user-settings-block-content"); // Get the HTML block
if (searchString.trim() !== "") {
highlightMatchingElements(searchableText[0], searchString); // Highlight matching elements
}
}
/**
* Check if the element is a child of a header element
* @param {HTMLElement | Text | Document | Comment} element Settings block HTML element
* @returns {boolean} True if the element is a child of a header element, false otherwise
*/
function isParentHeader(element) {
return $(element).closest('h4, h3').length > 0;
}
/**
* Recursively highlight elements that match the search string
* @param {HTMLElement | Text | Document | Comment} element Settings block HTML element
* @param {string} searchString Search string
*/
function highlightMatchingElements(element, searchString) {
$(element).contents().each(function () {
const isTextNode = this.nodeType === Node.TEXT_NODE;
@ -41,17 +41,14 @@ function highlightMatchingElements(element, searchString) {
}
});
}
/**
* Remove highlighting from previously highlighted elements.
*/
function removeHighlighting() {
$(".highlighted").removeClass("highlighted"); // Remove CSS class from previously highlighted elements
}
jQuery(() => {
//addSettingsSearchHTML();
$('#settingsSearch').on('input change', searchSettings);
});

View File

@ -194,10 +194,10 @@ async function getStats() {
/**
* Asynchronously recreates the stats file from chat files.
*
*
* Sends a POST request to the "/recreatestats" endpoint. If the request fails,
* it displays an error notification and throws an error.
*
*
* @throws {Error} If the request to recreate stats is unsuccessful.
*/
async function recreateStats() {
@ -330,23 +330,12 @@ async function statMesProcess(line, type, characters, this_chid, oldMesssage) {
updateStats();
}
jQuery(() => {
function init() {
$(".rm_stats_button").on('click', function () {
characterStatsHandler(characters, this_chid);
});
// Wait for debug functions to load, then add the refresh stats function
registerDebugFunction('refreshStats', 'Refresh Stat File', 'Recreates the stats file based on existing chat files', recreateStats);
}
// Check every 100ms if registerDebugFunction is defined (this is bad lmao)
const interval = setInterval(() => {
if (typeof registerDebugFunction !== 'undefined') {
clearInterval(interval); // Clear the interval once the function is found
init(); // Initialize your code
}
}, 100);
});
export function initStats() {
$(".rm_stats_button").on('click', function () {
characterStatsHandler(characters, this_chid);
});
// Wait for debug functions to load, then add the refresh stats function
registerDebugFunction('refreshStats', 'Refresh Stat File', 'Recreates the stats file based on existing chat files', recreateStats);
}
export { userStatsHandler, characterStatsHandler, getStats, statMesProcess, charStats };

View File

@ -3,6 +3,7 @@ System-wide Replacement Macros:
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> - your current Persona username</li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> - the Character's name</li>
<li><tt>&lcub;&lcub;input&rcub;&rcub;</tt> - the user input</li>
<li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> - you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li>
<li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> - the current time</li>
<li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> - the current date</li>
<li><tt>&lcub;&lcub;weekday&rcub;&rcub;</tt> - the current weekday</li>

View File

@ -182,6 +182,7 @@ export function getTokenizerModel() {
return oai_settings.openai_model;
}
const turbo0301Tokenizer = 'gpt-3.5-turbo-0301';
const turboTokenizer = 'gpt-3.5-turbo';
const gpt4Tokenizer = 'gpt-4';
const gpt2Tokenizer = 'gpt2';
@ -197,6 +198,9 @@ export function getTokenizerModel() {
if (oai_settings.windowai_model.includes('gpt-4')) {
return gpt4Tokenizer;
}
else if (oai_settings.windowai_model.includes('gpt-3.5-turbo-0301')) {
return turbo0301Tokenizer;
}
else if (oai_settings.windowai_model.includes('gpt-3.5-turbo')) {
return turboTokenizer;
}
@ -213,6 +217,9 @@ export function getTokenizerModel() {
if (oai_settings.openrouter_model.includes('gpt-4')) {
return gpt4Tokenizer;
}
else if (oai_settings.openrouter_model.includes('gpt-3.5-turbo-0301')) {
return turbo0301Tokenizer;
}
else if (oai_settings.openrouter_model.includes('gpt-3.5-turbo')) {
return turboTokenizer;
}

View File

@ -498,7 +498,7 @@ export function countOccurrences(string, character) {
let count = 0;
for (let i = 0; i < string.length; i++) {
if (string[i] === character) {
if (string.substring(i, i + character.length) === character) {
count++;
}
}
@ -870,7 +870,7 @@ export async function saveBase64AsFile(base64Data, characterName, filename = "",
// If the response is successful, get the saved image path from the server's response
if (response.ok) {
const responseData = await response.json();
return responseData.path;
return responseData.path.replace(/\\/g, '/'); // Replace backslashes with forward slashes
} else {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to upload the image to the server');

View File

@ -279,6 +279,11 @@ table.responsiveTable {
font-weight: bold;
}
.img_enlarged_container {
padding: 10px;
}
.img_enlarged_container pre code,
.mes_text pre code {
position: relative;
display: block;
@ -1171,6 +1176,14 @@ input[type="file"] {
align-items: center;
}
.bulk_select_checkbox {
align-self: center;
}
#rm_print_characters_block.bulk_select .wide100pLess70px {
width: calc(100% - 85px);
}
#rm_print_characters_block {
overflow-y: auto;
flex-grow: 1;
@ -1506,13 +1519,13 @@ input[type=search]:focus::-webkit-search-cancel-button {
z-index: 3001;
}
#bg_menu_content {
.bg_list {
display: flex;
flex-wrap: wrap;
width: calc(var(--sheldWidth) - 10px);
max-width: 100vw;
max-width: 100svw;
justify-content: center;
justify-content: space-evenly;
}
.bg_example {
@ -1531,6 +1544,26 @@ input[type=search]:focus::-webkit-search-cancel-button {
position: relative;
}
.bg_example.locked {
outline: 2px solid var(--golden);
}
.bg_example:hover.locked .bg_example_lock {
display: none;
}
.bg_example:hover:not(.locked) .bg_example_unlock {
display: none;
}
.bg_example:hover[custom="true"] .bg_example_edit {
display: none;
}
.bg_example:hover[custom="false"] .bg_example_copy {
display: none;
}
.BGSampleTitle {
display: flex;
width: 100%;
@ -1545,13 +1578,17 @@ input[type=search]:focus::-webkit-search-cancel-button {
font-size: calc(var(--fontScale) * 0.9em);
}
.bg_example[custom="true"] .BGSampleTitle {
display: none;
}
.bg_button {
width: 15px;
height: 15px;
position: absolute;
top: 5px;
cursor: pointer;
opacity: 0.7;
opacity: 0.8;
border-radius: 50%;
font-size: 20px;
color: var(--black70a);
@ -1579,6 +1616,16 @@ input[type=search]:focus::-webkit-search-cancel-button {
left: 10px;
}
.bg_example_copy {
left: 10px;
}
.bg_example_lock,
.bg_example_unlock {
left: 50%;
transform: translateX(-50%);
}
.add_bg_but {
cursor: pointer;
opacity: 0.1;
@ -3016,8 +3063,9 @@ a {
.img_enlarged {
max-width: 100%;
max-height: 100%;
padding: 10px 0;
border-radius: 2px;
border: 1px solid transparent;
outline: 1px solid var(--SmartThemeBorderColor);
}
.cropper-container {
@ -3356,19 +3404,6 @@ a {
flex-direction: column;
}
.openai_preset_buttons,
.preset_buttons {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 5px;
}
.openai_preset_buttons select,
.preset_buttons select {
flex-grow: 1;
}
#ReverseProxyWarningMessage {
display: none;
}

119
server.js
View File

@ -726,7 +726,7 @@ app.post("/getchat", jsonParser, function (request, response) {
const lines = data.split('\n');
// Iterate through the array of strings and parse each line as JSON
const jsonData = lines.map(tryParse).filter(x => x);
const jsonData = lines.map((l) => { try { return JSON.parse(l); } catch (_) { } }).filter(x => x);
return response.send(jsonData);
} catch (error) {
console.error(error);
@ -1535,8 +1535,8 @@ app.post("/delchat", jsonParser, function (request, response) {
app.post('/renamebackground', jsonParser, function (request, response) {
if (!request.body) return response.sendStatus(400);
const oldFileName = path.join('public/backgrounds/', sanitize(request.body.old_bg));
const newFileName = path.join('public/backgrounds/', sanitize(request.body.new_bg));
const oldFileName = path.join(DIRECTORIES.backgrounds, sanitize(request.body.old_bg));
const newFileName = path.join(DIRECTORIES.backgrounds, sanitize(request.body.new_bg));
if (!fs.existsSync(oldFileName)) {
console.log('BG file not found');
@ -1841,37 +1841,27 @@ function getImages(path) {
.sort(Intl.Collator().compare);
}
app.post("/getallchatsofcharacter", jsonParser, function (request, response) {
app.post("/getallchatsofcharacter", jsonParser, async function (request, response) {
if (!request.body) return response.sendStatus(400);
var char_dir = (request.body.avatar_url).replace('.png', '')
fs.readdir(chatsPath + char_dir, (err, files) => {
if (err) {
console.log('found error in history loading');
console.error(err);
const characterDirectory = (request.body.avatar_url).replace('.png', '');
try {
const chatsDirectory = path.join(chatsPath, characterDirectory);
const files = fs.readdirSync(chatsDirectory);
const jsonFiles = files.filter(file => path.extname(file) === '.jsonl');
if (jsonFiles.length === 0) {
response.send({ error: true });
return;
}
// filter for JSON files
const jsonFiles = files.filter(file => path.extname(file) === '.jsonl');
// sort the files by name
//jsonFiles.sort().reverse();
// print the sorted file names
var chatData = {};
let ii = jsonFiles.length; //this is the number of files belonging to the character
if (ii !== 0) {
//console.log('found '+ii+' chat logs to load');
for (let i = jsonFiles.length - 1; i >= 0; i--) {
const file = jsonFiles[i];
const fileStream = fs.createReadStream(chatsPath + char_dir + '/' + file);
const fullPathAndFile = chatsPath + char_dir + '/' + file
const stats = fs.statSync(fullPathAndFile);
const fileSizeInKB = (stats.size / 1024).toFixed(2) + "kb";
//console.log(fileSizeInKB);
const jsonFilesPromise = jsonFiles.map((file) => {
return new Promise(async (res) => {
const pathToFile = path.join(chatsPath, characterDirectory, file);
const fileStream = fs.createReadStream(pathToFile);
const stats = fs.statSync(pathToFile);
const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
const rl = readline.createInterface({
input: fileStream,
@ -1885,34 +1875,37 @@ app.post("/getallchatsofcharacter", jsonParser, function (request, response) {
lastLine = line;
});
rl.on('close', () => {
ii--;
if (lastLine) {
rl.close();
let jsonData = tryParse(lastLine);
if (jsonData && (jsonData.name !== undefined || jsonData.character_name !== undefined)) {
chatData[i] = {};
chatData[i]['file_name'] = file;
chatData[i]['file_size'] = fileSizeInKB;
chatData[i]['chat_items'] = itemCounter - 1;
chatData[i]['mes'] = jsonData['mes'] || '[The chat is empty]';
chatData[i]['last_mes'] = jsonData['send_date'] || Date.now();
if (lastLine) {
const jsonData = tryParse(lastLine);
if (jsonData && (jsonData.name || jsonData.character_name)) {
const chatData = {};
chatData['file_name'] = file;
chatData['file_size'] = fileSizeInKB;
chatData['chat_items'] = itemCounter - 1;
chatData['mes'] = jsonData['mes'] || '[The chat is empty]';
chatData['last_mes'] = jsonData['send_date'] || Date.now();
res(chatData);
} else {
console.log('Found an invalid or corrupted chat file: ' + fullPathAndFile);
console.log('Found an invalid or corrupted chat file:', pathToFile);
res({});
}
}
if (ii === 0) {
//console.log('ii count went to zero, responding with chatData');
response.send(chatData);
}
//console.log('successfully closing getallchatsofcharacter');
rl.close();
});
};
} else {
//console.log('Found No Chats. Exiting Load Routine.');
response.send({ error: true });
};
})
});
});
const chatData = await Promise.all(jsonFilesPromise);
const validFiles = chatData.filter(i => i.file_name);
return response.send(validFiles);
} catch (error) {
console.log(error);
return response.send({ error: true });
}
});
function getPngName(file) {
@ -2812,7 +2805,7 @@ app.post("/openai_bias", jsonParser, async function (request, response) {
}
try {
const tokens = tokenizer.encode(entry.text);
const tokens = getEntryTokens(entry.text);
for (const token of tokens) {
result[token] = entry.value;
@ -2825,6 +2818,28 @@ app.post("/openai_bias", jsonParser, async function (request, response) {
// not needed for cached tokenizers
//tokenizer.free();
return response.send(result);
/**
* Gets tokenids for a given entry
* @param {string} text Entry text
* @returns {Uint32Array} Array of token ids
*/
function getEntryTokens(text) {
// Get raw token ids from JSON array
if (text.trim().startsWith('[') && text.trim().endsWith(']')) {
try {
const json = JSON.parse(text);
if (Array.isArray(json) && json.every(x => typeof x === 'number')) {
return new Uint32Array(json);
}
} catch {
// ignore
}
}
// Otherwise, get token ids from tokenizer
return tokenizer.encode(text);
}
});
function convertChatMLPrompt(messages) {

View File

@ -79,7 +79,7 @@ function registerEndpoints(app, jsonParser) {
return response.status(409).send(`Directory already exists at ${extensionPath}`);
}
await git.clone(url, extensionPath);
await git.clone(url, extensionPath, { '--depth': 1 });
console.log(`Extension has been cloned at ${extensionPath}`);

View File

@ -1,6 +1,43 @@
const fetch = require('node-fetch').default;
const { getBasicAuthHeader, delay } = require('./util');
/**
* Sanitizes a string.
* @param {string} x String to sanitize
* @returns {string} Sanitized string
*/
function safeStr(x) {
x = String(x);
for (let i = 0; i < 16; i++) {
x = x.replace(/ /g, ' ');
}
x = x.trim();
x = x.replace(/^[\s,.]+|[\s,.]+$/g, '');
return x;
}
const splitStrings = [
', extremely',
', intricate,',
];
const dangerousPatterns = '[]【】()|:';
/**
* Removes patterns from a string.
* @param {string} x String to sanitize
* @param {string} pattern Pattern to remove
* @returns {string} Sanitized string
*/
function removePattern(x, pattern) {
for (let i = 0; i < pattern.length; i++) {
let p = pattern[i];
let regex = new RegExp("\\" + p, 'g');
x = x.replace(regex, '');
}
return x;
}
/**
* Registers the endpoints for the Stable Diffusion API extension.
* @param {import("express").Express} app Express app
@ -233,7 +270,8 @@ function registerEndpoints(app, jsonParser) {
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
const text = await result.text();
throw new Error('SD WebUI returned an error.', { cause: text });
}
const data = await result.json();
@ -275,6 +313,40 @@ function registerEndpoints(app, jsonParser) {
return response.sendStatus(500);
}
});
/**
* SD prompt expansion using GPT-2 text generation model.
* Adapted from: https://github.com/lllyasviel/Fooocus/blob/main/modules/expansion.py
*/
app.post('/api/sd/expand', jsonParser, async (request, response) => {
const originalPrompt = request.body.prompt;
if (!originalPrompt) {
console.warn('No prompt provided for SD expansion.');
return response.send({ prompt: '' });
}
console.log('Refine prompt input:', originalPrompt);
const splitString = splitStrings[Math.floor(Math.random() * splitStrings.length)];
let prompt = safeStr(originalPrompt) + splitString;
try {
const task = 'text-generation';
const module = await import('./transformers.mjs');
const pipe = await module.default.getPipeline(task);
const result = await pipe(prompt, { num_beams: 1, max_new_tokens: 256, do_sample: true });
const newText = result[0].generated_text;
const newPrompt = safeStr(removePattern(newText, dangerousPatterns));
console.log('Refine prompt output:', newPrompt);
return response.send({ prompt: newPrompt });
} catch {
console.warn('Failed to load transformers.js pipeline.');
return response.send({ prompt: originalPrompt });
}
});
}
module.exports = {

View File

@ -95,6 +95,10 @@ function getTokenizerModel(requestModel) {
return 'gpt-4';
}
if (requestModel.includes('gpt-3.5-turbo-0301')) {
return 'gpt-3.5-turbo-0301';
}
if (requestModel.includes('gpt-3.5-turbo')) {
return 'gpt-3.5-turbo';
}
@ -296,8 +300,8 @@ function registerEndpoints(app, jsonParser) {
return res.send({ "token_count": num_tokens });
}
const tokensPerName = model.includes('gpt-4') ? 1 : -1;
const tokensPerMessage = model.includes('gpt-4') ? 3 : 4;
const tokensPerName = queryModel.includes('gpt-3.5-turbo-0301') ? -1 : 1;
const tokensPerMessage = queryModel.includes('gpt-3.5-turbo-0301') ? 4 : 3;
const tokensPadding = 3;
const tokenizer = getTiktokenTokenizer(model);
@ -319,7 +323,7 @@ function registerEndpoints(app, jsonParser) {
// NB: Since 2023-10-14, the GPT-3.5 Turbo 0301 model shoves in 7-9 extra tokens to every message.
// More details: https://community.openai.com/t/gpt-3-5-turbo-0301-showing-different-behavior-suddenly/431326/14
if (queryModel.endsWith('-0301')) {
if (queryModel.includes('gpt-3.5-turbo-0301')) {
num_tokens += 9;
}

View File

@ -1,4 +1,4 @@
import { pipeline, env, RawImage } from 'sillytavern-transformers';
import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';
import { getConfigValue } from './util.js';
import path from 'path';
import _ from 'lodash';
@ -28,8 +28,18 @@ const tasks = {
pipeline: null,
configField: 'extras.embeddingModel',
},
'text-generation': {
defaultModel: 'Cohee/fooocus_expansion-onnx',
pipeline: null,
configField: 'extras.promptExpansionModel',
},
}
/**
* Gets a RawImage object from a base64-encoded image.
* @param {string} image Base64-encoded image
* @returns {Promise<RawImage|null>} Object representing the image
*/
async function getRawImage(image) {
try {
const buffer = Buffer.from(image, 'base64');
@ -43,6 +53,11 @@ async function getRawImage(image) {
}
}
/**
* Gets the model to use for a given transformers.js task.
* @param {string} task The task to get the model for
* @returns {string} The model to use for the given task
*/
function getModelForTask(task) {
const defaultModel = tasks[task].defaultModel;
@ -55,6 +70,11 @@ function getModelForTask(task) {
}
}
/**
* Gets the transformers.js pipeline for a given task.
* @param {string} task The task to get the pipeline for
* @returns {Promise<Pipeline>} Pipeline for the task
*/
async function getPipeline(task) {
if (tasks[task].pipeline) {
return tasks[task].pipeline;