mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-03 04:37:40 +01:00
Merge branch 'staging' into ru-l10n
This commit is contained in:
commit
1eca18f287
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,3 +47,4 @@ access.log
|
||||
public/css/user.css
|
||||
/plugins/
|
||||
/data
|
||||
/default/scaffold
|
||||
|
@ -231,6 +231,7 @@
|
||||
"api_url_scale": "",
|
||||
"show_external_models": false,
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"human_sysprompt_message": "Let's get started. Please generate your response based on the information and instructions provided above.",
|
||||
"use_ai21_tokenizer": false,
|
||||
"use_google_tokenizer": false,
|
||||
|
@ -624,6 +624,7 @@
|
||||
"show_external_models": false,
|
||||
"proxy_password": "",
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"use_ai21_tokenizer": false
|
||||
}
|
||||
}
|
||||
|
26
default/scaffold/README.md
Normal file
26
default/scaffold/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Content Scaffolding
|
||||
|
||||
Content files in this folder will be copied for all users (old and new) on the server startup.
|
||||
|
||||
1. You **must** create an `index.json` file in `/default/scaffold` for it to work. The syntax is the same as for default content.
|
||||
2. All file paths should be relative to `/default/scaffold`, the use of subdirectories is allowed.
|
||||
3. Scaffolded files are copied first, so they override any of the default files (presets/settings/etc.) that have the same file name.
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"filename": "themes/Midnight.json",
|
||||
"type": "theme"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/city.png",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "characters/Charlie.png",
|
||||
"type": "character"
|
||||
}
|
||||
]
|
||||
```
|
15
package-lock.json
generated
15
package-lock.json
generated
@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "sillytavern",
|
||||
"version": "1.12.0-preview",
|
||||
"version": "1.12.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sillytavern",
|
||||
"version": "1.12.0-preview",
|
||||
"version": "1.12.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@dqbd/tiktoken": "^1.0.13",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
@ -46,6 +45,7 @@
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sillytavern-transformers": "^2.14.6",
|
||||
"simple-git": "^3.19.1",
|
||||
"tiktoken": "^1.0.15",
|
||||
"vectra": "^0.2.2",
|
||||
"wavefile": "^11.0.0",
|
||||
"write-file-atomic": "^5.0.1",
|
||||
@ -82,10 +82,6 @@
|
||||
"version": "0.1.3",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@dqbd/tiktoken": {
|
||||
"version": "1.0.13",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"dev": true,
|
||||
@ -4403,6 +4399,11 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiktoken": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.15.tgz",
|
||||
"integrity": "sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw=="
|
||||
},
|
||||
"node_modules/timm": {
|
||||
"version": "1.7.1",
|
||||
"license": "MIT"
|
||||
|
@ -2,7 +2,6 @@
|
||||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@dqbd/tiktoken": "^1.0.13",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
@ -36,6 +35,7 @@
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sillytavern-transformers": "^2.14.6",
|
||||
"simple-git": "^3.19.1",
|
||||
"tiktoken": "^1.0.15",
|
||||
"vectra": "^0.2.2",
|
||||
"wavefile": "^11.0.0",
|
||||
"write-file-atomic": "^5.0.1",
|
||||
@ -68,14 +68,15 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/SillyTavern/SillyTavern.git"
|
||||
},
|
||||
"version": "1.12.0-preview",
|
||||
"version": "1.12.0",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start:no-csrf": "node server.js --disableCsrf",
|
||||
"postinstall": "node post-install.js",
|
||||
"lint": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js",
|
||||
"lint:fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix",
|
||||
"plugins:update": "node plugins update"
|
||||
"plugins:update": "node plugins update",
|
||||
"plugins:install": "node plugins install"
|
||||
},
|
||||
"bin": {
|
||||
"sillytavern": "./server.js"
|
||||
|
22
plugins.js
22
plugins.js
@ -15,6 +15,12 @@ if (command === 'update') {
|
||||
updatePlugins();
|
||||
}
|
||||
|
||||
if (command === 'install') {
|
||||
const pluginName = process.argv[3];
|
||||
console.log('Installing a new plugin', color.green(pluginName));
|
||||
installPlugin(pluginName);
|
||||
}
|
||||
|
||||
async function updatePlugins() {
|
||||
const directories = fs.readdirSync(pluginsPath)
|
||||
.filter(file => !file.startsWith('.'))
|
||||
@ -51,3 +57,19 @@ async function updatePlugins() {
|
||||
console.log(color.magenta('All plugins updated!'));
|
||||
|
||||
}
|
||||
|
||||
async function installPlugin(pluginName) {
|
||||
try {
|
||||
const pluginPath = path.join(pluginsPath, path.basename(pluginName, '.git'));
|
||||
|
||||
if (fs.existsSync(pluginPath)) {
|
||||
return console.log(color.yellow(`Directory already exists at ${pluginPath}`));
|
||||
}
|
||||
|
||||
await git().clone(pluginName, pluginPath, { '--depth': 1 });
|
||||
console.log(`Plugin ${color.green(pluginName)} installed to ${color.cyan(pluginPath)}`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(color.red(`Failed to install plugin ${pluginName}`), error);
|
||||
}
|
||||
}
|
||||
|
@ -171,3 +171,78 @@
|
||||
.select2-results__option.select2-results__message::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-selection__choice__display {
|
||||
/* Fix weird alignment on the left side */
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
/* Styling for choice remove icon */
|
||||
span.select2.select2-container .select2-selection__choice__remove {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
}
|
||||
|
||||
span.select2.select2-container .select2-selection__choice__remove:hover {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--white30a);
|
||||
}
|
||||
|
||||
/* Custom class to support styling to show clickable choice options */
|
||||
.select2_choice_clickable+span.select2-container .select2-selection__choice__display {
|
||||
cursor: pointer;
|
||||
}
|
||||
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
}
|
||||
|
||||
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display:hover {
|
||||
background-color: var(--white30a);
|
||||
}
|
||||
|
||||
/* Custom class to support same line multi inputs of select2 controls */
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
|
||||
/* Allow search placeholder to take up all space if needed */
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
/* Fix weird styling choice or huge margin around selected options */
|
||||
margin-block-start: 2px;
|
||||
margin-block-end: 2px;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search__field {
|
||||
/* Min height to reserve spacing */
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
/* Min width to be clickable */
|
||||
min-width: 4em;
|
||||
align-content: center;
|
||||
/* Fix search textarea alignment issue with UL elements */
|
||||
margin-top: 0px;
|
||||
height: unset;
|
||||
/* Prevent height from jumping around when input is focused */
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
/* Min height to reserve spacing */
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
}
|
||||
|
||||
/* Make search bar invisible unless the select2 is active, to save space */
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container.select2-container--focus .select2-selection--multiple .select2-search--inline {
|
||||
height: unset;
|
||||
}
|
||||
|
@ -103,7 +103,8 @@
|
||||
}
|
||||
|
||||
#bulkTagsList,
|
||||
#tagList .tag {
|
||||
#tagList .tag,
|
||||
#groupTagList .tag {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@ -193,7 +194,8 @@
|
||||
filter: brightness(75%) saturate(0.6);
|
||||
}
|
||||
|
||||
.tag_as_folder:hover {
|
||||
.tag_as_folder:hover,
|
||||
.tag_as_folder.flash {
|
||||
filter: brightness(150%) saturate(0.6) !important;
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,12 @@
|
||||
.world_entry_form_control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.world_entry_form_control .keyprimarytextpole,
|
||||
.world_entry_form_control .keysecondarytextpole {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.world_entry_thin_controls {
|
||||
@ -101,7 +107,7 @@
|
||||
height: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
min-height: calc(var(--mainFontSize) + 14px);
|
||||
}
|
||||
|
||||
.delete_entry_button {
|
||||
@ -197,20 +203,57 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#world_info+span.select2-container .select2-selection__choice__remove,
|
||||
#world_info+span.select2-container .select2-selection__choice__display {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
span.select2-container .select2-selection__choice__display:has(> .regex_item),
|
||||
span.select2-container .select2-results__option:has(> .result_block .regex_item) {
|
||||
background-color: #D27D2D30;
|
||||
}
|
||||
|
||||
.regex_item .regex_icon {
|
||||
background-color: var(--black30a);
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 7px;
|
||||
font-weight: bold;
|
||||
font-size: calc(var(--mainFontSize) * 0.75);
|
||||
padding: 0px 3px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
#world_info+span.select2-container .select2-selection__choice__display {
|
||||
/* Fix weird alignment on the left side */
|
||||
margin-left: 1px;
|
||||
.select2-results__option .regex_item .regex_icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
#world_info+span.select2-container .select2-selection__choice__remove:hover,
|
||||
#world_info+span.select2-container .select2-selection__choice__display:hover {
|
||||
background-color: var(--white30a);
|
||||
.select2-results__option .item_count {
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
select.keyselect+span.select2-container .select2-selection--multiple {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.switch_input_type_icon {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
width: fit-content;
|
||||
margin-right: 5px;
|
||||
margin-top: calc(5px + var(--mainFontSize));
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 1px;
|
||||
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 1em;
|
||||
|
||||
opacity: 0.5;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.switch_input_type_icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -116,7 +116,7 @@
|
||||
</h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_novel" class="flex1 text_pole" data-preset-manager-for="novel">
|
||||
<option value="gui" data-i18n="default">Default</option>
|
||||
<option value="gui" data-i18n="Default">Default</option>
|
||||
</select>
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input type="file" hidden data-preset-manager-file="novel" accept=".json, .settings">
|
||||
@ -134,7 +134,7 @@
|
||||
<h4 class="margin0"><span data-i18n="openaipresets">Chat Completion Presets</span></h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_openai" class="flex1 text_pole" data-preset-manager-for="openai">
|
||||
<option value="gui" data-i18n="default">Default</option>
|
||||
<option value="gui" data-i18n="Default">Default</option>
|
||||
</select>
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input id="openai_preset_import_file" type="file" accept=".json,.settings" hidden />
|
||||
@ -246,7 +246,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<div class="range-block-title" data-i18n="temperature">
|
||||
<div class="range-block-title" data-i18n="Temperature">
|
||||
Temperature
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
@ -942,7 +942,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<div class="range-block-title title_restorable">
|
||||
<div id="logit_bias_novel" class="range-block-title title_restorable">
|
||||
<span data-i18n="Logit Bias">Logit Bias</span>
|
||||
<div id="novelai_logit_bias_new_entry" class="menu_button menu_button_icon">
|
||||
<i class="fa-xs fa-solid fa-plus"></i>
|
||||
@ -1131,6 +1131,11 @@
|
||||
<div id="samplerResetButton" class="menu_button whitespacenowrap" data-i18n="Neutralize Samplers">Neutralize Samplers</div>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Set all samplers to their neutral/disabled state." data-i18n="[title]Set all samplers to their neutral/disabled state."></div>
|
||||
</small>
|
||||
|
||||
<small class="flex-container alignitemscenter">
|
||||
<div id="samplerSelectButton" class="menu_button whitespacenowrap" data-i18n="Sampler Select">Sampler Select</div>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Customize displayed samplers or add custom samplers." data-i18n="[title]Customize displayed samplers or add custom samplers."></div>
|
||||
</small>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="mancer, vllm, aphrodite" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small>
|
||||
@ -1267,14 +1272,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" name="dynaTempBlock" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter" data-i18n="DynaTemp">
|
||||
<div class="flex-container alignitemscenter" style="justify-content: center;">
|
||||
<div data-newbie-hidden data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" id="dynatemp_block_ooba" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter">
|
||||
<div class="flex-container alignitemscenter justifyCenter">
|
||||
<div class="checkbox_label" for="dynatemp_textgenerationwebui">
|
||||
<input type="checkbox" id="dynatemp_textgenerationwebui" />
|
||||
<small data-i18n="dynatemp"></small>
|
||||
</div>
|
||||
<span style="text-align: center;" data-i18n="Dynamic Temperature">Dynamic Temperature</span>
|
||||
<span class="textAlignCenter" data-i18n="Dynamic Temperature">Dynamic Temperature</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Scale Temperature dynamically per token, based on the variation of probabilities" title="Scale Temperature dynamically per token, based on the variation of probabilities."></div>
|
||||
</div>
|
||||
</h4>
|
||||
@ -1296,7 +1300,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden name="miroStatBlock" class="wide100p">
|
||||
<div data-newbie-hidden id="mirostat_block_ooba" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter">
|
||||
<label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>
|
||||
<div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat is a thermostat for output perplexity" title="Mirostat is a thermostat for output perplexity. Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap (where, as the autoregressive inference produces text, the perplexity of the output tends toward zero) and the confusion trap (where the perplexity diverges). For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020). Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>
|
||||
@ -1325,7 +1329,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="ooba" name="beamSearchBlock" class="wide100p">
|
||||
<div data-newbie-hidden data-tg-type="ooba, vllm" name="beamSearchBlock" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter">
|
||||
<label>
|
||||
<span data-i18n="Beam search">Beam Search</span>
|
||||
@ -1408,11 +1412,11 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div data-tg-type="mancer, ooba, koboldcpp, aphrodite, llamaccp, ollama" data-newbie-hidden class="flex-container flexFlowColumn alignitemscenter flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-tg-type="mancer, ooba, koboldcpp, vllm, aphrodite, llamacpp, ollama" data-newbie-hidden class="flex-container flexFlowColumn alignitemscenter flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Seed" class="textAlignCenter">Seed</small>
|
||||
<input type="number" id="seed_textgenerationwebui" class="text_pole textAlignCenter" min="-1" value="-1" maxlength="100" />
|
||||
</div>
|
||||
<div data-newbie-hidden class="wide100p">
|
||||
<div id="banned_tokens_block_ooba" data-newbie-hidden class="wide100p">
|
||||
<hr data-newbie-hidden class="width100p">
|
||||
<h4 class="range-block-title justifyCenter">
|
||||
<span data-i18n="Banned Tokens">Banned Tokens</span>
|
||||
@ -1423,7 +1427,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block wide100p">
|
||||
<div class="range-block-title title_restorable">
|
||||
<div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable">
|
||||
<span data-i18n="Logit Bias">Logit Bias</span>
|
||||
<div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
|
||||
<i class="fa-xs fa-solid fa-plus"></i>
|
||||
@ -1437,7 +1441,7 @@
|
||||
<div class="logit_bias_list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="ooba, tabby" class="wide100p">
|
||||
<div id="cfg_block_ooba" data-newbie-hidden data-tg-type="ooba, tabby" class="wide100p">
|
||||
<hr class="width100p">
|
||||
<h4 data-i18n="CFG" class="textAlignCenter">CFG
|
||||
<div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]Classifier Free Guidance. More helpful tip coming soon" title="Classifier Free Guidance. More helpful tip coming soon."></div>
|
||||
@ -1459,7 +1463,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby" class="wide100p">
|
||||
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby, llamacpp" class="wide100p">
|
||||
<hr class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter"><span data-i18n="JSON Schema">JSON Schema</span>
|
||||
<a href="https://json-schema.org/learn/getting-started-step-by-step" target="_blank">
|
||||
@ -1485,7 +1489,7 @@
|
||||
</h4>
|
||||
<textarea id="grammar_string_textgenerationwebui" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">
|
||||
<div id="sampler_order_block" data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">
|
||||
<hr class="wide100p">
|
||||
<div class="range-block-title">
|
||||
<span data-i18n="Samplers Order">Samplers Order</span>
|
||||
@ -1680,7 +1684,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,openrouter,makersuite,claude,custom">
|
||||
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand marginBot10">
|
||||
<input id="openai_image_inlining" type="checkbox" />
|
||||
<span data-i18n="Send inline images">Send inline images</span>
|
||||
<div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
|
||||
@ -1689,6 +1693,16 @@
|
||||
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code> <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter">
|
||||
<label for="openai_inline_image_quality">
|
||||
Inline Image Quality
|
||||
</label>
|
||||
<select id="openai_inline_image_quality">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="ai21">
|
||||
<label for="use_ai21_tokenizer" title="Use AI21 Tokenizer" class="checkbox_label widthFreeExpand">
|
||||
@ -1709,8 +1723,9 @@
|
||||
<div class="range-block" data-source="makersuite">
|
||||
<label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">
|
||||
<input id="use_makersuite_sysprompt" type="checkbox" />
|
||||
<span data-i18n="Use system prompt (Gemini 1.5 pro+ only)">
|
||||
Use system prompt (Gemini 1.5 pro+ only)
|
||||
<span>
|
||||
<span data-i18n="Use system prompt">Use system prompt</span><br>
|
||||
<small data-i18n="(Gemini 1.5 Pro/Flash only)">(Gemini 1.5 Pro/Flash only)</small>
|
||||
</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft marginBot5">
|
||||
@ -1723,6 +1738,8 @@
|
||||
<div class="wide100p">
|
||||
<span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
|
||||
<textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
|
||||
<span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
|
||||
<textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
|
||||
</div>
|
||||
<label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
|
||||
<input id="claude_use_sysprompt" type="checkbox" />
|
||||
@ -1747,7 +1764,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block m-t-1" data-source="openai,openrouter,scale">
|
||||
<div class="range-block-title openai_restorable" data-i18n="Logit Bias">
|
||||
<div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
|
||||
Logit Bias
|
||||
</div>
|
||||
<div class="toggle-description justifyLeft" data-i18n="Helps to ban or reenforce the usage of certain words">
|
||||
@ -2405,6 +2422,10 @@
|
||||
<option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
|
||||
<option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
|
||||
</optgroup>
|
||||
<optgroup label="GPT-4o">
|
||||
<option value="gpt-4o">gpt-4o</option>
|
||||
<option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
|
||||
</optgroup>
|
||||
<optgroup label="GPT-4 Turbo">
|
||||
<option value="gpt-4-turbo">gpt-4-turbo</option>
|
||||
<option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
|
||||
@ -2634,6 +2655,8 @@
|
||||
<h4 data-i18n="Google Model">Google Model</h4>
|
||||
<select id="model_google_select">
|
||||
<optgroup label="Latest">
|
||||
<!-- Doesn't work without "latest". Maybe my key is scuffed? -->
|
||||
<option value="gemini-1.5-flash-latest">Gemini 1.5 Flash</option>
|
||||
<!-- Points to 1.0, no default 1.5 endpoint -->
|
||||
<option value="gemini-pro">Gemini Pro</option>
|
||||
<option value="gemini-pro-vision">Gemini Pro Vision</option>
|
||||
@ -3310,7 +3333,7 @@
|
||||
<span data-i18n="Active World(s) for all chats"><small>Active World(s) for all chats</small></span>
|
||||
</div>
|
||||
<div class="range-block-range">
|
||||
<select id="world_info" multiple>
|
||||
<select id="world_info" class="select2_multi_sameline" multiple>
|
||||
<option value="" data-i18n="-- World Info not found --">-- World Info not found -- </option>
|
||||
</select>
|
||||
</div>
|
||||
@ -3426,8 +3449,8 @@
|
||||
</label>
|
||||
<label title="If the entry key consists of only one word, it would not be matched as part of other words" data-i18n="[title]If the entry key consists of only one word, it would not be matched as part of other words" class="checkbox_label flex1">
|
||||
<input id="world_info_match_whole_words" type="checkbox" />
|
||||
<small data-i18n="Match whole words" class="whitespacenowrap flex1">
|
||||
Match whole words
|
||||
<small data-i18n="Match Whole Words" class="whitespacenowrap flex1">
|
||||
Match Whole Words
|
||||
</small>
|
||||
</label>
|
||||
<label title="Only the entries with the most number of key matches will be selected for Inclusion Group filtering" data-i18n="[title]Only the entries with the most number of key matches will be selected for Inclusion Group filtering" class="checkbox_label flex1">
|
||||
@ -3580,7 +3603,7 @@
|
||||
<div class="flex-container">
|
||||
<span data-i18n="Chat Style:">Chat Style:</span><br>
|
||||
<select id="chat_display" class="widthNatural flex1 margin0">
|
||||
<option value="0" data-i18n="Default">Flat</span>
|
||||
<option value="0" data-i18n="Flat">Flat</span>
|
||||
<option value="1" data-i18n="Bubbles">Bubbles</option>
|
||||
<option value="2" data-i18n="Document">Document</option>
|
||||
</select>
|
||||
@ -3981,8 +4004,8 @@
|
||||
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]The number of chat history messages to load before pagination." title="The number of chat history messages to load before pagination."></div>
|
||||
</small>
|
||||
<input class="neo-range-slider" type="range" id="chat_truncation" name="chat_truncation" min="0" max="1000" step="25">
|
||||
<input class="neo-range-input" type="number" min="0" max="1000" step="25" data-for="chat_truncation" id="chat_truncation_counter">
|
||||
<input class="neo-range-slider" type="range" id="chat_truncation" name="chat_truncation" min="0" max="1000" step="5">
|
||||
<input class="neo-range-input" type="number" min="0" max="1000" step="5" data-for="chat_truncation" id="chat_truncation_counter">
|
||||
<small data-i18n="(0 = All)">(0 = All)</small>
|
||||
</div>
|
||||
|
||||
@ -4065,7 +4088,7 @@
|
||||
<small class="fa-solid fa-circle-question note-link-small"></small>
|
||||
</a>
|
||||
</label>
|
||||
<label class="checkbox_label" for="forbid_external_media" title="Disalow embedded media from other domains in chat messages." data-i18n="[title]Disalow embedded media from other domains in chat messages">
|
||||
<label class="checkbox_label" for="forbid_external_media" title="Disallow embedded media from other domains in chat messages." data-i18n="[title]Disallow embedded media from other domains in chat messages">
|
||||
<input id="forbid_external_media" type="checkbox" />
|
||||
<small data-i18n="Forbid External Media">Forbid External Media</small>
|
||||
</label>
|
||||
@ -4116,6 +4139,12 @@
|
||||
</div>
|
||||
<div name="AutoCompleteToggle">
|
||||
<h4 data-i18n="AutoComplete Settings">AutoComplete Settings</h4>
|
||||
<label data-newbie-hidden class="checkbox_label" for="stscript_autocomplete_autoHide">
|
||||
<input id="stscript_autocomplete_autoHide" type="checkbox" />
|
||||
<small data-i18n="Automatically hide details">
|
||||
Automatically hide details
|
||||
</small>
|
||||
</label>
|
||||
<div class="flex-container">
|
||||
<div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete.">
|
||||
<label for="stscript_matching" data-i18n="Autocomplete Matching"><small>Matching</small></label>
|
||||
@ -4171,14 +4200,14 @@
|
||||
<label class="checkbox_label" title="Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well." data-i18n="[title]Switch to stricter escaping, allowing all dellimiting characters to be escaped with a backslash, and backslashes to be escaped as well.">
|
||||
<input id="stscript_parser_flag_strict_escaping" type="checkbox" />
|
||||
<span data-i18n="STRICT_ESCAPING"><small>STRICT_ESCAPING</small></span>
|
||||
<a href="https://docs.sillytavern.app/" target="_blank" class="notes-link">
|
||||
<a href="https://docs.sillytavern.app/usage/st-script/#strict-escaping" target="_blank" class="notes-link">
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</label>
|
||||
<label class="checkbox_label" title="Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution." data-i18n="[title]Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.">
|
||||
<input id="stscript_parser_flag_replace_getvar" type="checkbox" />
|
||||
<span data-i18n="REPLACE_GETVAR"><small>REPLACE_GETVAR</small></span>
|
||||
<a href="https://docs.sillytavern.app/" target="_blank" class="notes-link">
|
||||
<a href="https://docs.sillytavern.app/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link">
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</label>
|
||||
@ -4473,6 +4502,9 @@
|
||||
<option id="replace_update" data-i18n="Replace / Update">
|
||||
Replace / Update
|
||||
</option>
|
||||
<option id="import_tags" data-i18n="Import Tags">
|
||||
Import Tags
|
||||
</option>
|
||||
<!--<option id="dupe_button">
|
||||
Duplicate
|
||||
</option>
|
||||
@ -5184,7 +5216,11 @@
|
||||
</span>
|
||||
</small>
|
||||
<small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>
|
||||
<textarea class="text_pole keyprimarytextpole" name="key" rows="1" data-i18n="[placeholder]Comma separated (required)" placeholder="Comma separated (required)" maxlength="2000"></textarea>
|
||||
<select class="keyprimaryselect keyselect select2_multi_sameline" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>
|
||||
<textarea class="text_pole keyprimarytextpole mobile" name="key" rows="1" data-i18n="[placeholder]Comma separated list" placeholder="Comma separated list" maxlength="2000" style="display: none;"></textarea>
|
||||
<button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
|
||||
⌨️
|
||||
</button>
|
||||
</div>
|
||||
<div class="world_entry_form_control">
|
||||
<small class="textAlignCenter" data-i18n="Logic">Logic</small>
|
||||
@ -5202,9 +5238,11 @@
|
||||
</span>
|
||||
</small>
|
||||
<small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>
|
||||
<div class="flex-container flexFlowRow alignitemscenter">
|
||||
<textarea class="text_pole keysecondarytextpole" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated (ignored if empty)" placeholder="Comma separated list" maxlength="2000"></textarea>
|
||||
</div>
|
||||
<select class="keysecondaryselect keyselect select2_multi_sameline" name="keysecondary" data-i18n="[placeholder]Keywords or Regexes (ignored if empty)" placeholder="Keywords or Regexes (ignored if empty)" multiple="multiple"></select>
|
||||
<textarea class="text_pole keysecondarytextpole mobile" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated list (ignored if empty)" placeholder="Comma separated list (ignored if empty)" maxlength="2000" style="display: none;"></textarea>
|
||||
<button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
|
||||
⌨️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter">
|
||||
@ -5266,6 +5304,12 @@
|
||||
Prevent further recursion (this entry will not activate others)
|
||||
</span>
|
||||
</label>
|
||||
<label class="checkbox flex-container alignitemscenter flexNoGap">
|
||||
<input type="checkbox" name="delay_until_recursion" />
|
||||
<span data-i18n="Delay until recursion (this entry can only be activated on recursive checking)">
|
||||
Delay until recursion (this entry can only be activated on recursive checking)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
</small>
|
||||
@ -5295,7 +5339,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="range-block-range">
|
||||
<select name="characterFilter" multiple>
|
||||
<select name="characterFilter" class="select2_multi_sameline" multiple>
|
||||
<option value="">
|
||||
<span data-i18n="-- Characters not found --">-- Characters not found --</span>
|
||||
</option>
|
||||
@ -5306,7 +5350,7 @@
|
||||
<div class="flex-container justifySpaceBetween">
|
||||
<small for="group" data-i18n="Inclusion Group">
|
||||
Inclusion Group
|
||||
<a href="https://docs.sillytavern.app/usage/core-concepts/worldinfo/#inclusion-group" class="notes-link" target="_blank" title="Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered. Documentation: World Info - Inclusion Group" data-i18n="[title]Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered. Documentation: World Info - Inclusion Group">
|
||||
<a href="https://docs.sillytavern.app/usage/core-concepts/worldinfo/#inclusion-group" class="notes-link" target="_blank" title="Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered. Supports multiple comma-separated groups. Documentation: World Info - Inclusion Group" data-i18n="[title]Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered. Documentation: World Info - Inclusion Group">
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</small>
|
||||
@ -6316,6 +6360,9 @@
|
||||
<script type="module" src="lib/swiped-events.js"></script>
|
||||
<script type="module" src="lib/eventemitter.js"></script>
|
||||
<script type="module" src="scripts/i18n.js"></script>
|
||||
<script type="module" src="scripts/bulk-edit.js"></script>
|
||||
<script type="module" src="scripts/setting-search.js"></script>
|
||||
<script type="module" src="scripts/server-history.js"></script>
|
||||
<script type="module" src="script.js"></script>
|
||||
<script>
|
||||
// Configure toast library:
|
||||
@ -6338,4 +6385,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
@ -42,6 +42,46 @@ EventEmitter.prototype.on = function (event, listener) {
|
||||
this.events[event].push(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the listener the last to be called when the event is emitted
|
||||
* @param {string} event Event name
|
||||
* @param {function} listener Event listener
|
||||
*/
|
||||
EventEmitter.prototype.makeLast = function (event, listener) {
|
||||
if (typeof this.events[event] !== 'object') {
|
||||
this.events[event] = [];
|
||||
}
|
||||
|
||||
const events = this.events[event];
|
||||
const idx = events.indexOf(listener);
|
||||
|
||||
if (idx > -1) {
|
||||
events.splice(idx, 1);
|
||||
}
|
||||
|
||||
events.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the listener the first to be called when the event is emitted
|
||||
* @param {string} event Event name
|
||||
* @param {function} listener Event listener
|
||||
*/
|
||||
EventEmitter.prototype.makeFirst = function (event, listener) {
|
||||
if (typeof this.events[event] !== 'object') {
|
||||
this.events[event] = [];
|
||||
}
|
||||
|
||||
const events = this.events[event];
|
||||
const idx = events.indexOf(listener);
|
||||
|
||||
if (idx > -1) {
|
||||
events.splice(idx, 1);
|
||||
}
|
||||
|
||||
events.unshift(listener);
|
||||
}
|
||||
|
||||
EventEmitter.prototype.removeListener = function (event, listener) {
|
||||
var idx;
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "الإعدادات المسبقة لـ Kobold",
|
||||
"guikoboldaisettings": "إعدادات واجهة KoboldAI",
|
||||
"novelaipreserts": "الإعدادات المسبقة لـ NovelAI",
|
||||
"default": "افتراضي",
|
||||
"openaipresets": "الإعدادات المسبقة لـ OpenAI",
|
||||
"text gen webio(ooba) presets": "الإعدادات المسبقة لـ WebUI(ooba)",
|
||||
"response legth(tokens)": "طول الاستجابة (بعدد الاحرف او الرموز)",
|
||||
@ -62,7 +61,7 @@
|
||||
"Temperature": "درجة الحرارة",
|
||||
"Frequency Penalty": "عقوبة التكرار",
|
||||
"Presence Penalty": "عقوبة الوجود",
|
||||
"Top-p": "أعلى p",
|
||||
"Top-p": "أعلى p",
|
||||
"Display bot response text chunks as they are generated": "عرض النصوص لجظة بلحظة",
|
||||
"Top A": "أعلى A",
|
||||
"Typical Sampling": "عينة نموذجية",
|
||||
@ -101,7 +100,7 @@
|
||||
"Inserts jailbreak as a last system message.": "يدرج كسر الحظر كرسالة نظام أخيرة.",
|
||||
"This tells the AI to ignore its usual content restrictions.": "هذا يخبر الذكاء الاصطناعي بتجاهل القيود المعتادة على المحتوى.",
|
||||
"NSFW Encouraged": "NSFW مشجع",
|
||||
"Tell the AI that NSFW is allowed.": "قل للذكاء الاصطناعي أنه يُسمح بـ NSFW",
|
||||
"Tell the AI that NSFW is allowed.": "قل للذكاء الاصطناعي أنه يُسمح بـ NSFW",
|
||||
"NSFW Prioritized": "الأولوية للمحتوى غير مناسب للعمل",
|
||||
"NSFW prompt text goes first in the prompt to emphasize its effect.": "النص الغير مناسب للعمل يأتي أولاً في التعليمات لتأكيد تأثيره.",
|
||||
"Streaming": "البث المباشر ل",
|
||||
@ -141,7 +140,7 @@
|
||||
"Influences bot behavior in its responses": "يؤثر على سلوك الروبوت في ردوده.",
|
||||
"Connect": "الاتصال",
|
||||
"Test Message": "رسالة اختبار",
|
||||
"API": "واجهة برمجة التطبيقات (API)",
|
||||
"API": "واجهة برمجة التطبيقات (API)",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "استخدام Horde",
|
||||
"API url": "رابط API",
|
||||
@ -206,7 +205,7 @@
|
||||
"Scale API Key": "مفتاح API لـ Scale",
|
||||
"Alt Method": "طريقة بديلة",
|
||||
"AI21 API Key": "مفتاح API لـ AI21",
|
||||
"AI21 Model": "نموذج AI21",
|
||||
"AI21 Model": "نموذج AI21",
|
||||
"View API Usage Metrics": "عرض مقاييس استخدام واجهة برمجة التطبيقات",
|
||||
"Show External models (provided by API)": "عرض النماذج الخارجية (المقدمة من قبل واجهة برمجة التطبيقات)",
|
||||
"Bot": "روبوت:",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "سرد العالم أولاً",
|
||||
"Recursive Scan": "فحص متكرر",
|
||||
"Case Sensitive": "حساس لحالة الأحرف",
|
||||
"Match whole words": "تطابق الكلمات الكاملة",
|
||||
"Alert On Overflow": "تنبيه عند التجاوز",
|
||||
"World/Lore Editor": "محرر العالم/السرد",
|
||||
"--- None ---": "--- لا شيء ---",
|
||||
@ -915,7 +913,7 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "استخدم المحلل النحوي المناسب لنماذج Google عبر واجهة برمجة التطبيقات الخاصة بهم. معالجة الإشارات الأولية بطيئة، ولكنها تقدم عداد رمز دقيق جدًا.",
|
||||
"Load koboldcpp order": "تحميل أمر koboldcpp",
|
||||
"Use Google Tokenizer": "استخدم محلل النحوي من Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold-Einstellungen von vorher",
|
||||
"guikoboldaisettings": "KoboldAI-Einstellungen für das Menü",
|
||||
"novelaipreserts": "NovelAI-Einstellungen von früher",
|
||||
"default": "Normal",
|
||||
"openaipresets": "OpenAI-Einstellungen von vorher",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)-Einstellungen für Texterstellung",
|
||||
"response legth(tokens)": "Länge der Antwort (Tokens)",
|
||||
@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Globale Lore zuerst",
|
||||
"Recursive Scan": "Rekursive Suche",
|
||||
"Case Sensitive": "Groß-/Kleinschreibung beachten",
|
||||
"Match whole words": "Ganze Wörter abgleichen",
|
||||
"Alert On Overflow": "Warnung bei Überlauf",
|
||||
"World/Lore Editor": "Welt-/Lore-Editor",
|
||||
"--- None ---": "--- Keine ---",
|
||||
@ -917,5 +915,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Preajustes de Kobold",
|
||||
"guikoboldaisettings": "Ajustes de interfaz de KoboldAI",
|
||||
"novelaipreserts": "Preajustes de NovelAI",
|
||||
"default": "Predeterminado",
|
||||
"openaipresets": "Preajustes de OpenAI",
|
||||
"text gen webio(ooba) presets": "Preajustes de Text Gen WebUI(ooba)",
|
||||
"response legth(tokens)": "Longitud de respuesta (tokens)",
|
||||
@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Historia Global Primero",
|
||||
"Recursive Scan": "Escaneo Recursiva",
|
||||
"Case Sensitive": "Sensible a mayúsculas y minúsculas",
|
||||
"Match whole words": "Coincidir palabras completas",
|
||||
"Alert On Overflow": "Alerta en Desbordamiento",
|
||||
"World/Lore Editor": "Editor de Mundo/Historia",
|
||||
"--- None ---": "--- Ninguno ---",
|
||||
@ -891,6 +889,7 @@
|
||||
"Chat API": " API de chat",
|
||||
"and pick a character": "y elige un personaje",
|
||||
"in the chat bar": "en la barra de chat",
|
||||
"You can browse a list of bundled characters in the Download Extensions & Assets menu within": "Puedes explorar una lista de personajes incluidos en el menú de Download Extensions & Assets dentro de ",
|
||||
"Confused or lost?": "¿Confundido o perdido?",
|
||||
"click these icons!": "¡Haz clic en estos iconos!",
|
||||
"SillyTavern Documentation Site": "Sitio de documentación de SillyTavern",
|
||||
@ -914,7 +913,4 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Usa el tokenizador apropiado para los modelos de Google a través de su API. Procesamiento de indicaciones más lento, pero ofrece un recuento de tokens mucho más preciso.",
|
||||
"Load koboldcpp order": "Cargar orden de koboldcpp",
|
||||
"Use Google Tokenizer": "Usar Tokenizador de Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Préréglages de Kobold",
|
||||
"guikoboldaisettings": "Paramètres de l'interface utilisateur de KoboldAI",
|
||||
"novelaipreserts": "Préréglages de NovelAI",
|
||||
"default": "Par défaut",
|
||||
"openaipresets": "Préréglages d'OpenAI",
|
||||
"text gen webio(ooba) presets": "Préréglages de WebUI(ooba)",
|
||||
"response legth(tokens)": "Longueur de la réponse (en tokens)",
|
||||
@ -205,7 +204,7 @@
|
||||
"Scale API Key": "Clé API Scale",
|
||||
"Alt Method": "Méthode alternative",
|
||||
"AI21 API Key": "Clé API AI21",
|
||||
"AI21 Model": "Modèle AI21",
|
||||
"AI21 Model": "Modèle AI21",
|
||||
"View API Usage Metrics": "Afficher les mesures d'utilisation de l'API",
|
||||
"Show External models (provided by API)": "Afficher les modèles externes (fournis par l'API)",
|
||||
"Bot": "Bot",
|
||||
@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Lore global d'abord",
|
||||
"Recursive Scan": "Analyse récursive",
|
||||
"Case Sensitive": "Sensible à la casse",
|
||||
"Match whole words": "Correspondre aux mots entiers",
|
||||
"Alert On Overflow": "Alerte en cas de dépassement",
|
||||
"World/Lore Editor": "Éditeur de monde/lore",
|
||||
"--- None ---": "--- Aucun ---",
|
||||
@ -914,5 +912,5 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Utilisez le tokenizer approprié pour les modèles Google via leur API. Traitement des invitations plus lent, mais offre un décompte de jetons beaucoup plus précis.",
|
||||
"Load koboldcpp order": "Charger l'ordre koboldcpp",
|
||||
"Use Google Tokenizer": "Utiliser le tokenizer Google"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Fyrir stillingar Kobold",
|
||||
"guikoboldaisettings": "Stillingar fyrir KoboldAI viðmót",
|
||||
"novelaipreserts": "Fyrir stillingar NovelAI",
|
||||
"default": "Sjálfgefið",
|
||||
"openaipresets": "Fyrir stillingar OpenAI",
|
||||
"text gen webio(ooba) presets": "Fyrir stillingar WebUI(ooba) textagerðar",
|
||||
"response legth(tokens)": "Lengd svars (í táknum eða stöfum)",
|
||||
@ -62,7 +61,7 @@
|
||||
"Temperature": "Hitastig",
|
||||
"Frequency Penalty": "Tíðnarefning",
|
||||
"Presence Penalty": "Tilkoma refning",
|
||||
"Top-p": "Topp-p",
|
||||
"Top-p": "Topp-p",
|
||||
"Display bot response text chunks as they are generated": "Birta bætir svarborðstextabrot þegar þau eru búnar til",
|
||||
"Top A": "Topp A",
|
||||
"Typical Sampling": "Venjuleg úrtaka",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Fyrst heimsfræði",
|
||||
"Recursive Scan": "Endurkvæm skoðun",
|
||||
"Case Sensitive": "Skilgreiningarfræðilegt",
|
||||
"Match whole words": "Nákvæm samræmi",
|
||||
"Alert On Overflow": "Viðvörun um flæði",
|
||||
"World/Lore Editor": "Heims-/fræðiritari",
|
||||
"--- None ---": "--- Engin ---",
|
||||
@ -915,5 +913,5 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Notaðu rétta tokenizer fyrir Google módel með þeirra API. Hægri umhvörf fyrir hvöttavinnslu, en býður upp á miklu nákvæmari talningu á táknunum.",
|
||||
"Load koboldcpp order": "Hlaðið inn færslu af koboldcpp",
|
||||
"Use Google Tokenizer": "Notaðu Google Tokenizer"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Preimpostazioni Kobold",
|
||||
"guikoboldaisettings": "Impostazioni dell'interfaccia KoboldAI",
|
||||
"novelaipreserts": "Preimpostazioni NovelAI",
|
||||
"default": "Predefinito",
|
||||
"openaipresets": "Preimpostazioni OpenAI",
|
||||
"text gen webio(ooba) presets": "Preimpostazioni WebUI(ooba) per la generazione di testo",
|
||||
"response legth(tokens)": "Lunghezza della risposta (token)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Lore Globale Prima",
|
||||
"Recursive Scan": "Scansione Ricorsiva",
|
||||
"Case Sensitive": "Sensibile alle Maiuscole",
|
||||
"Match whole words": "Corrispondi a parole intere",
|
||||
"Alert On Overflow": "Avviso Su Overflow",
|
||||
"World/Lore Editor": "Editor di Mondo/Lore",
|
||||
"--- None ---": "--- Nessuno ---",
|
||||
@ -917,5 +915,5 @@
|
||||
"Use Google Tokenizer": "Usa il Tokenizer di Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Koboldのプリセット",
|
||||
"guikoboldaisettings": "KoboldAIのGUI設定",
|
||||
"novelaipreserts": "NovelAIのプリセット",
|
||||
"default": "デフォルト",
|
||||
"openaipresets": "OpenAIのプリセット",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)のプリセット",
|
||||
"response legth(tokens)": "応答の長さ(トークン数)",
|
||||
@ -140,7 +139,7 @@
|
||||
"Influences bot behavior in its responses": "返信でボットの動作に影響を与えます",
|
||||
"Connect": "接続",
|
||||
"Test Message": "テストメッセージ",
|
||||
"API": "API",
|
||||
"API": "API",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "ホードを使用",
|
||||
"API url": "API URL",
|
||||
@ -494,7 +493,6 @@
|
||||
"Global Lore First": "グローバルロアを最初に表示",
|
||||
"Recursive Scan": "再帰的スキャン",
|
||||
"Case Sensitive": "大文字と小文字を区別する",
|
||||
"Match whole words": "完全な単語の一致",
|
||||
"Alert On Overflow": "オーバーフロー時に警告",
|
||||
"World/Lore Editor": "ワールド/ロアの編集",
|
||||
"--- None ---": "--- なし ---",
|
||||
@ -914,5 +912,5 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Googleモデル用の適切なトークナイザーを使用します。 API経由で。 処理が遅くなりますが、トークンの数え上げがはるかに正確になります。",
|
||||
"Load koboldcpp order": "koboldcppオーダーを読み込む",
|
||||
"Use Google Tokenizer": "Googleトークナイザーを使用"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "코볼드 사전 설정",
|
||||
"guikoboldaisettings": "KoboldAI 인터페이스 설정",
|
||||
"novelaipreserts": "NovelAI 사전 설정",
|
||||
"default": "기본값",
|
||||
"openaipresets": "OpenAI 사전 설정",
|
||||
"text gen webio(ooba) presets": "텍스트 생성 WebUI(ooba) 사전 설정",
|
||||
"response legth(tokens)": "응답 길이 (토큰)",
|
||||
@ -425,7 +424,7 @@
|
||||
"Start new chat": "새로운 채팅 시작",
|
||||
"View past chats": "과거 채팅 보기",
|
||||
"Delete messages": "메시지 삭제",
|
||||
"Impersonate": "사칭",
|
||||
"Impersonate": "대신 말하기",
|
||||
"Regenerate": "재생성",
|
||||
"PNG": "PNG",
|
||||
"JSON": "JSON",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "글로벌 로어 우선",
|
||||
"Recursive Scan": "재귀 스캔",
|
||||
"Case Sensitive": "대소문자 구분",
|
||||
"Match whole words": "전체 단어 일치",
|
||||
"Alert On Overflow": "오버플로우 알림",
|
||||
"World/Lore Editor": "월드/로어 편집기",
|
||||
"--- None ---": "--- 없음 ---",
|
||||
@ -914,7 +912,30 @@
|
||||
"Learn how to contribute your idle GPU cycles to the Horde": "여유로운 GPU 주기를 호드에 기여하는 방법 배우기",
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Google 모델용 적절한 토크나이저를 사용하여 API를 통해 제공됩니다. 더 느린 프롬프트 처리지만 훨씬 정확한 토큰 계산을 제공합니다.",
|
||||
"Load koboldcpp order": "코볼드 CPP 순서로 로드",
|
||||
"Use Google Tokenizer": "Google 토크나이저 사용"
|
||||
"Use Google Tokenizer": "구글 토크나이저 사용",
|
||||
"Hide Chat Avatars": "채팅 아바타 숨기기",
|
||||
"Hide avatars in chat messages.": "채팅 메시지에서 아바타 숨김.",
|
||||
"Avatar Hover Magnification": "아바타 마우스오버 시 확대",
|
||||
"Enable magnification for zoomed avatar display.": "마우스 오버 시 아바타가 커지도록 설정하세요.",
|
||||
"AutoComplete Settings": "자동 완성 설정",
|
||||
"Autocomplete Matching": "자동 완성 매칭",
|
||||
"Starts with": "시작하는 단어로",
|
||||
"Autocomplete Style": "자동 완성 스타일",
|
||||
"Includes": "포함하는",
|
||||
"Fuzzy": "퍼지 매칭",
|
||||
"Follow Theme": "테마 적용",
|
||||
"Dark": "다크 모드",
|
||||
"Sets the font size of the autocomplete.": "자동 완성 글꼴 크기 설정",
|
||||
"Autocomplete Width": "자동 완성 너비 조절",
|
||||
"Parser Flags": "파서 플래그 설정",
|
||||
"Sets default flags for the STscript parser.": "STscript 파서 기본 플래그 설정",
|
||||
"Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "모든 구분자를 백슬래시로 이스케이핑하고, 백슬래시 자체도 이스케이프할 수 있도록 엄격한 방식으로 전환합니다.",
|
||||
"STscript Settings": "STscript 설정",
|
||||
"Smooth Streaming": "부드러운 스트리밍",
|
||||
"Experimental feature. May not work for all backends.": "실험적인 기능으로, 모든 백엔드에서 작동이 보장되지는 않을 수 있습니다.",
|
||||
"Char List Subheader": "문자 목록 하위 제목",
|
||||
"Account": "계정",
|
||||
"Theme Colors": "테마 색상",
|
||||
"# Messages to Load": "로딩할 메시지 수"
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
[
|
||||
{ "lang": "ar-sa", "display": "عربي (Arabic)" },
|
||||
{ "lang": "zh-cn", "display": "简体中文 (Chinese) (Simplified)" },
|
||||
{ "lang": "zh-tw", "display": "繁體中文 (Chinese) (Taiwan)" },
|
||||
{ "lang": "nl-nl", "display": "Nederlands (Dutch)" },
|
||||
{ "lang": "de-de", "display": "Deutsch (German)" },
|
||||
{ "lang": "fr-fr", "display": "Français (French)" },
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold voorinstellingen",
|
||||
"guikoboldaisettings": "KoboldAI-interface-instellingen",
|
||||
"novelaipreserts": "NovelAI-voorinstellingen",
|
||||
"default": "Standaard",
|
||||
"openaipresets": "OpenAI-voorinstellingen",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)-voorinstellingen voor tekstgeneratie",
|
||||
"response legth(tokens)": "Reactielengte (tokens)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Globale Lore Eerst",
|
||||
"Recursive Scan": "Recursieve Scan",
|
||||
"Case Sensitive": "Hoofdlettergevoelig",
|
||||
"Match whole words": "Hele woorden matchen",
|
||||
"Alert On Overflow": "Waarschuwing bij overloop",
|
||||
"World/Lore Editor": "Wereld/Lore Editor",
|
||||
"--- None ---": "--- Geen ---",
|
||||
@ -917,5 +915,5 @@
|
||||
"Use Google Tokenizer": "Google Tokenizer gebruiken"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Configurações predefinidas do Kobold",
|
||||
"guikoboldaisettings": "Configurações da interface do KoboldAI",
|
||||
"novelaipreserts": "Configurações predefinidas do NovelAI",
|
||||
"default": "Padrão",
|
||||
"openaipresets": "Configurações predefinidas do OpenAI",
|
||||
"text gen webio(ooba) presets": "Configurações predefinidas do WebUI(ooba) para geração de texto",
|
||||
"response legth(tokens)": "Comprimento da resposta (tokens)",
|
||||
@ -493,7 +492,6 @@
|
||||
"Global Lore First": "Lore Global Primeiro",
|
||||
"Recursive Scan": "Verificação Recursiva",
|
||||
"Case Sensitive": "Sensível a Maiúsculas",
|
||||
"Match whole words": "Corresponder palavras inteiras",
|
||||
"Alert On Overflow": "Alerta em Overflow",
|
||||
"World/Lore Editor": "Editor de Mundo/Lore",
|
||||
"--- None ---": "--- Nenhum ---",
|
||||
@ -915,5 +913,5 @@
|
||||
"Use Google Tokenizer": "Usar Tokenizer do Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Пресеты для Kobold",
|
||||
"guikoboldaisettings": "Настройки из интерфейса KoboldAI",
|
||||
"novelaipreserts": "Пресеты для NovelAI",
|
||||
"default": "По умолчанию",
|
||||
"openaipresets": "Пресеты для OpenAI",
|
||||
"text gen webio(ooba) presets": "Пресеты для WebUI(ooba)",
|
||||
"response legth(tokens)": "Ответ (в токенах)",
|
||||
@ -276,7 +275,7 @@
|
||||
"World Info": "Информация о мире",
|
||||
"Scan Depth": "Глубина сканирования",
|
||||
"Case-Sensitive": "С учетом регистра",
|
||||
"Match Whole Words": "Только целые слова",
|
||||
"Match Whole Words": "Только полное совпадение",
|
||||
"Use global setting": "Использовать глобальную настройку",
|
||||
"Yes": "Да",
|
||||
"No": "Нет",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Сначала глобальный лор",
|
||||
"Recursive Scan": "Рекурсивное сканирование",
|
||||
"Case Sensitive": "Учитывать регистр",
|
||||
"Match whole words": "Только полное совпадение",
|
||||
"Alert On Overflow": "Оповещение о переполнении",
|
||||
"World/Lore Editor": "Редактировать мир или лор",
|
||||
"--- None ---": "--- Отсутствует ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Налаштування Kobold",
|
||||
"guikoboldaisettings": "З інтерфейсу KoboldAI",
|
||||
"novelaipreserts": "Налаштування NovelAI",
|
||||
"default": "За замовчуванням",
|
||||
"openaipresets": "Налаштування OpenAI",
|
||||
"text gen webio(ooba) presets": "Налаштування Text Completion",
|
||||
"response legth(tokens)": "Відповідь (токени)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Глобальна інформація першою",
|
||||
"Recursive Scan": "Рекурсивне сканування",
|
||||
"Case Sensitive": "Чутливість до регістру",
|
||||
"Match whole words": "Відповідність цілим словам",
|
||||
"Alert On Overflow": "Сповіщення при переповненні",
|
||||
"World/Lore Editor": "Редактор світу/книги",
|
||||
"--- None ---": "--- Нічого ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Cài đặt trước Kobold",
|
||||
"guikoboldaisettings": "Cài đặt giao diện KoboldAI",
|
||||
"novelaipreserts": "Cài đặt trước NovelAI",
|
||||
"default": "Mặc định",
|
||||
"openaipresets": "Cài đặt trước OpenAI",
|
||||
"text gen webio(ooba) presets": "Cài đặt trước WebUI(ooba) của máy tạo văn bản",
|
||||
"response legth(tokens)": "Độ dài phản hồi (trong các token)",
|
||||
@ -62,7 +61,7 @@
|
||||
"Temperature": "Nhiệt độ",
|
||||
"Frequency Penalty": "Phạt Tần số",
|
||||
"Presence Penalty": "Phạt Sự hiện",
|
||||
"Top-p": "Top-p",
|
||||
"Top-p": "Top-p",
|
||||
"Display bot response text chunks as they are generated": "Hiển thị các phần văn bản phản hồi của bot khi chúng được tạo ra",
|
||||
"Top A": "Top A",
|
||||
"Typical Sampling": "Mẫu Đại diện",
|
||||
@ -141,7 +140,7 @@
|
||||
"Influences bot behavior in its responses": "Ảnh hưởng đến hành vi của bot trong các phản hồi của nó",
|
||||
"Connect": "Kết nối",
|
||||
"Test Message": "Tin nhắn kiểm tra",
|
||||
"API": "Giao diện lập trình ứng dụng (API)",
|
||||
"API": "Giao diện lập trình ứng dụng (API)",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "Sử dụng Horde",
|
||||
"API url": "URL API",
|
||||
@ -206,7 +205,7 @@
|
||||
"Scale API Key": "Khóa API của Scale",
|
||||
"Alt Method": "Phương pháp thay thế",
|
||||
"AI21 API Key": "Khóa API của AI21",
|
||||
"AI21 Model": "Mô hình AI21",
|
||||
"AI21 Model": "Mô hình AI21",
|
||||
"View API Usage Metrics": "Xem số liệu sử dụng API",
|
||||
"Show External models (provided by API)": "Hiển thị các mô hình bên ngoài (do API cung cấp)",
|
||||
"Bot": "Bot:",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Sử liệu toàn cầu đầu tiên",
|
||||
"Recursive Scan": "Quét đệ quy",
|
||||
"Case Sensitive": "Phân biệt chữ hoa chữ thường",
|
||||
"Match whole words": "Khớp toàn bộ từ",
|
||||
"Alert On Overflow": "Cảnh báo khi tràn",
|
||||
"World/Lore Editor": "Trình soạn thảo Thế giới/Sử liệu",
|
||||
"--- None ---": "--- Không ---",
|
||||
@ -915,5 +913,5 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Sử dụng bộ mã hóa phù hợp cho các mô hình của Google thông qua API của họ. Xử lý lời mời chậm hơn, nhưng cung cấp đếm token chính xác hơn nhiều.",
|
||||
"Load koboldcpp order": "Tải đơn hàng koboldcpp",
|
||||
"Use Google Tokenizer": "Sử dụng bộ mã hóa của Google"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold 预设",
|
||||
"guikoboldaisettings": "KoboldAI 用户界面设置",
|
||||
"novelaipreserts": "NovelAI 预设",
|
||||
"default": "默认",
|
||||
"openaipresets": "对话补全预设",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba) 预设",
|
||||
"response legth(tokens)": "响应长度(以词符数计)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "全局世界书优先",
|
||||
"Recursive Scan": "递归扫描",
|
||||
"Case Sensitive": "区分大小写",
|
||||
"Match whole words": "完整匹配单词",
|
||||
"Alert On Overflow": "溢出警报",
|
||||
"World/Lore Editor": "世界书编辑器",
|
||||
"--- None ---": "--- 无 ---",
|
||||
|
1192
public/locales/zh-tw.json
Normal file
1192
public/locales/zh-tw.json
Normal file
File diff suppressed because it is too large
Load Diff
254
public/script.js
254
public/script.js
@ -191,7 +191,7 @@ import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, sh
|
||||
import { registerPromptManagerMigration } from './scripts/PromptManager.js';
|
||||
import { getRegexedString, regex_placement } from './scripts/extensions/regex/engine.js';
|
||||
import { initLogprobs, saveLogprobsForActiveMessage } from './scripts/logprobs.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './scripts/filters.js';
|
||||
import { FILTER_STATES, FILTER_TYPES, FilterHelper, isFilterState } from './scripts/filters.js';
|
||||
import { getCfgPrompt, getGuidanceScale, initCfg } from './scripts/cfg-scale.js';
|
||||
import {
|
||||
force_output_sequence,
|
||||
@ -231,6 +231,7 @@ import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.
|
||||
import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './scripts/slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
|
||||
import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
|
||||
|
||||
//exporting functions and vars for mods
|
||||
export {
|
||||
@ -384,6 +385,7 @@ export const event_types = {
|
||||
MESSAGE_RECEIVED: 'message_received',
|
||||
MESSAGE_EDITED: 'message_edited',
|
||||
MESSAGE_DELETED: 'message_deleted',
|
||||
MESSAGE_UPDATED: 'message_updated',
|
||||
IMPERSONATE_READY: 'impersonate_ready',
|
||||
CHAT_CHANGED: 'chat_id_changed',
|
||||
GENERATION_STARTED: 'generation_started',
|
||||
@ -415,6 +417,7 @@ export const event_types = {
|
||||
GROUP_MEMBER_DRAFTED: 'group_member_drafted',
|
||||
WORLD_INFO_ACTIVATED: 'world_info_activated',
|
||||
TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
|
||||
CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready',
|
||||
CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected',
|
||||
// TODO: Naming convention is inconsistent with other events
|
||||
CHARACTER_DELETED: 'characterDeleted',
|
||||
@ -1386,7 +1389,7 @@ function verifyCharactersSearchSortRule() {
|
||||
* @typedef {object} Entity - Object representing a display entity
|
||||
* @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
|
||||
* @property {string|number} id - The id
|
||||
* @property {string} type - The type of this entity (character, group, tag)
|
||||
* @property {'character'|'group'|'tag'} type - The type of this entity (character, group, tag)
|
||||
* @property {Entity[]} [entities] - An optional list of entities relevant for this item
|
||||
* @property {number} [hidden] - An optional number representing how many hidden entities this entity contains
|
||||
*/
|
||||
@ -1461,7 +1464,11 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
|
||||
const subCount = subEntities.length;
|
||||
subEntities = filterByTagState(entities, { subForEntity: entity });
|
||||
if (doFilter) {
|
||||
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false });
|
||||
// sub entities filter "hacked" because folder filter should not be applied there, so even in "only folders" mode characters show up
|
||||
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false, tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED } });
|
||||
}
|
||||
if (doSort) {
|
||||
sortEntitiesList(subEntities);
|
||||
}
|
||||
entity.entities = subEntities;
|
||||
entity.hidden = subCount - subEntities.length;
|
||||
@ -1470,8 +1477,13 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
|
||||
|
||||
// Second run filters, hiding whatever should be filtered later
|
||||
if (doFilter) {
|
||||
entities = filterByTagState(entities, { globalDisplayFilters: true });
|
||||
entities = entitiesFilter.applyFilters(entities);
|
||||
const beforeFinalEntities = filterByTagState(entities, { globalDisplayFilters: true });
|
||||
entities = entitiesFilter.applyFilters(beforeFinalEntities);
|
||||
|
||||
// Magic for folder filter. If that one is enabled, and no folders are display anymore, we remove that filter to actually show the characters.
|
||||
if (isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED) && entities.filter(x => x.type == 'tag').length == 0) {
|
||||
entities = entitiesFilter.applyFilters(beforeFinalEntities, { tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED } });
|
||||
}
|
||||
}
|
||||
|
||||
if (doSort) {
|
||||
@ -1523,6 +1535,18 @@ function getCharacterSource(chId = this_chid) {
|
||||
return `https://pygmalion.chat/${pygmalionId}`;
|
||||
}
|
||||
|
||||
const githubRepo = characters[chId]?.data?.extensions?.github_repo;
|
||||
|
||||
if (githubRepo) {
|
||||
return `https://github.com/${githubRepo}`;
|
||||
}
|
||||
|
||||
const sourceUrl = characters[chId]?.data?.extensions?.source_url;
|
||||
|
||||
if (sourceUrl) {
|
||||
return sourceUrl;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -2695,7 +2719,7 @@ class StreamingProcessor {
|
||||
let messageId = -1;
|
||||
|
||||
if (this.type == 'impersonate') {
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
else {
|
||||
await saveReply(this.type, text, true);
|
||||
@ -2731,7 +2755,7 @@ class StreamingProcessor {
|
||||
}
|
||||
|
||||
if (isImpersonate) {
|
||||
$('#send_textarea').val(processedText)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
$('#send_textarea').val(processedText)[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
else {
|
||||
let currentTime = new Date();
|
||||
@ -2865,6 +2889,9 @@ class StreamingProcessor {
|
||||
this.onErrorStreaming();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs }, void, void>}
|
||||
*/
|
||||
*nullStreamingGeneration() {
|
||||
throw new Error('Generation function for streaming is not hooked up');
|
||||
}
|
||||
@ -2892,7 +2919,7 @@ class StreamingProcessor {
|
||||
}
|
||||
|
||||
this.result = text;
|
||||
this.swipes = swipes;
|
||||
this.swipes = Array.from(swipes ?? []);
|
||||
if (logprobs) {
|
||||
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
|
||||
}
|
||||
@ -3110,7 +3137,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
|
||||
if (!pingResult) {
|
||||
unblockGeneration(type);
|
||||
toastr.error('Verify that the server is running and accessible.', 'ST Server cannot be reached' );
|
||||
toastr.error('Verify that the server is running and accessible.', 'ST Server cannot be reached');
|
||||
throw new Error('Server unreachable');
|
||||
}
|
||||
|
||||
@ -3173,7 +3200,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) {
|
||||
is_send_press = true;
|
||||
textareaText = String($('#send_textarea').val());
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
} else {
|
||||
textareaText = '';
|
||||
if (chat.length && chat[chat.length - 1]['is_user']) {
|
||||
@ -4108,6 +4135,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
let messageChunk = '';
|
||||
|
||||
if (data.error) {
|
||||
unblockGeneration(type);
|
||||
generatedPromptCache = '';
|
||||
|
||||
if (data?.response) {
|
||||
@ -4135,7 +4163,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
|
||||
if (getMessage.length > 0) {
|
||||
if (isImpersonate) {
|
||||
$('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
$('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
generatedPromptCache = '';
|
||||
await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);
|
||||
}
|
||||
@ -5910,7 +5938,7 @@ export function changeMainAPI() {
|
||||
getStatusHorde();
|
||||
getHordeModels(true);
|
||||
}
|
||||
|
||||
validateDisabledSamplers()
|
||||
setupChatCompletionPromptManager(oai_settings);
|
||||
forceCharacterEditorTokenize();
|
||||
}
|
||||
@ -6064,6 +6092,7 @@ export async function getSettings() {
|
||||
// TextGen
|
||||
loadTextGenSettings(data, settings);
|
||||
|
||||
|
||||
// OpenAI
|
||||
loadOpenAISettings(data, settings.oai_settings ?? settings);
|
||||
|
||||
@ -6109,6 +6138,7 @@ export async function getSettings() {
|
||||
);
|
||||
changeMainAPI();
|
||||
|
||||
|
||||
//Load User's Name and Avatar
|
||||
initUserAvatar(settings.user_avatar);
|
||||
setPersonaDescription();
|
||||
@ -6139,7 +6169,7 @@ export async function getSettings() {
|
||||
firstRun = false;
|
||||
}
|
||||
}
|
||||
|
||||
await validateDisabledSamplers()
|
||||
settingsReady = true;
|
||||
eventSource.emit(event_types.SETTINGS_LOADED);
|
||||
}
|
||||
@ -6203,20 +6233,14 @@ export async function saveSettings(type) {
|
||||
});
|
||||
}
|
||||
|
||||
export function setGenerationParamsFromPreset(preset, isMancerChange = null) {
|
||||
export function setGenerationParamsFromPreset(preset) {
|
||||
const needsUnlock = (preset.max_length ?? max_context) > MAX_CONTEXT_DEFAULT || (preset.genamt ?? amount_gen) > MAX_RESPONSE_DEFAULT;
|
||||
$('#max_context_unlocked').prop('checked', needsUnlock).trigger('change');
|
||||
|
||||
if (preset.genamt !== undefined) {
|
||||
amount_gen = preset.genamt;
|
||||
if (isMancerChange) {
|
||||
$('#amount_gen').attr('max', amount_gen);
|
||||
$('#amount_gen_counter').val($('#amount_gen').val());
|
||||
}
|
||||
else {
|
||||
$('#amount_gen').val(amount_gen);
|
||||
$('#amount_gen_counter').val(amount_gen);
|
||||
}
|
||||
$('#amount_gen').val(amount_gen);
|
||||
$('#amount_gen_counter').val(amount_gen);
|
||||
}
|
||||
|
||||
if (preset.max_length !== undefined) {
|
||||
@ -6274,6 +6298,8 @@ function updateMessage(div) {
|
||||
mes.extra.bias = null;
|
||||
}
|
||||
|
||||
chat_metadata['tainted'] = true;
|
||||
|
||||
return { mesBlock, text, mes, bias };
|
||||
}
|
||||
|
||||
@ -6342,6 +6368,7 @@ async function messageEditDone(div) {
|
||||
|
||||
this_edit_mes_id = undefined;
|
||||
await saveChatConditional();
|
||||
await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -7566,6 +7593,7 @@ window['SillyTavern'].getContext = function () {
|
||||
getCurrentChatId: getCurrentChatId,
|
||||
getRequestHeaders: getRequestHeaders,
|
||||
reloadCurrentChat: reloadCurrentChat,
|
||||
renameChat: renameChat,
|
||||
saveSettingsDebounced: saveSettingsDebounced,
|
||||
onlineStatus: online_status,
|
||||
maxContext: Number(max_context),
|
||||
@ -8288,6 +8316,75 @@ async function doDeleteChat() {
|
||||
$('#dialogue_popup_ok').trigger('click', { fromSlashCommand: true });
|
||||
}
|
||||
|
||||
async function doRenameChat(_, chatName) {
|
||||
if (!chatName) {
|
||||
toastr.warning('Name must be provided as an argument to rename this chat.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChatName = getCurrentChatId();
|
||||
if (!currentChatName) {
|
||||
toastr.warning('No chat selected that can be renamed.');
|
||||
return;
|
||||
}
|
||||
|
||||
await renameChat(currentChatName, chatName);
|
||||
|
||||
toastr.success(`Successfully renamed chat to: ${chatName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the currently selected chat.
|
||||
* @param {string} oldFileName Old name of the chat (no JSONL extension)
|
||||
* @param {string} newName New name for the chat (no JSONL extension)
|
||||
*/
|
||||
export async function renameChat(oldFileName, newName) {
|
||||
const body = {
|
||||
is_group: !!selected_group,
|
||||
avatar_url: characters[this_chid]?.avatar,
|
||||
original_file: `${oldFileName}.jsonl`,
|
||||
renamed_file: `${newName}.jsonl`,
|
||||
};
|
||||
|
||||
try {
|
||||
showLoader();
|
||||
const response = await fetch('/api/chats/rename', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Unsuccessful request.');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error('Server returned an error.');
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
await renameGroupChat(selected_group, oldFileName, newName);
|
||||
}
|
||||
else {
|
||||
if (characters[this_chid].chat == oldFileName) {
|
||||
characters[this_chid].chat = newName;
|
||||
$('#selected_chat_pole').val(characters[this_chid].chat);
|
||||
await createOrEditCharacter();
|
||||
}
|
||||
}
|
||||
|
||||
await reloadCurrentChat();
|
||||
} catch {
|
||||
hideLoader();
|
||||
await delay(500);
|
||||
await callPopup('An error has occurred. Chat was not renamed.', 'text');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /getchatname` slash command
|
||||
*/
|
||||
@ -8474,11 +8571,13 @@ jQuery(async function () {
|
||||
toastr.success('Chat and settings saved.');
|
||||
}
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'dupe',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'dupe',
|
||||
callback: DupeChar,
|
||||
helpString: 'Duplicates the currently selected character.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'api',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'api',
|
||||
callback: connectAPISlash,
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
@ -8501,7 +8600,8 @@ jQuery(async function () {
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'impersonate',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'impersonate',
|
||||
callback: doImpersonate,
|
||||
aliases: ['imp'],
|
||||
unnamedArgumentList: [
|
||||
@ -8523,29 +8623,45 @@ jQuery(async function () {
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'delchat',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'delchat',
|
||||
callback: doDeleteChat,
|
||||
helpString: 'Deletes the current chat.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getchatname',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'renamechat',
|
||||
callback: doRenameChat,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'new chat name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Renames the current chat.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'getchatname',
|
||||
callback: doGetChatName,
|
||||
returns: 'chat file name',
|
||||
helpString: 'Returns the name of the current chat file into the pipe.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'closechat',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'closechat',
|
||||
callback: doCloseChat,
|
||||
helpString: 'Closes the current chat.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'panels',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'panels',
|
||||
callback: doTogglePanels,
|
||||
aliases: ['togglepanels'],
|
||||
helpString: 'Toggle UI panels on/off',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'forcesave',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'forcesave',
|
||||
callback: doForceSave,
|
||||
helpString: 'Forces a save of the current chat and settings',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'instruct',
|
||||
callback: selectInstructCallback,
|
||||
returns: 'current preset',
|
||||
namedArgumentList: [],
|
||||
@ -8568,15 +8684,18 @@ jQuery(async function () {
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct-on',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'instruct-on',
|
||||
callback: enableInstructCallback,
|
||||
helpString: 'Enables instruct mode.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct-off',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'instruct-off',
|
||||
callback: disableInstructCallback,
|
||||
helpString: 'Disables instruct mode',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'context',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'context',
|
||||
callback: selectContextCallback,
|
||||
returns: 'template name',
|
||||
unnamedArgumentList: [
|
||||
@ -8586,7 +8705,8 @@ jQuery(async function () {
|
||||
],
|
||||
helpString: 'Selects context template by name. Gets the current template if no name is provided',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'chat-manager',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'chat-manager',
|
||||
callback: () => $('#option_select_chat').trigger('click'),
|
||||
aliases: ['chat-history', 'manage-chats'],
|
||||
helpString: 'Opens the chat manager for the current character/group.',
|
||||
@ -8966,69 +9086,26 @@ jQuery(async function () {
|
||||
|
||||
$(document).on('click', '.renameChatButton', async function (e) {
|
||||
e.stopPropagation();
|
||||
const old_filenamefull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();
|
||||
const old_filename = old_filenamefull.replace('.jsonl', '');
|
||||
const oldFileNameFull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();
|
||||
const oldFileName = oldFileNameFull.replace('.jsonl', '');
|
||||
|
||||
const popupText = `<h3>Enter the new name for the chat:<h3>
|
||||
<small>!!Using an existing filename will produce an error!!<br>
|
||||
This will break the link between checkpoint chats.<br>
|
||||
No need to add '.jsonl' at the end.<br>
|
||||
</small>`;
|
||||
const newName = await callPopup(popupText, 'input', old_filename);
|
||||
const newName = await callPopup(popupText, 'input', oldFileName);
|
||||
|
||||
if (!newName || newName == old_filename) {
|
||||
if (!newName || newName == oldFileName) {
|
||||
console.log('no new name found, aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
is_group: !!selected_group,
|
||||
avatar_url: characters[this_chid]?.avatar,
|
||||
original_file: `${old_filename}.jsonl`,
|
||||
renamed_file: `${newName}.jsonl`,
|
||||
};
|
||||
await renameChat(oldFileName, newName);
|
||||
|
||||
try {
|
||||
showLoader();
|
||||
const response = await fetch('/api/chats/rename', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Unsuccessful request.');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error('Server returned an error.');
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
await renameGroupChat(selected_group, old_filename, newName);
|
||||
}
|
||||
else {
|
||||
if (characters[this_chid].chat == old_filename) {
|
||||
characters[this_chid].chat = newName;
|
||||
$('#selected_chat_pole').val(characters[this_chid].chat);
|
||||
await createOrEditCharacter();
|
||||
}
|
||||
}
|
||||
|
||||
await reloadCurrentChat();
|
||||
|
||||
await delay(250);
|
||||
$('#option_select_chat').trigger('click');
|
||||
$('#options').hide();
|
||||
} catch {
|
||||
hideLoader();
|
||||
await delay(500);
|
||||
await callPopup('An error has occurred. Chat was not renamed.', 'text');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
await delay(250);
|
||||
$('#option_select_chat').trigger('click');
|
||||
$('#options').hide();
|
||||
});
|
||||
|
||||
$(document).on('click', '.exportChatButton, .exportRawChatButton', async function (e) {
|
||||
@ -10281,6 +10358,9 @@ jQuery(async function () {
|
||||
$('#character_replace_file').off('change').on('change', uploadReplacementCard).trigger('click');
|
||||
}
|
||||
} break;
|
||||
case 'import_tags': {
|
||||
await importTags(characters[this_chid]);
|
||||
} break;
|
||||
/*case 'delete_button':
|
||||
popup_type = "del_ch";
|
||||
callPopup(`
|
||||
@ -10477,4 +10557,6 @@ jQuery(async function () {
|
||||
eventSource.on(event_types.GROUP_CHAT_DELETED, async (name) => {
|
||||
await deleteItemizedPrompts(name);
|
||||
});
|
||||
|
||||
initCustomSelectedSamplers();
|
||||
});
|
||||
|
@ -702,7 +702,7 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
*/
|
||||
function autoFitSendTextArea() {
|
||||
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
|
||||
if (sendTextArea.scrollHeight + 3 == sendTextArea.offsetHeight) {
|
||||
if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {
|
||||
// Needs to be pulled dynamically because it is affected by font size changes
|
||||
const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height');
|
||||
sendTextArea.style.height = sendTextAreaMinHeight;
|
||||
@ -1133,6 +1133,11 @@ export function initRossMods() {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#dialogue_del_mes_cancel').is(':visible')) {
|
||||
$('#dialogue_del_mes_cancel').trigger('click');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('.drawer-content')
|
||||
.not('#WorldInfo')
|
||||
.not('#left-nav-panel')
|
||||
|
@ -25,6 +25,8 @@ export class AutoComplete {
|
||||
/**@type {boolean}*/ isReplaceable = false;
|
||||
/**@type {boolean}*/ isShowingDetails = false;
|
||||
/**@type {boolean}*/ wasForced = false;
|
||||
/**@type {boolean}*/ isForceHidden = false;
|
||||
/**@type {boolean}*/ canBeAutoHidden = false;
|
||||
|
||||
/**@type {string}*/ text;
|
||||
/**@type {AutoCompleteNameResult}*/ parserResult;
|
||||
@ -57,6 +59,10 @@ export class AutoComplete {
|
||||
return power_user.stscript.matching ?? 'fuzzy';
|
||||
}
|
||||
|
||||
get autoHide() {
|
||||
return power_user.stscript.autocomplete.autoHide ?? false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -224,6 +230,16 @@ export class AutoComplete {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
basicAutoHideCheck() {
|
||||
// auto hide only if at least one char has been typed after the name + space
|
||||
return this.textarea.selectionStart > this.parserResult.start
|
||||
+ this.parserResult.name.length
|
||||
+ (this.startQuote ? 1 : 0)
|
||||
+ (this.endQuote ? 1 : 0)
|
||||
+ 1
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the autocomplete.
|
||||
* @param {boolean} isInput Whether triggered by input.
|
||||
@ -244,6 +260,9 @@ export class AutoComplete {
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
// disable force-hide if trigger was forced
|
||||
if (isForced) this.isForceHidden = false;
|
||||
|
||||
// request provider to get name result (potentially "incomplete", i.e. not an actual existing name) for
|
||||
// cursor position
|
||||
this.parserResult = await this.getNameAt(this.text, this.textarea.selectionStart);
|
||||
@ -275,12 +294,16 @@ export class AutoComplete {
|
||||
this.name = this.name.slice(0, this.textarea.selectionStart - (this.parserResult.start) - (this.startQuote ? 1 : 0));
|
||||
this.parserResult.name = this.name;
|
||||
this.isReplaceable = true;
|
||||
this.isForceHidden = false;
|
||||
this.canBeAutoHidden = false;
|
||||
} else {
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
} else {
|
||||
// if not forced and no user input -> just show details
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
|
||||
if (isForced || isInput || isSelect) {
|
||||
@ -292,8 +315,11 @@ export class AutoComplete {
|
||||
this.secondaryParserResult = result;
|
||||
this.name = this.secondaryParserResult.name;
|
||||
this.isReplaceable = isForced || this.secondaryParserResult.isRequired;
|
||||
this.isForceHidden = false;
|
||||
this.canBeAutoHidden = false;
|
||||
} else {
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -314,7 +340,17 @@ export class AutoComplete {
|
||||
// filter the list of options by the partial name according to the matching type
|
||||
.filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)
|
||||
// remove aliases
|
||||
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx)
|
||||
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
|
||||
|
||||
if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
|
||||
// no matching secondary results and forced trigger -> show current command details
|
||||
this.secondaryParserResult = null;
|
||||
this.result = [this.effectiveParserResult.optionList.find(it=>it.name == this.effectiveParserResult.name)];
|
||||
this.name = this.effectiveParserResult.name;
|
||||
this.fuzzyRegex = /(.*)(.*)(.*)/;
|
||||
}
|
||||
|
||||
this.result = this.result
|
||||
// update remaining options
|
||||
.map(option => {
|
||||
// build element
|
||||
@ -336,6 +372,15 @@ export class AutoComplete {
|
||||
;
|
||||
|
||||
|
||||
|
||||
if (this.isForceHidden) {
|
||||
// hidden with escape
|
||||
return this.hide();
|
||||
}
|
||||
if (this.autoHide && this.canBeAutoHidden && !isForced && this.effectiveParserResult == this.parserResult && this.result.length == 1) {
|
||||
// auto hide user setting enabled and somewhere after name part and would usually show command details
|
||||
return this.hide();
|
||||
}
|
||||
if (this.result.length == 0) {
|
||||
if (!isInput) {
|
||||
// no result and no input? hide autocomplete
|
||||
@ -683,6 +728,8 @@ export class AutoComplete {
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.isForceHidden = true;
|
||||
this.wasForced = false;
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@
|
||||
* @property {boolean} group_override - Overrides any existing group assignment for the extension.
|
||||
* @property {number} group_weight - A value used for prioritizing extensions within the same group.
|
||||
* @property {boolean} prevent_recursion - Completely disallows recursive application of the extension.
|
||||
* @property {boolean} delay_until_recursion - Will only be checked during recursion.
|
||||
* @property {number} scan_depth - The maximum depth to search for matches when applying the extension.
|
||||
* @property {boolean} match_whole_words - Specifies if only entire words should be matched during extension application.
|
||||
* @property {boolean} use_group_scoring - Indicates if group weight is considered when selecting extensions.
|
||||
@ -69,6 +70,8 @@
|
||||
* @property {"system" | "user" | "assistant"} depth_prompt.role - The role the character takes on during the prompted interaction (system, user, or assistant).
|
||||
* // Non-standard extensions added by external tools
|
||||
* @property {string} [pygmalion_id] - The unique identifier assigned to the character by the Pygmalion.chat.
|
||||
* @property {string} [github_repo] - The gitHub repository associated with the character.
|
||||
* @property {string} [source_url] - The source URL associated with the character.
|
||||
* @property {{full_path: string}} [chub] - The Chub-specific data associated with the character.
|
||||
*/
|
||||
|
||||
|
@ -109,7 +109,7 @@ function downloadAssetsList(url) {
|
||||
</div>`);
|
||||
}
|
||||
|
||||
for (const i in availableAssets[assetType]) {
|
||||
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
|
||||
const asset = availableAssets[assetType][i];
|
||||
const elemId = `assets_install_${assetType}_${i}`;
|
||||
let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
|
||||
@ -200,6 +200,9 @@ function downloadAssetsList(url) {
|
||||
</div>`);
|
||||
|
||||
if (assetType === 'character') {
|
||||
if (asset.highlight) {
|
||||
assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
|
||||
}
|
||||
assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset['url']}" alt="${displayName}"></div>`);
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
Enter a URL or the ID of a Fandom wiki page to scrape:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://harrypotter.fandom.com/</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>harrypotter</code>
|
||||
|
@ -7,7 +7,7 @@
|
||||
Don't include the page name!
|
||||
</i>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://streetcat.wiki/index.php</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>https://tcrf.net</code>
|
||||
|
@ -435,12 +435,17 @@ jQuery(function () {
|
||||
<select id="caption_multimodal_model" class="flex1 text_pole">
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
|
||||
<option data-type="openai" value="gpt-4o">gpt-4o</option>
|
||||
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
|
||||
@ -449,6 +454,8 @@ jQuery(function () {
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
|
||||
<option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
|
||||
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
|
||||
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
|
||||
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
|
||||
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
|
||||
<option data-type="ollama" value="llava:latest">llava:latest</option>
|
||||
|
@ -1020,12 +1020,12 @@ function parseLlmResponse(emotionResponse, labels) {
|
||||
const parsedEmotion = JSON.parse(emotionResponse);
|
||||
return parsedEmotion?.emotion ?? fallbackExpression;
|
||||
} catch {
|
||||
const fuse = new Fuse([emotionResponse]);
|
||||
for (const label of labels) {
|
||||
const result = fuse.search(label);
|
||||
if (result.length > 0) {
|
||||
return label;
|
||||
}
|
||||
const fuse = new Fuse(labels, { includeScore: true });
|
||||
console.debug('Using fuzzy search in labels:', labels);
|
||||
const result = fuse.search(emotionResponse);
|
||||
if (result.length > 0) {
|
||||
console.debug(`fuzzy search found: ${result[0].item} as closest for the LLM response:`, emotionResponse);
|
||||
return result[0].item;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -804,10 +804,7 @@ function setMemoryContext(value, saveToMessage, index = null) {
|
||||
const context = getContext();
|
||||
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, false, extension_settings.memory.role);
|
||||
$('#memory_contents').val(value);
|
||||
console.log('Summary set to: ' + value);
|
||||
console.debug('Position: ' + extension_settings.memory.position);
|
||||
console.debug('Depth: ' + extension_settings.memory.depth);
|
||||
console.debug('Role: ' + extension_settings.memory.role);
|
||||
console.log('Summary set to: ' + value, 'Position: ' + extension_settings.memory.position, 'Depth: ' + extension_settings.memory.depth, 'Role: ' + extension_settings.memory.role);
|
||||
|
||||
if (saveToMessage && context.chat.length) {
|
||||
const idx = index ?? context.chat.length - 2;
|
||||
|
@ -342,6 +342,16 @@ export class QuickReply {
|
||||
message.addEventListener('scroll', (evt)=>{
|
||||
updateScrollDebounced();
|
||||
});
|
||||
/** @type {any} */
|
||||
const resizeListener = debounce((evt) => {
|
||||
updateSyntax();
|
||||
updateScrollDebounced(evt);
|
||||
if (document.activeElement == message) {
|
||||
message.blur();
|
||||
message.focus();
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', resizeListener);
|
||||
message.style.color = 'transparent';
|
||||
message.style.background = 'transparent';
|
||||
message.style.setProperty('text-shadow', 'none', 'important');
|
||||
@ -514,6 +524,8 @@ export class QuickReply {
|
||||
});
|
||||
|
||||
await popupResult;
|
||||
|
||||
window.removeEventListener('resize', resizeListener);
|
||||
} else {
|
||||
warn('failed to fetch qrEditor template');
|
||||
}
|
||||
|
@ -209,6 +209,10 @@
|
||||
justify-content: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
#qr--qrOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -218,12 +222,17 @@
|
||||
@media screen and (max-width: 750px) {
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
|
||||
flex-direction: column;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
min-height: 50svh;
|
||||
height: 50svh;
|
||||
}
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) {
|
||||
|
@ -229,6 +229,8 @@
|
||||
|
||||
|
||||
#qr--qrOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> #qr--ctxEditor {
|
||||
.qr--ctxItem {
|
||||
display: flex;
|
||||
@ -244,11 +246,16 @@
|
||||
@media screen and (max-width: 750px) {
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
> #qr--main {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
> #qr--main > .qr--labels {
|
||||
flex-direction: column;
|
||||
}
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
min-height: 50svh;
|
||||
height: 50svh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
<span data-i18n="ext_regex_import_script">Import Script</span>
|
||||
</div>
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" />
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" multiple />
|
||||
</div>
|
||||
<hr />
|
||||
<label data-i18n="ext_regex_saved_scripts">Saved Scripts</label>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { substituteParams } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { regexFromString } from '../../utils.js';
|
||||
export {
|
||||
regex_placement,
|
||||
getRegexedString,
|
||||
@ -21,29 +22,6 @@ const regex_placement = {
|
||||
WORLD_INFO: 5,
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a regular expression from a string.
|
||||
* @param {string} input The input string.
|
||||
* @returns {RegExp} The regular expression instance.
|
||||
* @copyright Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
|
||||
*/
|
||||
function regexFromString(input) {
|
||||
try {
|
||||
// Parse input
|
||||
var m = input.match(/(\/?)(.+)\1([a-z]*)/i);
|
||||
|
||||
// Invalid flags
|
||||
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
|
||||
return RegExp(input);
|
||||
}
|
||||
|
||||
// Create the regular expression
|
||||
return new RegExp(m[2], m[3]);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent function to fetch a regexed version of a raw string
|
||||
* @param {string} rawString The raw string to be regexed
|
||||
|
@ -325,7 +325,9 @@ jQuery(async () => {
|
||||
});
|
||||
$('#import_regex_file').on('change', async function () {
|
||||
const inputElement = this instanceof HTMLInputElement && this;
|
||||
await onRegexImportFileChange(inputElement.files[0]);
|
||||
for (const file of inputElement.files) {
|
||||
await onRegexImportFileChange(file);
|
||||
}
|
||||
inputElement.value = '';
|
||||
});
|
||||
$('#import_regex').on('click', function () {
|
||||
|
@ -1948,7 +1948,7 @@ async function generatePicture(args, trigger, message, callback) {
|
||||
}
|
||||
|
||||
if (!isValidState()) {
|
||||
toastr.warning('Extensions API is not connected or doesn\'t provide SD module. Enable Stable Horde to generate images.');
|
||||
toastr.warning('Image generation is not available. Check your settings and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -642,9 +642,9 @@ jQuery(() => {
|
||||
|
||||
loadSettings();
|
||||
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
|
||||
eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
|
||||
eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
|
||||
eventSource.on(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
|
||||
eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
|
||||
eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit);
|
||||
|
||||
|
@ -261,6 +261,7 @@ async function playAudioData(audioJob) {
|
||||
audioElement.addEventListener('ended', completeCurrentAudioJob);
|
||||
audioElement.addEventListener('canplay', () => {
|
||||
console.debug('Starting TTS playback');
|
||||
audioElement.playbackRate = extension_settings.tts.playback_rate;
|
||||
audioElement.play();
|
||||
});
|
||||
}
|
||||
@ -529,6 +530,10 @@ function loadSettings() {
|
||||
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
|
||||
$('#tts_skip_codeblocks').prop('checked', extension_settings.tts.skip_codeblocks);
|
||||
$('#tts_skip_tags').prop('checked', extension_settings.tts.skip_tags);
|
||||
$('#playback_rate').val(extension_settings.tts.playback_rate);
|
||||
$('#playback_rate_counter').val(Number(extension_settings.tts.playback_rate).toFixed(2));
|
||||
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
|
||||
|
||||
$('body').toggleClass('tts', extension_settings.tts.enabled);
|
||||
}
|
||||
|
||||
@ -538,6 +543,7 @@ const defaultSettings = {
|
||||
currentProvider: 'ElevenLabs',
|
||||
auto_generation: true,
|
||||
narrate_user: false,
|
||||
playback_rate: 1,
|
||||
};
|
||||
|
||||
function setTtsStatus(status, success) {
|
||||
@ -649,6 +655,7 @@ async function loadTtsProvider(provider) {
|
||||
function onTtsProviderChange() {
|
||||
const ttsProviderSelection = $('#tts_provider').val();
|
||||
extension_settings.tts.currentProvider = ttsProviderSelection;
|
||||
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
|
||||
loadTtsProvider(ttsProviderSelection);
|
||||
}
|
||||
|
||||
@ -1022,6 +1029,20 @@ $(document).ready(function () {
|
||||
<small>Pass Asterisks to TTS Engine</small>
|
||||
</label>
|
||||
</div>
|
||||
<div id="playback_rate_block" class="range-block">
|
||||
<hr>
|
||||
<div class="range-block-title justifyLeft" data-i18n="Audio Playback Speed">
|
||||
<small>Audio Playback Speed</small>
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="playback_rate" name="volume" min="0" max="3" step="0.05">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<input type="number" min="0" max="3" step="0.05" data-for="playback_rate" id="playback_rate_counter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tts_voicemap_block">
|
||||
</div>
|
||||
<hr>
|
||||
@ -1046,6 +1067,15 @@ $(document).ready(function () {
|
||||
$('#tts_pass_asterisks').on('click', onPassAsterisksClick);
|
||||
$('#tts_auto_generation').on('click', onAutoGenerationClick);
|
||||
$('#tts_narrate_user').on('click', onNarrateUserClick);
|
||||
|
||||
$('#playback_rate').on('input', function () {
|
||||
const value = $(this).val();
|
||||
const formattedValue = Number(value).toFixed(2);
|
||||
extension_settings.tts.playback_rate = value;
|
||||
$('#playback_rate_counter').val(formattedValue);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#tts_voices').on('click', onTtsVoicesClick);
|
||||
for (const provider in ttsProviders) {
|
||||
$('#tts_provider').append($('<option />').val(provider).text(provider));
|
||||
@ -1063,8 +1093,8 @@ $(document).ready(function () {
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
|
||||
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
|
||||
eventSource.on(event_types.MESSAGE_SENT, onMessageEvent);
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageEvent);
|
||||
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent);
|
||||
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'speak',
|
||||
callback: onNarrateText,
|
||||
aliases: ['narrate', 'tts'],
|
||||
|
@ -258,9 +258,8 @@ export class FilterHelper {
|
||||
*/
|
||||
folderFilter(data) {
|
||||
const state = this.filterData[FILTER_TYPES.FOLDER];
|
||||
// Slightly different than the other filters, as a positive folder filter means it doesn't filter anything (folders get "not hidden" at another place),
|
||||
// while a negative state should then filter out all folders.
|
||||
const isFolder = entity => isFilterState(state, FILTER_STATES.SELECTED) ? true : entity.type === 'tag';
|
||||
// Filter directly on folder. Special rules on still displaying characters with active folder filter are implemented in 'getEntitiesList' directly.
|
||||
const isFolder = entity => entity.type === 'tag';
|
||||
|
||||
return this.filterDataByState(data, state, isFolder);
|
||||
}
|
||||
@ -342,15 +341,40 @@ export class FilterHelper {
|
||||
* Applies all filters to the given data.
|
||||
* @param {any[]} data - The data to filter.
|
||||
* @param {object} options - Optional call parameters
|
||||
* @param {boolean|FilterType} [options.clearScoreCache=true] - Whether the score
|
||||
* @param {boolean} [options.clearScoreCache=true] - Whether the score cache should be cleared.
|
||||
* @param {Object.<FilterType, any>} [options.tempOverrides={}] - Temporarily override specific filters for this filter application
|
||||
* @returns {any[]} The filtered data.
|
||||
*/
|
||||
applyFilters(data, { clearScoreCache = true } = {}) {
|
||||
applyFilters(data, { clearScoreCache = true, tempOverrides = {} } = {}) {
|
||||
if (clearScoreCache) this.clearScoreCache();
|
||||
return Object.values(this.filterFunctions)
|
||||
.reduce((data, fn) => fn(data), data);
|
||||
|
||||
// Save original filter states
|
||||
const originalStates = {};
|
||||
for (const key in tempOverrides) {
|
||||
originalStates[key] = this.filterData[key];
|
||||
this.filterData[key] = tempOverrides[key];
|
||||
}
|
||||
|
||||
try {
|
||||
const result = Object.values(this.filterFunctions)
|
||||
.reduce((data, fn) => fn(data), data);
|
||||
|
||||
// Restore original filter states
|
||||
for (const key in originalStates) {
|
||||
this.filterData[key] = originalStates[key];
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Restore original filter states in case of an error
|
||||
for (const key in originalStates) {
|
||||
this.filterData[key] = originalStates[key];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cache scores for a specific filter type
|
||||
* @param {FilterType} type - The type of data being cached
|
||||
|
@ -637,7 +637,7 @@ function isValidImageUrl(url) {
|
||||
if (Object.keys(url).length === 0) {
|
||||
return false;
|
||||
}
|
||||
return isDataURL(url) || (url && url.startsWith('user'));
|
||||
return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user')));
|
||||
}
|
||||
|
||||
function getGroupAvatar(group) {
|
||||
@ -1418,6 +1418,10 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
* @returns {Promise<void>} - A promise that resolves when the processing and upload is complete.
|
||||
*/
|
||||
async function uploadGroupAvatar(event) {
|
||||
if (!(event.target instanceof HTMLInputElement) || !event.target.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
|
@ -5,7 +5,9 @@ const storageKey = 'language';
|
||||
const overrideLanguage = localStorage.getItem(storageKey);
|
||||
const localeFile = String(overrideLanguage || navigator.language || navigator.userLanguage || 'en').toLowerCase();
|
||||
const langs = await fetch('/locales/lang.json').then(response => response.json());
|
||||
const localeData = await getLocaleData(localeFile);
|
||||
// Don't change to let/const! It will break module loading.
|
||||
// eslint-disable-next-line prefer-const
|
||||
var localeData = await getLocaleData(localeFile);
|
||||
|
||||
/**
|
||||
* Fetches the locale data for the given language.
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
power_user,
|
||||
context_presets,
|
||||
} from './power-user.js';
|
||||
import { resetScrollHeight } from './utils.js';
|
||||
import { regexFromString, resetScrollHeight } from './utils.js';
|
||||
|
||||
/**
|
||||
* @type {any[]} Instruct mode presets.
|
||||
@ -189,10 +189,10 @@ export function autoSelectInstructPreset(modelId) {
|
||||
// If activation regex is set, check if it matches the model id
|
||||
if (preset.activation_regex) {
|
||||
try {
|
||||
const regex = new RegExp(preset.activation_regex, 'i');
|
||||
const regex = regexFromString(preset.activation_regex);
|
||||
|
||||
// Stop on first match so it won't cycle back and forth between presets if multiple regexes match
|
||||
if (regex.test(modelId)) {
|
||||
if (regex instanceof RegExp && regex.test(modelId)) {
|
||||
selectInstructPreset(preset.name);
|
||||
|
||||
return true;
|
||||
|
@ -50,6 +50,7 @@ import {
|
||||
download,
|
||||
getBase64Async,
|
||||
getFileText,
|
||||
getImageSizeFromDataURL,
|
||||
getSortableDelay,
|
||||
isDataURL,
|
||||
parseJsonFile,
|
||||
@ -265,6 +266,7 @@ const default_settings = {
|
||||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
assistant_impersonation: '',
|
||||
human_sysprompt_message: default_claude_human_sysprompt_message,
|
||||
use_ai21_tokenizer: false,
|
||||
use_google_tokenizer: false,
|
||||
@ -273,6 +275,7 @@ const default_settings = {
|
||||
use_alt_scale: false,
|
||||
squash_system_messages: false,
|
||||
image_inlining: false,
|
||||
inline_image_quality: 'low',
|
||||
bypass_status_check: false,
|
||||
continue_prefill: false,
|
||||
names_behavior: character_names_behavior.NONE,
|
||||
@ -340,6 +343,7 @@ const oai_settings = {
|
||||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
assistant_impersonation: '',
|
||||
human_sysprompt_message: default_claude_human_sysprompt_message,
|
||||
use_ai21_tokenizer: false,
|
||||
use_google_tokenizer: false,
|
||||
@ -348,6 +352,7 @@ const oai_settings = {
|
||||
use_alt_scale: false,
|
||||
squash_system_messages: false,
|
||||
image_inlining: false,
|
||||
inline_image_quality: 'low',
|
||||
bypass_status_check: false,
|
||||
continue_prefill: false,
|
||||
names_behavior: character_names_behavior.NONE,
|
||||
@ -1764,7 +1769,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
|
||||
// Don't add a prefill on quiet gens (summarization)
|
||||
if (!isQuiet) {
|
||||
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
|
||||
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1844,6 +1849,8 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
generate_data['seed'] = oai_settings.seed;
|
||||
}
|
||||
|
||||
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
|
||||
|
||||
const generate_url = '/api/backends/chat-completions/generate';
|
||||
const response = await fetch(generate_url, {
|
||||
method: 'POST',
|
||||
@ -2188,12 +2195,47 @@ class Message {
|
||||
}
|
||||
}
|
||||
|
||||
const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
|
||||
this.content = [
|
||||
{ type: 'text', text: textContent },
|
||||
{ type: 'image_url', image_url: { 'url': image, 'detail': 'low' } },
|
||||
{ type: 'image_url', image_url: { 'url': image, 'detail': quality } },
|
||||
];
|
||||
|
||||
this.tokens += Message.tokensPerImage;
|
||||
const tokens = await this.getImageTokenCost(image, quality);
|
||||
this.tokens += tokens;
|
||||
}
|
||||
|
||||
async getImageTokenCost(dataUrl, quality) {
|
||||
if (quality === 'low') {
|
||||
return Message.tokensPerImage;
|
||||
}
|
||||
|
||||
const size = await getImageSizeFromDataURL(dataUrl);
|
||||
|
||||
// If the image is small enough, we can use the low quality token cost
|
||||
if (quality === 'auto' && size.width <= 512 && size.height <= 512) {
|
||||
return Message.tokensPerImage;
|
||||
}
|
||||
|
||||
/*
|
||||
* Images are first scaled to fit within a 2048 x 2048 square, maintaining their aspect ratio.
|
||||
* Then, they are scaled such that the shortest side of the image is 768px long.
|
||||
* Finally, we count how many 512px squares the image consists of.
|
||||
* Each of those squares costs 170 tokens. Another 85 tokens are always added to the final total.
|
||||
* https://platform.openai.com/docs/guides/vision/calculating-costs
|
||||
*/
|
||||
|
||||
const scale = 2048 / Math.min(size.width, size.height);
|
||||
const scaledWidth = Math.round(size.width * scale);
|
||||
const scaledHeight = Math.round(size.height * scale);
|
||||
|
||||
const finalScale = 768 / Math.min(scaledWidth, scaledHeight);
|
||||
const finalWidth = Math.round(scaledWidth * finalScale);
|
||||
const finalHeight = Math.round(scaledHeight * finalScale);
|
||||
|
||||
const squares = Math.ceil(finalWidth / 512) * Math.ceil(finalHeight / 512);
|
||||
const tokens = squares * 170 + 85;
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2720,8 +2762,10 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
|
||||
oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
|
||||
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
|
||||
oai_settings.assistant_impersonation = settings.assistant_impersonation ?? default_settings.assistant_impersonation;
|
||||
oai_settings.human_sysprompt_message = settings.human_sysprompt_message ?? default_settings.human_sysprompt_message;
|
||||
oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining;
|
||||
oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
|
||||
oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
|
||||
oai_settings.seed = settings.seed ?? default_settings.seed;
|
||||
oai_settings.n = settings.n ?? default_settings.n;
|
||||
@ -2755,10 +2799,14 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#api_url_scale').val(oai_settings.api_url_scale);
|
||||
$('#openai_proxy_password').val(oai_settings.proxy_password);
|
||||
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
|
||||
$('#claude_assistant_impersonation').val(oai_settings.assistant_impersonation);
|
||||
$('#claude_human_sysprompt_textarea').val(oai_settings.human_sysprompt_message);
|
||||
$('#openai_image_inlining').prop('checked', oai_settings.image_inlining);
|
||||
$('#openai_bypass_status_check').prop('checked', oai_settings.bypass_status_check);
|
||||
|
||||
$('#openai_inline_image_quality').val(oai_settings.inline_image_quality);
|
||||
$(`#openai_inline_image_quality option[value="${oai_settings.inline_image_quality}"]`).prop('selected', true);
|
||||
|
||||
$('#model_openai_select').val(oai_settings.openai_model);
|
||||
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
|
||||
$('#model_claude_select').val(oai_settings.claude_model);
|
||||
@ -3071,6 +3119,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
api_url_scale: settings.api_url_scale,
|
||||
show_external_models: settings.show_external_models,
|
||||
assistant_prefill: settings.assistant_prefill,
|
||||
assistant_impersonation: settings.assistant_impersonation,
|
||||
human_sysprompt_message: settings.human_sysprompt_message,
|
||||
use_ai21_tokenizer: settings.use_ai21_tokenizer,
|
||||
use_google_tokenizer: settings.use_google_tokenizer,
|
||||
@ -3079,6 +3128,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
use_alt_scale: settings.use_alt_scale,
|
||||
squash_system_messages: settings.squash_system_messages,
|
||||
image_inlining: settings.image_inlining,
|
||||
inline_image_quality: settings.inline_image_quality,
|
||||
bypass_status_check: settings.bypass_status_check,
|
||||
continue_prefill: settings.continue_prefill,
|
||||
continue_postfix: settings.continue_postfix,
|
||||
@ -3456,6 +3506,7 @@ function onSettingsPresetChange() {
|
||||
show_external_models: ['#openai_show_external_models', 'show_external_models', true],
|
||||
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
|
||||
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
|
||||
assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false],
|
||||
human_sysprompt_message: ['#claude_human_sysprompt_textarea', 'human_sysprompt_message', false],
|
||||
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
|
||||
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
|
||||
@ -3464,6 +3515,7 @@ function onSettingsPresetChange() {
|
||||
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
|
||||
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
|
||||
image_inlining: ['#openai_image_inlining', 'image_inlining', true],
|
||||
inline_image_quality: ['#openai_inline_image_quality', 'inline_image_quality', false],
|
||||
continue_prefill: ['#continue_prefill', 'continue_prefill', true],
|
||||
continue_postfix: ['#continue_postfix', 'continue_postfix', false],
|
||||
seed: ['#seed_openai', 'seed', false],
|
||||
@ -3480,6 +3532,11 @@ function onSettingsPresetChange() {
|
||||
preset.names_behavior = character_names_behavior.COMPLETION;
|
||||
}
|
||||
|
||||
// Claude: Assistant Impersonation Prefill = Inherit from Assistant Prefill
|
||||
if (preset.assistant_prefill !== undefined && preset.assistant_impersonation === undefined) {
|
||||
preset.assistant_impersonation = preset.assistant_prefill;
|
||||
}
|
||||
|
||||
const updateInput = (selector, value) => $(selector).val(value).trigger('input');
|
||||
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input');
|
||||
|
||||
@ -3515,7 +3572,7 @@ function getMaxContextOpenAI(value) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
return unlocked_max;
|
||||
}
|
||||
else if (value.includes('gpt-4-turbo') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
|
||||
else if (value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
|
||||
return max_128k;
|
||||
}
|
||||
else if (value.includes('gpt-3.5-turbo-1106')) {
|
||||
@ -3679,7 +3736,7 @@ async function onModelChange() {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', max_1mil);
|
||||
} else if (value === 'gemini-1.5-pro-latest') {
|
||||
} else if (value === 'gemini-1.5-pro-latest' || value.includes('gemini-1.5-flash')) {
|
||||
$('#openai_max_context').attr('max', max_1mil);
|
||||
} else if (value === 'gemini-ultra' || value === 'gemini-1.0-pro-latest' || value === 'gemini-pro' || value === 'gemini-1.0-ultra-latest') {
|
||||
$('#openai_max_context').attr('max', max_32k);
|
||||
@ -4239,11 +4296,14 @@ export function isImageInliningSupported() {
|
||||
// gultra just isn't being offered as multimodal, thanks google.
|
||||
const visionSupportedModels = [
|
||||
'gpt-4-vision',
|
||||
'gemini-1.5-flash-latest',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.0-pro-vision-latest',
|
||||
'gemini-1.5-pro-latest',
|
||||
'gemini-pro-vision',
|
||||
'claude-3',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4o',
|
||||
];
|
||||
|
||||
switch (oai_settings.chat_completion_source) {
|
||||
@ -4672,6 +4732,11 @@ $(document).ready(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_assistant_impersonation').on('input', function () {
|
||||
oai_settings.assistant_impersonation = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_human_sysprompt_textarea').on('input', function () {
|
||||
oai_settings.human_sysprompt_message = String($('#claude_human_sysprompt_textarea').val());
|
||||
saveSettingsDebounced();
|
||||
@ -4707,6 +4772,11 @@ $(document).ready(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#openai_inline_image_quality').on('input', function () {
|
||||
oai_settings.inline_image_quality = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#continue_prefill').on('input', function () {
|
||||
oai_settings.continue_prefill = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
|
@ -259,6 +259,7 @@ let power_user = {
|
||||
stscript: {
|
||||
matching: 'fuzzy',
|
||||
autocomplete: {
|
||||
autoHide: false,
|
||||
style: 'theme',
|
||||
font: {
|
||||
scale: 0.8,
|
||||
@ -1618,6 +1619,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
$('#token_padding').val(power_user.token_padding);
|
||||
$('#aux_field').val(power_user.aux_field);
|
||||
|
||||
$('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
|
||||
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
|
||||
$('#stscript_autocomplete_style').val(power_user.stscript.autocomplete_style ?? 'theme');
|
||||
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
|
||||
@ -3646,6 +3648,11 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_autocomplete_autoHide').on('input', function () {
|
||||
power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_matching').on('change', function () {
|
||||
const value = $(this).find(':selected').val();
|
||||
power_user.stscript.matching = String(value);
|
||||
|
@ -114,7 +114,9 @@ class PresetManager {
|
||||
* @returns {any} Preset value
|
||||
*/
|
||||
findPreset(name) {
|
||||
return $(this.select).find(`option:contains(${name})`).val();
|
||||
return $(this.select).find('option').filter(function() {
|
||||
return $(this).text() === name;
|
||||
}).val();
|
||||
}
|
||||
|
||||
/**
|
||||
|
441
public/scripts/samplerSelect.js
Normal file
441
public/scripts/samplerSelect.js
Normal file
@ -0,0 +1,441 @@
|
||||
import {
|
||||
main_api,
|
||||
saveSettingsDebounced,
|
||||
novelai_setting_names,
|
||||
callPopup,
|
||||
settings,
|
||||
} from '../script.js';
|
||||
import { power_user } from './power-user.js';
|
||||
//import { BIAS_CACHE, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
|
||||
//import { getEventSourceStream } from './sse-stream.js';
|
||||
//import { getSortableDelay, onlyUnique } from './utils.js';
|
||||
//import { getCfgPrompt } from './cfg-scale.js';
|
||||
import { setting_names } from './textgen-settings.js';
|
||||
|
||||
|
||||
const TGsamplerNames = setting_names;
|
||||
|
||||
const forcedOnColoring = 'filter: sepia(1) hue-rotate(59deg) contrast(1.5) saturate(3.5)';
|
||||
const forcedOffColoring = 'filter: sepia(1) hue-rotate(308deg) contrast(0.7) saturate(10)';
|
||||
|
||||
let userDisabledSamplers, userShownSamplers;
|
||||
|
||||
/*
|
||||
|
||||
for reference purposes:
|
||||
|
||||
//NAI
|
||||
const nai_settings = {
|
||||
temperature: 1.5,
|
||||
repetition_penalty: 2.25,
|
||||
repetition_penalty_range: 2048,
|
||||
repetition_penalty_slope: 0.09,
|
||||
repetition_penalty_frequency: 0,
|
||||
repetition_penalty_presence: 0.005,
|
||||
tail_free_sampling: 0.975,
|
||||
top_k: 10,
|
||||
top_p: 0.75,
|
||||
top_a: 0.08,
|
||||
typical_p: 0.975,
|
||||
min_length: 1,
|
||||
model_novel: 'clio-v1',
|
||||
preset_settings_novel: 'Talker-Chat-Clio',
|
||||
streaming_novel: false,
|
||||
preamble: default_preamble,
|
||||
prefix: '',
|
||||
cfg_uc: '',
|
||||
banned_tokens: '',
|
||||
order: default_order,
|
||||
logit_bias: [],
|
||||
};
|
||||
|
||||
// TG Types
|
||||
export const textgen_types = {
|
||||
OOBA: 'ooba',
|
||||
MANCER: 'mancer',
|
||||
VLLM: 'vllm',
|
||||
APHRODITE: 'aphrodite',
|
||||
TABBY: 'tabby',
|
||||
KOBOLDCPP: 'koboldcpp',
|
||||
TOGETHERAI: 'togetherai',
|
||||
LLAMACPP: 'llamacpp',
|
||||
OLLAMA: 'ollama',
|
||||
INFERMATICAI: 'infermaticai',
|
||||
DREAMGEN: 'dreamgen',
|
||||
OPENROUTER: 'openrouter',
|
||||
};
|
||||
|
||||
//KAI and TextGen
|
||||
const setting_names = [
|
||||
'temp',
|
||||
'temperature_last',
|
||||
'rep_pen',
|
||||
'rep_pen_range',
|
||||
'no_repeat_ngram_size',
|
||||
'top_k',
|
||||
'top_p',
|
||||
'top_a',
|
||||
'tfs',
|
||||
'epsilon_cutoff',
|
||||
'eta_cutoff',
|
||||
'typical_p',
|
||||
'min_p',
|
||||
'penalty_alpha',
|
||||
'num_beams',
|
||||
'length_penalty',
|
||||
'min_length',
|
||||
'dynatemp',
|
||||
'min_temp',
|
||||
'max_temp',
|
||||
'dynatemp_exponent',
|
||||
'smoothing_factor',
|
||||
'smoothing_curve',
|
||||
'max_tokens_second',
|
||||
'encoder_rep_pen',
|
||||
'freq_pen',
|
||||
'presence_pen',
|
||||
'do_sample',
|
||||
'early_stopping',
|
||||
'seed',
|
||||
'add_bos_token',
|
||||
'ban_eos_token',
|
||||
'skip_special_tokens',
|
||||
'streaming',
|
||||
'mirostat_mode',
|
||||
'mirostat_tau',
|
||||
'mirostat_eta',
|
||||
'guidance_scale',
|
||||
'negative_prompt',
|
||||
'grammar_string',
|
||||
'json_schema',
|
||||
'banned_tokens',
|
||||
'legacy_api',
|
||||
//'n_aphrodite',
|
||||
//'best_of_aphrodite',
|
||||
'ignore_eos_token',
|
||||
'spaces_between_special_tokens',
|
||||
//'logits_processors_aphrodite',
|
||||
//'log_probs_aphrodite',
|
||||
//'prompt_log_probs_aphrodite'
|
||||
'sampler_order',
|
||||
'sampler_priority',
|
||||
'samplers',
|
||||
'n',
|
||||
'logit_bias',
|
||||
'custom_model',
|
||||
'bypass_status_check',
|
||||
];
|
||||
|
||||
//OAI settings
|
||||
|
||||
const default_settings = {
|
||||
preset_settings_openai: 'Default',
|
||||
temp_openai: 1.0,
|
||||
freq_pen_openai: 0,
|
||||
pres_pen_openai: 0,
|
||||
count_pen: 0.0,
|
||||
top_p_openai: 1.0,
|
||||
top_k_openai: 0,
|
||||
min_p_openai: 0,
|
||||
top_a_openai: 1,
|
||||
repetition_penalty_openai: 1,
|
||||
stream_openai: false,
|
||||
//...
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
|
||||
// Goal 1: show popup with all samplers for active API
|
||||
async function showSamplerSelectPopup() {
|
||||
const popup = $('#dialogue_popup');
|
||||
popup.addClass('large_dialogue_popup');
|
||||
const html = $(document.createElement('div'));
|
||||
html.attr('id', 'sampler_view_list')
|
||||
.addClass('flex-container flexFlowColumn');
|
||||
html.append(`
|
||||
<div class="title_restorable flexFlowColumn alignItemsBaseline">
|
||||
<div class="flex-container justifyCenter">
|
||||
<h3>Sampler Select</h3>
|
||||
<div class="flex-container alignItemsBaseline">
|
||||
<div id="resetSelectedSamplers" class="menu_button menu_button_icon tag_view_create" title="Reset custom sampler selection">
|
||||
<i class="fa-solid fa-recycle"></i>
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="flex-container alignItemsBaseline">
|
||||
<div class="menu_button menu_button_icon tag_view_create" title="Create a new sampler">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span data-i18n="Create">Create</span>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
<small>Here you can toggle the display of individual samplers. (WIP)</small>
|
||||
</div>
|
||||
<hr>`);
|
||||
|
||||
const listContainer = $('<div id="apiSamplersList" class="flex-container flexNoGap"></div>');
|
||||
const APISamplers = await listSamplers(main_api);
|
||||
listContainer.append(APISamplers);
|
||||
html.append(listContainer);
|
||||
|
||||
callPopup(html, 'text', null, { allowVerticalScrolling: true });
|
||||
|
||||
setSamplerListListeners();
|
||||
|
||||
$('#resetSelectedSamplers').off('click').on('click', async function () {
|
||||
console.log('saw sampler select reset click');
|
||||
userDisabledSamplers = [];
|
||||
userShownSamplers = [];
|
||||
power_user.selectSamplers.forceShown = [];
|
||||
power_user.selectSamplers.forceHidden = [];
|
||||
await validateDisabledSamplers(true);
|
||||
});
|
||||
}
|
||||
|
||||
function setSamplerListListeners() {
|
||||
// Goal 2: hide unchecked samplers from DOM
|
||||
let listContainer = $('#apiSamplersList');
|
||||
listContainer.find('input').off('change').on('change', async function () {
|
||||
|
||||
const samplerName = this.name.replace('_checkbox', '');
|
||||
let relatedDOMElement = $(`#${samplerName}_${main_api}`).parent();
|
||||
let targetDisplayType = 'flex';
|
||||
|
||||
if (samplerName === 'json_schema') {
|
||||
relatedDOMElement = $('#json_schema_block');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'grammar_string') {
|
||||
relatedDOMElement = $('#grammar_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'guidance_scale') {
|
||||
relatedDOMElement = $('#cfg_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'mirostat_mode') {
|
||||
relatedDOMElement = $('#mirostat_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'dynatemp') {
|
||||
relatedDOMElement = $('#dynatemp_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'banned_tokens') {
|
||||
relatedDOMElement = $('#banned_tokens_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'sampler_order') {
|
||||
relatedDOMElement = $('#sampler_order_block');
|
||||
targetDisplayType = 'flex';
|
||||
}
|
||||
|
||||
// Get the current state of the custom data attribute
|
||||
const previousState = relatedDOMElement.data('selectsampler');
|
||||
|
||||
if ($(this).prop('checked') === false) {
|
||||
//console.log('saw clicking checkbox from on to off...');
|
||||
if (previousState === 'shown') {
|
||||
console.log('saw previously custom shown sampler');
|
||||
//console.log('removing from custom force show list');
|
||||
relatedDOMElement.removeData('selectsampler');
|
||||
$(this).parent().find('.sampler_name').removeAttr('style');
|
||||
power_user?.selectSamplers?.forceShown.splice(power_user?.selectSamplers?.forceShown.indexOf(samplerName), 1);
|
||||
console.log(power_user?.selectSamplers?.forceShown);
|
||||
} else {
|
||||
console.log('saw previous untouched sampler');
|
||||
//console.log(`adding ${samplerName} to force hide list`);
|
||||
relatedDOMElement.data('selectsampler', 'hidden');
|
||||
console.log(relatedDOMElement.data('selectsampler'));
|
||||
power_user.selectSamplers.forceHidden.push(samplerName);
|
||||
$(this).parent().find('.sampler_name').attr('style', forcedOffColoring);
|
||||
console.log(power_user.selectSamplers.forceHidden);
|
||||
}
|
||||
} else { // going from unchecked to checked
|
||||
//console.log('saw clicking checkbox from off to on...');
|
||||
if (previousState === 'hidden') {
|
||||
console.log('saw previously custom hidden sampler');
|
||||
//console.log('removing from custom force hide list');
|
||||
relatedDOMElement.removeData('selectsampler');
|
||||
$(this).parent().find('.sampler_name').removeAttr('style');
|
||||
power_user?.selectSamplers?.forceHidden.splice(power_user?.selectSamplers?.forceHidden.indexOf(samplerName), 1);
|
||||
console.log(power_user?.selectSamplers?.forceHidden);
|
||||
} else {
|
||||
console.log('saw previous untouched sampler');
|
||||
//console.log(`adding ${samplerName} to force shown list`);
|
||||
relatedDOMElement.data('selectsampler', 'shown');
|
||||
console.log(relatedDOMElement.data('selectsampler'));
|
||||
power_user.selectSamplers.forceShown.push(samplerName);
|
||||
$(this).parent().find('.sampler_name').attr('style', forcedOnColoring);
|
||||
console.log(power_user.selectSamplers.forceShown);
|
||||
}
|
||||
}
|
||||
await saveSettingsDebounced();
|
||||
|
||||
const shouldDisplay = $(this).prop('checked') ? targetDisplayType : 'none';
|
||||
relatedDOMElement.css('display', shouldDisplay);
|
||||
|
||||
console.log(samplerName, relatedDOMElement.data('selectsampler'), shouldDisplay);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function isElementVisibleInDOM(element) {
|
||||
while (element && element !== document.body) {
|
||||
if (window.getComputedStyle(element).display === 'none') {
|
||||
return false;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
async function listSamplers(main_api, arrayOnly = false) {
|
||||
let availableSamplers;
|
||||
if (main_api === 'textgenerationwebui') {
|
||||
availableSamplers = TGsamplerNames;
|
||||
const valuesToRemove = new Set(['streaming', 'seed', 'bypass_status_check', 'custom_model', 'legacy_api', 'samplers']);
|
||||
availableSamplers = availableSamplers.filter(sampler => !valuesToRemove.has(sampler));
|
||||
availableSamplers.sort();
|
||||
}
|
||||
|
||||
if (arrayOnly) {
|
||||
console.log('returning full samplers array');
|
||||
return availableSamplers;
|
||||
}
|
||||
|
||||
const samplersListHTML = availableSamplers.reduce((html, sampler) => {
|
||||
let customColor;
|
||||
const targetDOMelement = $(`#${sampler}_${main_api}`);
|
||||
|
||||
const isInForceHiddenArray = userDisabledSamplers.includes(sampler);
|
||||
const isInForceShownArray = userShownSamplers.includes(sampler);
|
||||
let isVisibleInDOM = isElementVisibleInDOM(targetDOMelement[0]);
|
||||
const isInDefaultState = () => {
|
||||
if (isVisibleInDOM && isInForceShownArray) { return false; }
|
||||
else if (!isVisibleInDOM && isInForceHiddenArray) { return false; }
|
||||
else { return true; }
|
||||
};
|
||||
|
||||
const shouldBeChecked = () => {
|
||||
if (isInForceHiddenArray) {
|
||||
customColor = forcedOffColoring;
|
||||
return false;
|
||||
}
|
||||
else if (isInForceShownArray) {
|
||||
customColor = forcedOnColoring;
|
||||
return true;
|
||||
}
|
||||
else { return isVisibleInDOM; }
|
||||
};
|
||||
console.log(sampler, isInDefaultState(), isInForceHiddenArray, shouldBeChecked());
|
||||
return html + `
|
||||
<div class="sampler_view_list_item wide50p flex-container">
|
||||
<input type="checkbox" name="${sampler}_checkbox" ${shouldBeChecked() ? 'checked' : ''}>
|
||||
<small class="sampler_name" style="${customColor}">${sampler}</small>
|
||||
</div>
|
||||
`;
|
||||
}, '');
|
||||
|
||||
return samplersListHTML;
|
||||
}
|
||||
|
||||
// Goal 3: make "sampler is hidden/disabled" status persistent (save settings)
|
||||
// this runs on initial getSettings as well as after API changes
|
||||
|
||||
export async function validateDisabledSamplers(redraw = false) {
|
||||
const APISamplers = await listSamplers(main_api, true);
|
||||
|
||||
if (!Array.isArray(APISamplers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const sampler of APISamplers) {
|
||||
let relatedDOMElement = $(`#${sampler}_${main_api}`).parent();
|
||||
let targetDisplayType = 'flex';
|
||||
|
||||
if (sampler === 'json_schema') {
|
||||
relatedDOMElement = $('#json_schema_block');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'grammar_string') {
|
||||
relatedDOMElement = $('#grammar_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'guidance_scale') {
|
||||
relatedDOMElement = $('#cfg_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'mirostat_mode') {
|
||||
relatedDOMElement = $('#mirostat_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'dynatemp') {
|
||||
relatedDOMElement = $('#dynatemp_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'banned_tokens') {
|
||||
relatedDOMElement = $('#banned_tokens_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'sampler_order') {
|
||||
relatedDOMElement = $('#sampler_order_block');
|
||||
}
|
||||
|
||||
if (power_user?.selectSamplers?.forceHidden.includes(sampler)) {
|
||||
//default handling for standard sliders
|
||||
relatedDOMElement.data('selectsampler', 'hidden');
|
||||
relatedDOMElement.css('display', 'none');
|
||||
} else if (power_user?.selectSamplers?.forceShown.includes(sampler)) {
|
||||
relatedDOMElement.data('selectsampler', 'shown');
|
||||
relatedDOMElement.css('display', targetDisplayType);
|
||||
} else {
|
||||
if (relatedDOMElement.data('selectsampler') === 'hidden') {
|
||||
relatedDOMElement.removeAttr('selectsampler');
|
||||
relatedDOMElement.css('display', targetDisplayType);
|
||||
}
|
||||
if (relatedDOMElement.data('selectsampler') === 'shown') {
|
||||
relatedDOMElement.removeAttr('selectsampler');
|
||||
relatedDOMElement.css('display', 'none');
|
||||
}
|
||||
}
|
||||
if (redraw) {
|
||||
let samplersHTML = await listSamplers(main_api);
|
||||
$('#apiSamplersList').empty().append(samplersHTML);
|
||||
setSamplerListListeners();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function initCustomSelectedSamplers() {
|
||||
|
||||
userDisabledSamplers = power_user?.selectSamplers?.forceHidden || [];
|
||||
userShownSamplers = power_user?.selectSamplers?.forceShown || [];
|
||||
power_user.selectSamplers = {};
|
||||
power_user.selectSamplers.forceHidden = userDisabledSamplers;
|
||||
power_user.selectSamplers.forceShown = userShownSamplers;
|
||||
await saveSettingsDebounced();
|
||||
$('#samplerSelectButton').off('click').on('click', showSamplerSelectPopup);
|
||||
|
||||
}
|
||||
|
||||
// Goal 4: filter hidden samplers from API output
|
||||
|
||||
// Goal 5: allow addition of custom samplers to be displayed
|
||||
// Goal 6: send custom sampler values into prompt
|
@ -447,8 +447,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unhide',
|
||||
],
|
||||
helpString: 'Unhides a message from the prompt.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'disable',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-disable',
|
||||
callback: disableGroupMemberCallback,
|
||||
aliases: ['disable', 'disablemember', 'memberdisable'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@ -456,7 +457,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'disable',
|
||||
],
|
||||
helpString: 'Disables a group member from being drafted for replies.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'enable',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-enable',
|
||||
aliases: ['enable', 'enablemember', 'memberenable'],
|
||||
callback: enableGroupMemberCallback,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
@ -465,9 +467,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'enable',
|
||||
],
|
||||
helpString: 'Enables a group member to be drafted for replies.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberadd',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-add',
|
||||
callback: addGroupMemberCallback,
|
||||
aliases: ['addmember'],
|
||||
aliases: ['addmember', 'memberadd'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'character name', [ARGUMENT_TYPE.STRING], true,
|
||||
@ -481,15 +483,15 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberadd',
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/memberadd John Doe</code></pre>
|
||||
<pre><code>/member-add John Doe</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberremove',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-remove',
|
||||
callback: removeGroupMemberCallback,
|
||||
aliases: ['removemember'],
|
||||
aliases: ['removemember', 'memberremove'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@ -503,16 +505,16 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberremove
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/memberremove 2</code></pre>
|
||||
<pre><code>/memberremove John Doe</code></pre>
|
||||
<pre><code>/member-remove 2</code></pre>
|
||||
<pre><code>/member-remove John Doe</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberup',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-up',
|
||||
callback: moveGroupMemberUpCallback,
|
||||
aliases: ['upmember'],
|
||||
aliases: ['upmember', 'memberup'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@ -520,9 +522,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberup',
|
||||
],
|
||||
helpString: 'Moves a group member up in the group chat list.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberdown',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-down',
|
||||
callback: moveGroupMemberDownCallback,
|
||||
aliases: ['downmember'],
|
||||
aliases: ['downmember', 'memberdown'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@ -702,6 +704,19 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'addswipe',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'abort',
|
||||
callback: abortCallback,
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({ name: 'quiet',
|
||||
description: 'Whether to suppress the toast message notifying about the /abort call.',
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
defaultValue: 'true',
|
||||
enumList: ['true', 'false'],
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({ description: 'The reason for aborting command execution. Shown when quiet=false',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
}),
|
||||
],
|
||||
helpString: 'Aborts the slash command batch execution.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'fuzzy',
|
||||
@ -847,6 +862,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'messages',
|
||||
new SlashCommandNamedArgument(
|
||||
'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', ['off', 'on'],
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', ['off', 'on'],
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'role', 'filter messages by role' , [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'assistant', 'user'],
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
@ -858,6 +879,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'messages',
|
||||
<div>
|
||||
Returns the specified message or range of messages as a string.
|
||||
</div>
|
||||
<div>
|
||||
Use the <code>hidden=off</code> argument to exclude hidden messages.
|
||||
</div>
|
||||
<div>
|
||||
Use the <code>role</code> argument to filter messages by role. Possible values are: system, assistant, user.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Examples:</strong>
|
||||
<ul>
|
||||
@ -1310,13 +1337,37 @@ async function popupCallback(args, value) {
|
||||
|
||||
function getMessagesCallback(args, value) {
|
||||
const includeNames = !isFalseBoolean(args?.names);
|
||||
const includeHidden = isTrueBoolean(args?.hidden);
|
||||
const role = args?.role;
|
||||
const range = stringToRange(value, 0, chat.length - 1);
|
||||
|
||||
if (!range) {
|
||||
console.warn(`WARN: Invalid range provided for /getmessages command: ${value}`);
|
||||
console.warn(`WARN: Invalid range provided for /messages command: ${value}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const filterByRole = (mes) => {
|
||||
if (!role) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isNarrator = mes.extra?.type === system_message_types.NARRATOR;
|
||||
|
||||
if (role === 'system') {
|
||||
return isNarrator && !mes.is_user;
|
||||
}
|
||||
|
||||
if (role === 'assistant') {
|
||||
return !isNarrator && !mes.is_user;
|
||||
}
|
||||
|
||||
if (role === 'user') {
|
||||
return !isNarrator && mes.is_user;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
|
||||
};
|
||||
|
||||
const messages = [];
|
||||
|
||||
for (let messageId = range.start; messageId <= range.end; messageId++) {
|
||||
@ -1326,7 +1377,13 @@ function getMessagesCallback(args, value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.is_system) {
|
||||
if (role && !filterByRole(message)) {
|
||||
console.debug(`/messages: Skipping message with ID ${messageId} due to role filter`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!includeHidden && message.is_system) {
|
||||
console.debug(`/messages: Skipping hidden message with ID ${messageId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1377,9 +1434,15 @@ async function runCallback(args, name) {
|
||||
}
|
||||
}
|
||||
|
||||
function abortCallback() {
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
throw new Error('/abort command executed');
|
||||
/**
|
||||
*
|
||||
* @param {object} param0
|
||||
* @param {SlashCommandAbortController} param0._abortController
|
||||
* @param {string} [param0.quiet]
|
||||
* @param {string} [reason]
|
||||
*/
|
||||
function abortCallback({ _abortController, quiet }, reason) {
|
||||
_abortController.abort((reason ?? '').toString().length == 0 ? '/abort command executed' : reason, !isFalseBoolean(quiet ?? 'true'));
|
||||
}
|
||||
|
||||
async function delayCallback(_, amount) {
|
||||
@ -2645,7 +2708,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
|
||||
* @prop {boolean} [handleParserErrors] (true) Whether to handle parser errors (show toast on error) or throw.
|
||||
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
|
||||
* @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
|
||||
* @prop {PARSER_FLAG[]} [parserFlags] (null) Parser flags to apply
|
||||
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
|
||||
* @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
|
||||
* @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
|
||||
*/
|
||||
@ -2653,7 +2716,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
|
||||
/**
|
||||
* @typedef ExecuteSlashCommandsOnChatInputOptions
|
||||
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
|
||||
* @prop {PARSER_FLAG[]} [parserFlags] (null) Parser flags to apply
|
||||
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
|
||||
* @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
|
||||
*/
|
||||
|
||||
@ -2705,10 +2768,12 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
|
||||
}
|
||||
} catch (e) {
|
||||
document.querySelector('#form_sheld').classList.add('script_error');
|
||||
toastr.error(e.message);
|
||||
result = new SlashCommandClosureResult();
|
||||
result.isError = true;
|
||||
result.errorMessage = e.message;
|
||||
if (e.cause !== 'abort') {
|
||||
toastr.error(e.message);
|
||||
}
|
||||
} finally {
|
||||
delay(1000).then(()=>clearCommandProgressDebounced());
|
||||
|
||||
@ -2740,7 +2805,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
|
||||
|
||||
let closure;
|
||||
try {
|
||||
closure = parser.parse(text, true, options.parserFlags, options.abortController);
|
||||
closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController());
|
||||
closure.scope.parent = options.scope;
|
||||
closure.onProgress = options.onProgress;
|
||||
} catch (e) {
|
||||
@ -2767,8 +2832,9 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
|
||||
|
||||
try {
|
||||
const result = await closure.execute();
|
||||
if (result.isAborted) {
|
||||
if (result.isAborted && !result.isQuietlyAborted) {
|
||||
toastr.warning(result.abortReason, 'Command execution aborted');
|
||||
closure.abortController.signal.isQuiet = true;
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
@ -1,5 +1,25 @@
|
||||
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { PARSER_FLAG } from './SlashCommandParser.js';
|
||||
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* _pipe:string|SlashCommandClosure,
|
||||
* _scope:SlashCommandScope,
|
||||
* _parserFlags:{[id:PARSER_FLAG]:boolean},
|
||||
* _abortController:SlashCommandAbortController,
|
||||
* [id:string]:string|SlashCommandClosure,
|
||||
* }} NamedArguments
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {string|SlashCommandClosure|(string|SlashCommandClosure)[]} UnnamedArguments
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@ -8,7 +28,7 @@ export class SlashCommand {
|
||||
* Creates a SlashCommand from a properties object.
|
||||
* @param {Object} props
|
||||
* @param {string} [props.name]
|
||||
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||
* @param {(namedArguments:NamedArguments, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||
* @param {string} [props.helpString]
|
||||
* @param {boolean} [props.splitUnnamedArgument]
|
||||
* @param {string[]} [props.aliases]
|
||||
@ -25,7 +45,7 @@ export class SlashCommand {
|
||||
|
||||
|
||||
/**@type {string}*/ name;
|
||||
/**@type {(namedArguments:Object<string, string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
|
||||
/**@type {(namedArguments:{_pipe:string|SlashCommandClosure, _scope:SlashCommandScope, _abortController:SlashCommandAbortController, [id:string]:string|SlashCommandClosure}, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
|
||||
/**@type {string}*/ helpString;
|
||||
/**@type {boolean}*/ splitUnnamedArgument = false;
|
||||
/**@type {string[]}*/ aliases = [];
|
||||
|
@ -5,7 +5,8 @@ export class SlashCommandAbortController {
|
||||
constructor() {
|
||||
this.signal = new SlashCommandAbortSignal();
|
||||
}
|
||||
abort(reason = 'No reason.') {
|
||||
abort(reason = 'No reason.', isQuiet = false) {
|
||||
this.signal.isQuiet = isQuiet;
|
||||
this.signal.aborted = true;
|
||||
this.signal.reason = reason;
|
||||
}
|
||||
@ -20,8 +21,8 @@ export class SlashCommandAbortController {
|
||||
}
|
||||
|
||||
export class SlashCommandAbortSignal {
|
||||
/**@type {boolean}*/ isQuiet = false;
|
||||
/**@type {boolean}*/ paused = false;
|
||||
/**@type {boolean}*/ aborted = false;
|
||||
/**@type {string}*/ reason = null;
|
||||
|
||||
}
|
||||
|
@ -50,6 +50,17 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
}
|
||||
|
||||
getNamedArgumentAt(text, index, isSelect) {
|
||||
function getSplitRegex() {
|
||||
try {
|
||||
return new RegExp('(?<==)');
|
||||
} catch {
|
||||
// For browsers that don't support lookbehind
|
||||
return new RegExp('=(.*)');
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(this.executor.command?.namedArgumentList)) {
|
||||
return null;
|
||||
}
|
||||
const notProvidedNamedArguments = this.executor.command.namedArgumentList.filter(arg=>!this.executor.namedArgumentList.find(it=>it.name == arg.name));
|
||||
let name;
|
||||
let value;
|
||||
@ -62,7 +73,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
// cursor is somewhere within the named arguments (including final space)
|
||||
argAssign = this.executor.namedArgumentList.find(it=>it.start <= index && it.end >= index);
|
||||
if (argAssign) {
|
||||
const [argName, ...v] = text.slice(argAssign.start, index).split(/(?<==)/);
|
||||
const [argName, ...v] = text.slice(argAssign.start, index).split(getSplitRegex());
|
||||
name = argName;
|
||||
value = v.join('');
|
||||
start = argAssign.start;
|
||||
@ -99,7 +110,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
value,
|
||||
start + name.length,
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(it)),
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
true,
|
||||
);
|
||||
result.isRequired = true;
|
||||
@ -122,6 +133,9 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
}
|
||||
|
||||
getUnnamedArgumentAt(text, index, isSelect) {
|
||||
if (!Array.isArray(this.executor.command?.unnamedArgumentList)) {
|
||||
return null;
|
||||
}
|
||||
const lastArgIsBlank = this.executor.unnamedArgumentList.slice(-1)[0]?.value == '';
|
||||
const notProvidedArguments = this.executor.command.unnamedArgumentList.slice(this.executor.unnamedArgumentList.length - (lastArgIsBlank ? 1 : 0));
|
||||
let value;
|
||||
@ -154,7 +168,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
value,
|
||||
start,
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(it)),
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
false,
|
||||
);
|
||||
const isCompleteValue = cmdArg.enumList.find(it=>it.value == value);
|
||||
|
@ -161,6 +161,7 @@ export class SlashCommandClosure {
|
||||
let args = {
|
||||
_scope: this.scope,
|
||||
_parserFlags: executor.parserFlags,
|
||||
_abortController: this.abortController,
|
||||
};
|
||||
let value;
|
||||
// substitute named arguments
|
||||
@ -223,6 +224,15 @@ export class SlashCommandClosure {
|
||||
?.replace(/\\\{/g, '{')
|
||||
?.replace(/\\\}/g, '}')
|
||||
;
|
||||
} else if (Array.isArray(value)) {
|
||||
value = value.map(v=>{
|
||||
if (typeof v == 'string') {
|
||||
return v
|
||||
?.replace(/\\\{/g, '{')
|
||||
?.replace(/\\\}/g, '}');
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
let abortResult = await this.testAbortController();
|
||||
@ -254,6 +264,7 @@ export class SlashCommandClosure {
|
||||
if (this.abortController?.signal?.aborted) {
|
||||
const result = new SlashCommandClosureResult();
|
||||
result.isAborted = true;
|
||||
result.isQuietlyAborted = this.abortController.signal.isQuiet;
|
||||
result.abortReason = this.abortController.signal.reason.toString();
|
||||
return result;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ export class SlashCommandClosureResult {
|
||||
/**@type {boolean}*/ interrupt = false;
|
||||
/**@type {string}*/ pipe;
|
||||
/**@type {boolean}*/ isAborted = false;
|
||||
/**@type {boolean}*/ isQuietlyAborted = false;
|
||||
/**@type {string}*/ abortReason;
|
||||
/**@type {boolean}*/ isError = false;
|
||||
/**@type {string}*/ errorMessage;
|
||||
|
@ -1,16 +1,20 @@
|
||||
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||
|
||||
export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
|
||||
/**@type {SlashCommand}*/ cmd;
|
||||
/**@type {SlashCommandEnumValue}*/ enumValue;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {SlashCommand} cmd
|
||||
* @param {SlashCommandEnumValue} enumValue
|
||||
*/
|
||||
constructor(enumValue) {
|
||||
constructor(cmd, enumValue) {
|
||||
super(enumValue.value, '◊');
|
||||
this.cmd = cmd;
|
||||
this.enumValue = enumValue;
|
||||
}
|
||||
|
||||
@ -25,22 +29,6 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
|
||||
|
||||
|
||||
renderDetails() {
|
||||
const frag = document.createDocumentFragment();
|
||||
const specs = document.createElement('div'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('div'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = this.name;
|
||||
specs.append(name);
|
||||
}
|
||||
frag.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
help.textContent = this.enumValue.description;
|
||||
frag.append(help);
|
||||
}
|
||||
return frag;
|
||||
return this.cmd.renderHelpDetails();
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export class SlashCommandExecutor {
|
||||
// @ts-ignore
|
||||
/**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];
|
||||
/**@type {SlashCommandUnnamedArgumentAssignment[]}*/ unnamedArgumentList = [];
|
||||
/**@type {Object<PARSER_FLAG,boolean>} */ parserFlags;
|
||||
/**@type {{[id:PARSER_FLAG]:boolean}} */ parserFlags;
|
||||
|
||||
get commandCount() {
|
||||
return 1
|
||||
|
@ -27,22 +27,6 @@ export class SlashCommandNamedArgumentAutoCompleteOption extends AutoCompleteOpt
|
||||
|
||||
|
||||
renderDetails() {
|
||||
const frag = document.createDocumentFragment();
|
||||
const specs = document.createElement('div'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('div'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = this.name;
|
||||
specs.append(name);
|
||||
}
|
||||
frag.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
help.innerHTML = `${this.arg.isRequired ? '' : '(optional) '}${this.arg.description ?? ''}`;
|
||||
frag.append(help);
|
||||
}
|
||||
return frag;
|
||||
return this.cmd.renderHelpDetails();
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +114,6 @@ export class SlashCommandParser {
|
||||
|
||||
|
||||
constructor() {
|
||||
//TODO should not be re-registered from every instance
|
||||
// add dummy commands for help strings / autocomplete
|
||||
if (!Object.keys(this.commands).includes('parser-flag')) {
|
||||
const help = {};
|
||||
@ -186,6 +185,15 @@ export class SlashCommandParser {
|
||||
relevance: 0,
|
||||
};
|
||||
|
||||
function getQuotedRunRegex() {
|
||||
try {
|
||||
return new RegExp('(".+?(?<!\\\\)")|(\\S+?)');
|
||||
} catch {
|
||||
// fallback for browsers that don't support lookbehind
|
||||
return /(".+?")|(\S+?)/;
|
||||
}
|
||||
}
|
||||
|
||||
const COMMENT = {
|
||||
scope: 'comment',
|
||||
begin: /\/[/#]/,
|
||||
@ -225,7 +233,7 @@ export class SlashCommandParser {
|
||||
const RUN = {
|
||||
match: [
|
||||
/\/:/,
|
||||
/(".+?(?<!\\)") |(\S+?) /,
|
||||
getQuotedRunRegex(),
|
||||
],
|
||||
className: {
|
||||
1: 'variable.language',
|
||||
@ -362,7 +370,6 @@ export class SlashCommandParser {
|
||||
;
|
||||
if (childClosure !== null) return null;
|
||||
const macro = this.macroIndex.findLast(it=>it.start <= index && it.end >= index);
|
||||
console.log(macro);
|
||||
if (macro) {
|
||||
const frag = document.createRange().createContextualFragment(await (await fetch('/scripts/templates/macros.html')).text());
|
||||
const options = [...frag.querySelectorAll('ul:nth-of-type(2n+1) > li')].map(li=>new MacroAutoCompleteOption(
|
||||
@ -821,6 +828,7 @@ export class SlashCommandParser {
|
||||
assignment.start = this.index;
|
||||
value = '';
|
||||
}
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseClosure();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
|
@ -174,7 +174,7 @@ async function* parseStreamData(json) {
|
||||
else if (Array.isArray(json.choices)) {
|
||||
const isNotPrimary = json?.choices?.[0]?.index > 0;
|
||||
if (isNotPrimary || json.choices.length === 0) {
|
||||
return null;
|
||||
throw new Error('Not a primary swipe');
|
||||
}
|
||||
|
||||
if (typeof json.choices[0].text === 'string' && json.choices[0].text.length > 0) {
|
||||
@ -271,7 +271,7 @@ export class SmoothEventSourceStream extends EventSourceStream {
|
||||
hasFocus && await eventSource.emit(event_types.SMOOTH_STREAM_TOKEN_RECEIVED, parsed.chunk);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Smooth Streaming parsing error', error);
|
||||
console.debug('Smooth Streaming parsing error', error);
|
||||
controller.enqueue(event);
|
||||
}
|
||||
},
|
||||
|
@ -14,9 +14,12 @@ import {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
|
||||
|
||||
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
|
||||
import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
|
||||
export {
|
||||
TAG_FOLDER_TYPES,
|
||||
@ -63,7 +66,7 @@ export const tag_filter_types = {
|
||||
const ACTIONABLE_TAGS = {
|
||||
FAV: { id: '1', sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
|
||||
GROUP: { id: '0', sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' },
|
||||
FOLDER: { id: '4', sort_order: 3, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' },
|
||||
FOLDER: { id: '4', sort_order: 3, name: 'Show only folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' },
|
||||
VIEW: { id: '2', sort_order: 4, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
|
||||
HINT: { id: '3', sort_order: 5, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
|
||||
UNFILTER: { id: '5', sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' },
|
||||
@ -174,9 +177,8 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity
|
||||
}
|
||||
|
||||
// Hide folders that have 0 visible sub entities after the first filtering round
|
||||
const alwaysFolder = isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED);
|
||||
if (entity.type === 'tag') {
|
||||
return alwaysFolder || entity.entities.length > 0;
|
||||
return entity.entities.length > 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -322,6 +324,13 @@ function filterByGroups(filterHelper) {
|
||||
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
|
||||
*/
|
||||
function filterByFolder(filterHelper) {
|
||||
if (!power_user.bogus_folders) {
|
||||
$('#bogus_folders').prop('checked', true).trigger('input');
|
||||
onViewTagsListClick();
|
||||
flashHighlight($('#dialogue_popup .tag_as_folder, #dialogue_popup .tag_folder_indicator'));
|
||||
return;
|
||||
}
|
||||
|
||||
const state = toggleTagThreeState($(this));
|
||||
ACTIONABLE_TAGS.FOLDER.filter_state = state;
|
||||
filterHelper.setFilterData(FILTER_TYPES.FOLDER, state);
|
||||
@ -350,7 +359,7 @@ function createTagMapFromList(listElement, key) {
|
||||
* If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`.
|
||||
*
|
||||
* @param {string} key - The key for which to get tags via the tag map
|
||||
* @param {boolean} [sort=true] -
|
||||
* @param {boolean} [sort=true] - Whether the tag list should be sorted
|
||||
* @returns {Tag[]} A list of tags
|
||||
*/
|
||||
function getTagsList(key, sort = true) {
|
||||
@ -456,35 +465,122 @@ export function getTagKeyForEntityElement(element) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag to a given entity
|
||||
* @param {Tag} tag - The tag to add
|
||||
* @param {string|string[]} entityId - The entity to add this tag to. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function)
|
||||
* @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
|
||||
* @returns {boolean} Whether at least one tag was added
|
||||
*/
|
||||
export function addTagToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
|
||||
let result = false;
|
||||
// Add tags to the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = addTagToMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = addTagToMap(tag.id, entityId);
|
||||
}
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
if (tagListSelector) printTagList(tagListSelector, tagListOptions);
|
||||
const inlineSelector = getInlineListSelector();
|
||||
if (inlineSelector) {
|
||||
printTagList($(inlineSelector), tagListOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a given entity
|
||||
* @param {Tag} tag - The tag to remove
|
||||
* @param {string|string[]} entityId - The entity to remove this tag from. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the tag removed too (for example because the add was triggered for that function)
|
||||
* @param {JQuery<HTMLElement>?} [options.tagElement=null] - Optionally a direct html element of the tag to be removed, so it can be removed from the UI
|
||||
* @returns {boolean} Whether at least one tag was removed
|
||||
*/
|
||||
export function removeTagFromEntity(tag, entityId, { tagListSelector = null, tagElement = null } = {}) {
|
||||
let result = false;
|
||||
// Remove tag from the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = removeTagFromMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = removeTagFromMap(tag.id, entityId);
|
||||
}
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We don't reprint the lists, we can just remove the html elements from them.
|
||||
if (tagListSelector) {
|
||||
const $selector = (typeof tagListSelector === 'string') ? $(tagListSelector) : tagListSelector;
|
||||
$selector.find(`.tag[id="${tag.id}"]`).remove();
|
||||
}
|
||||
if (tagElement) tagElement.remove();
|
||||
$(`${getInlineListSelector()} .tag[id="${tag.id}"]`).remove();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag from a given character. If no character is provided, adds it from the currently active one.
|
||||
* @param {string} tagId - The id of the tag
|
||||
* @param {string} characterId - The id/key of the character or group
|
||||
* @returns {boolean} Whether the tag was added or not
|
||||
*/
|
||||
function addTagToMap(tagId, characterId = null) {
|
||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [tagId];
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (tag_map[key].includes(tagId))
|
||||
return false;
|
||||
|
||||
tag_map[key].push(tagId);
|
||||
tag_map[key] = tag_map[key].filter(onlyUnique);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a given character. If no character is provided, removes it from the currently active one.
|
||||
* @param {string} tagId - The id of the tag
|
||||
* @param {string} characterId - The id/key of the character or group
|
||||
* @returns {boolean} Whether the tag was removed or not
|
||||
*/
|
||||
function removeTagFromMap(tagId, characterId = null) {
|
||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [];
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
const indexOf = tag_map[key].indexOf(tagId);
|
||||
tag_map[key].splice(indexOf, 1);
|
||||
return indexOf !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -528,24 +624,7 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
|
||||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
if (characterIds) {
|
||||
characterIds.forEach((characterId) => addTagToMap(tag.id, characterId));
|
||||
} else {
|
||||
addTagToMap(tag.id);
|
||||
}
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
printTagList(listSelector, tagListOptions);
|
||||
const inlineSelector = getInlineListSelector();
|
||||
if (inlineSelector) {
|
||||
printTagList($(inlineSelector), tagListOptions);
|
||||
}
|
||||
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
|
||||
|
||||
// need to return false to keep the input clear
|
||||
return false;
|
||||
@ -628,6 +707,7 @@ function createNewTag(tagName) {
|
||||
create_date: Date.now(),
|
||||
};
|
||||
tags.push(tag);
|
||||
console.debug('Created new tag', tag.name, 'with id', tag.id);
|
||||
return tag;
|
||||
}
|
||||
|
||||
@ -883,8 +963,9 @@ function printTagFilters(type = tag_filter_types.character) {
|
||||
const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR;
|
||||
$(FILTER_SELECTOR).empty();
|
||||
|
||||
// Print all action tags. (Exclude folder if that setting isn't chosen)
|
||||
const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id);
|
||||
// Print all action tags. (Rework 'Folder' button to some kind of onboarding if no folders are enabled yet)
|
||||
const actionTags = Object.values(ACTIONABLE_TAGS);
|
||||
actionTags.find(x => x == ACTIONABLE_TAGS.FOLDER).name = power_user.bogus_folders ? 'Show only folders' : 'Enable \'Tags as Folder\'\n\nAllows characters to be grouped in folders by their assigned tags.\nTags have to be explicitly chosen as folder to show up.\n\nClick here to start';
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, sort: false, tags: actionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
|
||||
|
||||
const inListActionTags = Object.values(InListActionable);
|
||||
@ -924,8 +1005,8 @@ function updateTagFilterIndicator() {
|
||||
|
||||
function onTagRemoveClick(event) {
|
||||
event.stopPropagation();
|
||||
const tag = $(this).closest('.tag');
|
||||
const tagId = tag.attr('id');
|
||||
const tagElement = $(this).closest('.tag');
|
||||
const tagId = tagElement.attr('id');
|
||||
|
||||
// Check if we are inside the drilldown. If so, we call remove on the bogus folder
|
||||
if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) {
|
||||
@ -934,24 +1015,13 @@ function onTagRemoveClick(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = tags.find(t => t.id === tagId);
|
||||
|
||||
// Optional, check for multiple character ids being present.
|
||||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
tag.remove();
|
||||
|
||||
if (characterIds) {
|
||||
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
|
||||
} else {
|
||||
removeTagFromMap(tagId);
|
||||
}
|
||||
|
||||
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
|
||||
removeTagFromEntity(tag, characterIds, { tagElement: tagElement });
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
@ -977,7 +1047,7 @@ function onGroupCreateClick() {
|
||||
|
||||
export function applyTagsOnCharacterSelect() {
|
||||
//clearTagsFilter();
|
||||
const chid = Number($(this).attr('chid'));
|
||||
const chid = Number(this_chid);
|
||||
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
@ -1453,14 +1523,200 @@ function printViewTagList(empty = true) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerTagsSlashCommands() {
|
||||
/**
|
||||
* Gets the key for char/group for a slash command. If none can be found, a toastr will be shown and null returned.
|
||||
* @param {string?} [charName] The optionally provided char name
|
||||
* @returns {string?} - The char/group key, or null if none found
|
||||
*/
|
||||
function paraGetCharKey(charName) {
|
||||
const entity = charName
|
||||
? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))
|
||||
: (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
|
||||
const key = getTagKeyForEntity(entity);
|
||||
if (!key) {
|
||||
toastr.warning(`Character ${charName} not found.`);
|
||||
return null;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
/**
|
||||
* Gets a tag by its name. Optionally can create the tag if it does not exist.
|
||||
* @param {string} tagName - The name of the tag
|
||||
* @param {object} options - Optional arguments
|
||||
* @param {boolean} [options.allowCreate=false] - Whether a new tag should be created if no tag with the name exists
|
||||
* @returns {Tag?} The tag, or null if not found
|
||||
*/
|
||||
function paraGetTag(tagName, { allowCreate = false } = {}) {
|
||||
if (!tagName) {
|
||||
toastr.warning('Tag name must be provided.');
|
||||
return null;
|
||||
}
|
||||
let tag = tags.find(t => t.name === tagName);
|
||||
if (allowCreate && !tag) {
|
||||
tag = createNewTag(tagName);
|
||||
}
|
||||
if (!tag) {
|
||||
toastr.warning(`Tag ${tagName} not found.`);
|
||||
return null;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
function updateTagsList() {
|
||||
switch (menu_type) {
|
||||
case 'characters':
|
||||
printTagFilters(tag_filter_types.character);
|
||||
printTagFilters(tag_filter_types.group_member);
|
||||
break;
|
||||
case 'character_edit':
|
||||
applyTagsOnCharacterSelect();
|
||||
break;
|
||||
case 'group_edit':
|
||||
select_group_chats(selected_group, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-add',
|
||||
returns: 'true/false - Whether the tag was added or was assigned already',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName, { allowCreate: true });
|
||||
if (!tag) return 'false';
|
||||
const result = addTagToEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Adds a tag to the character. If no character is provided, it adds it to the current character (<code>{{char}}</code>).
|
||||
If the tag doesn't exist, it is created.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-add name="Chloe" scenario</code></pre>
|
||||
will add the tag "scenario" to the character named Chloe.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-remove',
|
||||
returns: 'true/false - Whether the tag was removed or wasn\'t assigned already',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
const result = removeTagFromEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Removes a tag from the character. If no character is provided, it removes it from the current character (<code>{{char}}</code>).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-remove name="Chloe" scenario</code></pre>
|
||||
will remove the tag "scenario" from the character named Chloe.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-exists',
|
||||
returns: 'true/false - Whether the given tag name is assigned to the character',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
return String(tag_map[key].includes(tag.id));
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Checks whether the given tag is assigned to the character. If no character is provided, it checks the current character (<code>{{char}}</code>).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-exists name="Chloe" scenario</code></pre>
|
||||
will return true if the character named Chloe has the tag "scenario".
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-list',
|
||||
returns: 'Comma-separated list of all assigned tags',
|
||||
/** @param {{name: string}} namedArgs @returns {string} */
|
||||
callback: ({ name }) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return '';
|
||||
const tags = getTagsList(key);
|
||||
return tags.map(x => x.name).join(', ');
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Lists all assigned tags of the character. If no character is provided, it uses the current character (<code>{{char}}</code>).
|
||||
<br />
|
||||
Note that there is no special handling for tags containing commas, they will be printed as-is.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-list name="Chloe"</code></pre>
|
||||
could return something like <code>OC, scenario, edited, funny</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
}
|
||||
|
||||
export function initTags() {
|
||||
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
|
||||
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
|
||||
|
||||
$(document).on('click', '#rm_button_create', onCharacterCreateClick);
|
||||
$(document).on('click', '#rm_button_group_chats', onGroupCreateClick);
|
||||
$(document).on('click', '.character_select', applyTagsOnCharacterSelect);
|
||||
$(document).on('click', '.group_select', applyTagsOnGroupSelect);
|
||||
$(document).on('click', '.tag_remove', onTagRemoveClick);
|
||||
$(document).on('input', '.tag_input', onTagInput);
|
||||
$(document).on('click', '.tags_view', onViewTagsListClick);
|
||||
@ -1471,6 +1727,7 @@ export function initTags() {
|
||||
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
|
||||
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
|
||||
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
|
||||
eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
|
||||
|
||||
$(document).on('input', '#dialogue_popup input[name="auto_sort_tags"]', (evt) => {
|
||||
const toggle = $(evt.target).is(':checked');
|
||||
@ -1498,4 +1755,6 @@ export function initTags() {
|
||||
printCharactersDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
registerTagsSlashCommands();
|
||||
}
|
||||
|
@ -11,9 +11,10 @@
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-plug"></i></code><span data-i18n="and select a"> and select a </span><a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank" data-i18n="Chat API">Chat API</a>.</span>
|
||||
</li>
|
||||
<li>
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character"> and pick a character</span>
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character."> and pick a character.</span>
|
||||
</li>
|
||||
</ol>
|
||||
<span data-i18n="You can browse a list of bundled characters in the Download Extensions & Assets menu within">You can browse a list of bundled characters in the <i>Download Extensions & Assets</i> menu within </span><code><i class="fa-solid fa-cubes"></i></code><span>.</span>
|
||||
<hr>
|
||||
<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
|
||||
<ul>
|
||||
|
@ -237,7 +237,7 @@ function onMancerModelSelect() {
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
|
||||
const limits = mancerModels.find(x => x.id === modelId)?.limits;
|
||||
setGenerationParamsFromPreset({ max_length: limits.context, genamt: limits.completion }, true);
|
||||
setGenerationParamsFromPreset({ max_length: limits.context });
|
||||
}
|
||||
|
||||
function onTogetherModelSelect() {
|
||||
|
@ -166,7 +166,7 @@ export let textgenerationwebui_banned_in_macros = [];
|
||||
export let textgenerationwebui_presets = [];
|
||||
export let textgenerationwebui_preset_names = [];
|
||||
|
||||
const setting_names = [
|
||||
export const setting_names = [
|
||||
'temp',
|
||||
'temperature_last',
|
||||
'rep_pen',
|
||||
@ -659,7 +659,7 @@ jQuery(function () {
|
||||
'no_repeat_ngram_size_textgenerationwebui': 0,
|
||||
'min_length_textgenerationwebui': 0,
|
||||
'num_beams_textgenerationwebui': 1,
|
||||
'length_penalty_textgenerationwebui': 0,
|
||||
'length_penalty_textgenerationwebui': 1,
|
||||
'penalty_alpha_textgenerationwebui': 0,
|
||||
'typical_p_textgenerationwebui': 1, // Added entry
|
||||
'guidance_scale_textgenerationwebui': 1,
|
||||
@ -991,7 +991,7 @@ export function getTextGenModel() {
|
||||
}
|
||||
|
||||
export function isJsonSchemaSupported() {
|
||||
return settings.type === TABBY && main_api === 'textgenerationwebui';
|
||||
return [TABBY, LLAMACPP].includes(settings.type) && main_api === 'textgenerationwebui';
|
||||
}
|
||||
|
||||
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
|
||||
@ -1065,7 +1065,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
|
||||
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
|
||||
'grammar_string': settings.grammar_string,
|
||||
'json_schema': settings.type === TABBY ? settings.json_schema : undefined,
|
||||
'json_schema': [TABBY, LLAMACPP].includes(settings.type) ? settings.json_schema : undefined,
|
||||
// llama.cpp aliases. In case someone wants to use LM Studio as Text Completion API
|
||||
'repeat_penalty': settings.rep_pen,
|
||||
'tfs_z': settings.tfs,
|
||||
@ -1150,5 +1150,15 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
|
||||
eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
|
||||
|
||||
// Grammar conflicts with with json_schema
|
||||
if (settings.type === LLAMACPP) {
|
||||
if (params.json_schema && Object.keys(params.json_schema).length > 0) {
|
||||
delete params.grammar_string;
|
||||
delete params.grammar;
|
||||
} else {
|
||||
delete params.json_schema;
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
@ -732,6 +732,24 @@ export function isDataURL(str) {
|
||||
return regex.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of an image from a data URL.
|
||||
* @param {string} dataUrl Image data URL
|
||||
* @returns {Promise<{ width: number, height: number }>} Image size
|
||||
*/
|
||||
export function getImageSizeFromDataURL(dataUrl) {
|
||||
const image = new Image();
|
||||
image.src = dataUrl;
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = function () {
|
||||
resolve({ width: image.width, height: image.height });
|
||||
};
|
||||
image.onerror = function () {
|
||||
reject(new Error('Failed to load image'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getCharaFilename(chid) {
|
||||
const context = getContext();
|
||||
const fileName = context.characters[chid ?? context.characterId].avatar;
|
||||
@ -773,6 +791,29 @@ export function escapeRegex(string) {
|
||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a regular expression from a string.
|
||||
* @param {string} input The input string.
|
||||
* @returns {RegExp} The regular expression instance.
|
||||
* @copyright Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
|
||||
*/
|
||||
export function regexFromString(input) {
|
||||
try {
|
||||
// Parse input
|
||||
var m = input.match(/(\/?)(.+)\1([a-z]*)/i);
|
||||
|
||||
// Invalid flags
|
||||
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
|
||||
return RegExp(input);
|
||||
}
|
||||
|
||||
// Create the regular expression
|
||||
return new RegExp(m[2], m[3]);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export class Stopwatch {
|
||||
/**
|
||||
* Initializes a Stopwatch class.
|
||||
@ -1449,3 +1490,166 @@ export function includesIgnoreCaseAndAccents(text, searchTerm) {
|
||||
// Check if the normalized text includes the normalized search term
|
||||
return normalizedText.includes(normalizedSearchTerm);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Select2Option The option object for select2 controls
|
||||
* @property {string} id - The unique ID inside this select
|
||||
* @property {string} text - The text for this option
|
||||
* @property {number?} [count] - Optionally show the count how often that option was chosen already
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a unique hash as ID for a select2 option text
|
||||
*
|
||||
* @param {string} option - The option
|
||||
* @returns {string} A hashed version of that option
|
||||
*/
|
||||
export function getSelect2OptionId(option) {
|
||||
return String(getStringHash(option));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the select2 options by adding not existing one and optionally selecting them
|
||||
*
|
||||
* @param {JQuery<HTMLElement>} element - The "select" element to add the options to
|
||||
* @param {string[]|Select2Option[]} items - The option items to build, add or select
|
||||
* @param {object} [options] - Optional arguments
|
||||
* @param {boolean} [options.select=false] - Whether the options should be selected right away
|
||||
* @param {object} [options.changeEventArgs=null] - Optional event args being passed into the "change" event when its triggered because a new options is selected
|
||||
*/
|
||||
export function select2ModifyOptions(element, items, { select = false, changeEventArgs = null } = {}) {
|
||||
if (!items.length) return;
|
||||
/** @type {Select2Option[]} */
|
||||
const dataItems = items.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||
|
||||
const existingValues = [];
|
||||
dataItems.forEach(item => {
|
||||
// Set the value, creating a new option if necessary
|
||||
if (element.find('option[value=\'' + item.id + '\']').length) {
|
||||
if (select) existingValues.push(item.id);
|
||||
} else {
|
||||
// Create a DOM Option and optionally pre-select by default
|
||||
var newOption = new Option(item.text, item.id, select, select);
|
||||
// Append it to the select
|
||||
element.append(newOption);
|
||||
if (select) element.trigger('change', changeEventArgs);
|
||||
}
|
||||
if (existingValues.length) element.val(existingValues).trigger('change', changeEventArgs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ajax settings that can be used on the select2 ajax property to dynamically get the data.
|
||||
* Can be used on a single global array, querying data from the server or anything similar.
|
||||
*
|
||||
* @param {function():Select2Option[]} dataProvider - The provider/function to retrieve the data - can be as simple as "() => myData" for arrays
|
||||
* @return {{transport: (params, success, failure) => any}} The ajax object with the transport function to use on the select2 ajax property
|
||||
*/
|
||||
export function dynamicSelect2DataViaAjax(dataProvider) {
|
||||
function dynamicSelect2DataTransport(params, success, failure) {
|
||||
var items = dataProvider();
|
||||
// fitering if params.data.q available
|
||||
if (params.data && params.data.q) {
|
||||
items = items.filter(function (item) {
|
||||
return includesIgnoreCaseAndAccents(item.text, params.data.q);
|
||||
});
|
||||
}
|
||||
var promise = new Promise(function (resolve, reject) {
|
||||
resolve({ results: items });
|
||||
});
|
||||
promise.then(success);
|
||||
promise.catch(failure);
|
||||
}
|
||||
const ajax = {
|
||||
transport: dynamicSelect2DataTransport,
|
||||
};
|
||||
return ajax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given control is a select2 choice element - meaning one of the results being displayed in the select multi select box
|
||||
* @param {JQuery<HTMLElement>|HTMLElement} element - The element to check
|
||||
* @returns {boolean} Whether this is a choice element
|
||||
*/
|
||||
export function isSelect2ChoiceElement(element) {
|
||||
const $element = $(element);
|
||||
return ($element.hasClass('select2-selection__choice__display') || $element.parents('.select2-selection__choice__display').length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a 'click' event handler to the choice elements of a select2 multi-select control
|
||||
*
|
||||
* @param {JQuery<HTMLElement>} control The original control the select2 was applied to
|
||||
* @param {function(HTMLElement):void} action - The action to execute when a choice element is clicked
|
||||
* @param {object} options - Optional parameters
|
||||
* @param {boolean} [options.buttonStyle=false] - Whether the choices should be styles as a clickable button with color and hover transition, instead of just changed cursor
|
||||
* @param {boolean} [options.closeDrawer=false] - Whether the drawer should be closed and focus removed after the choice item was clicked
|
||||
* @param {boolean} [options.openDrawer=false] - Whether the drawer should be opened, even if this click would normally close it
|
||||
*/
|
||||
export function select2ChoiceClickSubscribe(control, action, { buttonStyle = false, closeDrawer = false, openDrawer = false } = {}) {
|
||||
// Add class for styling (hover color, changed cursor, etc)
|
||||
control.addClass('select2_choice_clickable');
|
||||
if (buttonStyle) control.addClass('select2_choice_clickable_buttonstyle');
|
||||
|
||||
// Get the real container below and create a click handler on that one
|
||||
const select2Container = control.next('span.select2-container');
|
||||
select2Container.on('click', function (event) {
|
||||
const isChoice = isSelect2ChoiceElement(event.target);
|
||||
if (isChoice) {
|
||||
event.preventDefault();
|
||||
|
||||
// select2 still bubbles the event to open the dropdown. So we close it here and remove focus if we want that
|
||||
if (closeDrawer) {
|
||||
control.select2('close');
|
||||
setTimeout(() => select2Container.find('textarea').trigger('blur'), debounce_timeout.quick);
|
||||
}
|
||||
if (openDrawer) {
|
||||
control.select2('open');
|
||||
}
|
||||
|
||||
// Now execute the actual action that was subscribed
|
||||
action(event.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies syntax highlighting to a given regex string by generating HTML with classes
|
||||
*
|
||||
* @param {string} regexStr - The javascript compatible regex string
|
||||
* @returns {string} The html representation of the highlighted regex
|
||||
*/
|
||||
export function highlightRegex(regexStr) {
|
||||
// Function to escape HTML special characters for safety
|
||||
const escapeHtml = (str) => str.replace(/[&<>"']/g, match => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', '\'': ''',
|
||||
})[match]);
|
||||
|
||||
// Replace special characters with their HTML-escaped forms
|
||||
regexStr = escapeHtml(regexStr);
|
||||
|
||||
// Patterns that we want to highlight only if they are not escaped
|
||||
const patterns = {
|
||||
brackets: /(?<!\\)\[.*?\]/g, // Non-escaped squary brackets
|
||||
quantifiers: /(?<!\\)[*+?{}]/g, // Non-escaped quantifiers
|
||||
operators: /(?<!\\)[|.^$()]/g, // Non-escaped operators like | and ()
|
||||
specialChars: /\\./g,
|
||||
flags: /(?<=\/)([gimsuy]*)$/g, // Match trailing flags
|
||||
delimiters: /^\/|(?<![\\<])\//g, // Match leading or trailing delimiters
|
||||
};
|
||||
|
||||
// Function to replace each pattern with a highlighted HTML span
|
||||
const wrapPattern = (pattern, className) => {
|
||||
regexStr = regexStr.replace(pattern, match => `<span class="${className}">${match}</span>`);
|
||||
};
|
||||
|
||||
// Apply highlighting patterns
|
||||
wrapPattern(patterns.brackets, 'regex-brackets');
|
||||
wrapPattern(patterns.quantifiers, 'regex-quantifier');
|
||||
wrapPattern(patterns.operators, 'regex-operator');
|
||||
wrapPattern(patterns.specialChars, 'regex-special');
|
||||
wrapPattern(patterns.flags, 'regex-flags');
|
||||
wrapPattern(patterns.delimiters, 'regex-delimiter');
|
||||
|
||||
return `<span class="regex-highlight">${regexStr}</span>`;
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
|
||||
import { extension_settings, saveMetadataDebounced } from './extensions.js';
|
||||
import { executeSlashCommands } from './slash-commands.js';
|
||||
import { executeSlashCommands, executeSlashCommandsWithOptions } from './slash-commands.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
|
||||
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||||
import { isFalseBoolean } from './utils.js';
|
||||
|
||||
@ -314,61 +316,117 @@ function listVariablesCallback() {
|
||||
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
|
||||
}
|
||||
|
||||
async function whileCallback(args, command) {
|
||||
/**
|
||||
*
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args
|
||||
* @param {(string|SlashCommandClosure)[]} value
|
||||
*/
|
||||
async function whileCallback(args, value) {
|
||||
const isGuardOff = isFalseBoolean(args.guard);
|
||||
const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS;
|
||||
/**@type {string|SlashCommandClosure} */
|
||||
let command;
|
||||
if (value) {
|
||||
if (value[0] instanceof SlashCommandClosure) {
|
||||
command = value[0];
|
||||
} else {
|
||||
command = value.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
let commandResult;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const { a, b, rule } = parseBooleanOperands(args);
|
||||
const result = evalBoolean(rule, a, b);
|
||||
|
||||
if (result && command) {
|
||||
if (command instanceof SlashCommandClosure) await command.execute();
|
||||
else await executeSubCommands(command, args._scope, args._parserFlags);
|
||||
if (command instanceof SlashCommandClosure) {
|
||||
commandResult = await command.execute();
|
||||
} else {
|
||||
commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
|
||||
}
|
||||
if (commandResult.isAborted) break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (commandResult) {
|
||||
return commandResult.pipe;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args
|
||||
* @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value
|
||||
* @returns
|
||||
*/
|
||||
async function timesCallback(args, value) {
|
||||
let repeats;
|
||||
let command;
|
||||
if (Array.isArray(value)) {
|
||||
[repeats, command] = value;
|
||||
[repeats, ...command] = value;
|
||||
if (command[0] instanceof SlashCommandClosure) {
|
||||
command = command[0];
|
||||
} else {
|
||||
command = command.join(' ');
|
||||
}
|
||||
} else {
|
||||
[repeats, ...command] = value.split(' ');
|
||||
[repeats, ...command] = /**@type {string}*/(value).split(' ');
|
||||
command = command.join(' ');
|
||||
}
|
||||
const isGuardOff = isFalseBoolean(args.guard);
|
||||
const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
|
||||
let result;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
/**@type {SlashCommandClosureResult}*/
|
||||
if (command instanceof SlashCommandClosure) {
|
||||
command.scope.setMacro('timesIndex', i);
|
||||
await command.execute();
|
||||
result = await command.execute();
|
||||
}
|
||||
else {
|
||||
await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i), args._scope, args._parserFlags);
|
||||
result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags, args._abortController);
|
||||
}
|
||||
if (result.isAborted) break;
|
||||
}
|
||||
|
||||
return '';
|
||||
return result?.pipe ?? '';
|
||||
}
|
||||
|
||||
async function ifCallback(args, command) {
|
||||
/**
|
||||
*
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args
|
||||
* @param {(string|SlashCommandClosure)[]} value
|
||||
*/
|
||||
async function ifCallback(args, value) {
|
||||
const { a, b, rule } = parseBooleanOperands(args);
|
||||
const result = evalBoolean(rule, a, b);
|
||||
|
||||
if (result && command) {
|
||||
if (command instanceof SlashCommandClosure) return (await command.execute()).pipe;
|
||||
return await executeSubCommands(command, args._scope, args._parserFlags);
|
||||
} else if (!result && args.else && ((typeof args.else === 'string' && args.else !== '') || args.else instanceof SlashCommandClosure)) {
|
||||
if (args.else instanceof SlashCommandClosure) return (await args.else.execute(args._scope)).pipe;
|
||||
return await executeSubCommands(args.else, args._scope, args._parserFlags);
|
||||
/**@type {string|SlashCommandClosure} */
|
||||
let command;
|
||||
if (value) {
|
||||
if (value[0] instanceof SlashCommandClosure) {
|
||||
command = value[0];
|
||||
} else {
|
||||
command = value.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
let commandResult;
|
||||
if (result && command) {
|
||||
if (command instanceof SlashCommandClosure) return (await command.execute()).pipe;
|
||||
commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
|
||||
} else if (!result && args.else && ((typeof args.else === 'string' && args.else !== '') || args.else instanceof SlashCommandClosure)) {
|
||||
if (args.else instanceof SlashCommandClosure) return (await args.else.execute()).pipe;
|
||||
commandResult = await executeSubCommands(args.else, args._scope, args._parserFlags, args._abortController);
|
||||
}
|
||||
|
||||
if (commandResult) {
|
||||
return commandResult.pipe;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -511,20 +569,25 @@ function evalBoolean(rule, a, b) {
|
||||
/**
|
||||
* Executes a slash command from a string (may be enclosed in quotes) and returns the result.
|
||||
* @param {string} command Command to execute. May contain escaped macro and batch separators.
|
||||
* @returns {Promise<string>} Pipe result
|
||||
* @param {SlashCommandScope} [scope] The scope to use.
|
||||
* @param {{[id:PARSER_FLAG]:boolean}} [parserFlags] The parser flags to use.
|
||||
* @param {SlashCommandAbortController} [abortController] The abort controller to use.
|
||||
* @returns {Promise<SlashCommandClosureResult>} Closure execution result
|
||||
*/
|
||||
async function executeSubCommands(command, scope = null, parserFlags = null) {
|
||||
async function executeSubCommands(command, scope = null, parserFlags = null, abortController = null) {
|
||||
if (command.startsWith('"') && command.endsWith('"')) {
|
||||
command = command.slice(1, -1);
|
||||
}
|
||||
|
||||
const result = await executeSlashCommands(command, true, scope, true, parserFlags);
|
||||
const result = await executeSlashCommandsWithOptions(command, {
|
||||
handleExecutionErrors: false,
|
||||
handleParserErrors: false,
|
||||
parserFlags,
|
||||
scope,
|
||||
abortController: abortController ?? new SlashCommandAbortController(),
|
||||
});
|
||||
|
||||
if (!result || typeof result !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return result?.pipe || '';
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1066,6 +1129,7 @@ export function registerVariableCommands() {
|
||||
'command to execute if true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], true,
|
||||
),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: `
|
||||
<div>
|
||||
Compares the value of the left operand <code>a</code> with the value of the right operand <code>b</code>,
|
||||
@ -1132,6 +1196,7 @@ export function registerVariableCommands() {
|
||||
'command to execute while true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], true,
|
||||
),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: `
|
||||
<div>
|
||||
Compares the value of the left operand <code>a</code> with the value of the right operand <code>b</code>,
|
||||
@ -1158,7 +1223,7 @@ export function registerVariableCommands() {
|
||||
<strong>Examples:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=let "/addvar key=i 1"</code></pre>
|
||||
<pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>
|
||||
adds 1 to the value of "i" until it reaches 10.
|
||||
</li>
|
||||
</ul>
|
||||
@ -1184,6 +1249,7 @@ export function registerVariableCommands() {
|
||||
true,
|
||||
),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: `
|
||||
<div>
|
||||
Execute any valid slash command enclosed in quotes <code>repeats</code> number of times.
|
||||
@ -1592,7 +1658,7 @@ export function registerVariableCommands() {
|
||||
returns: 'length of the provided value',
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], true
|
||||
'value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight } from './utils.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean } from './utils.js';
|
||||
import { extension_settings, getContext } from './extensions.js';
|
||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
@ -69,7 +69,7 @@ const saveSettingsDebounced = debounce(() => {
|
||||
saveSettings();
|
||||
}, debounce_timeout.relaxed);
|
||||
const sortFn = (a, b) => b.order - a.order;
|
||||
let updateEditor = (navigation) => { console.debug('Triggered WI navigation', navigation); };
|
||||
let updateEditor = (navigation, flashOnNav = true) => { console.debug('Triggered WI navigation', navigation, flashOnNav); };
|
||||
|
||||
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
|
||||
const worldInfoFilter = new FilterHelper(() => updateEditor());
|
||||
@ -197,6 +197,13 @@ class WorldInfoBuffer {
|
||||
* @returns {boolean} True if the string was found in the buffer
|
||||
*/
|
||||
matchKeys(haystack, needle, entry) {
|
||||
// If the needle is a regex, we do regex pattern matching and override all the other options
|
||||
const keyRegex = parseRegexFromString(needle);
|
||||
if (keyRegex) {
|
||||
return keyRegex.test(haystack);
|
||||
}
|
||||
|
||||
// Otherwise we do normal matching of plaintext with the chosen entry settings
|
||||
const transformedString = this.#transformString(needle, entry);
|
||||
const matchWholeWords = entry.matchWholeWords ?? world_info_match_whole_words;
|
||||
|
||||
@ -541,6 +548,19 @@ function registerWorldInfoSlashCommands() {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof newEntryTemplate[field] === 'boolean') {
|
||||
const isTrue = isTrueBoolean(value);
|
||||
const isFalse = isFalseBoolean(value);
|
||||
|
||||
if (isTrue) {
|
||||
value = String(true);
|
||||
}
|
||||
|
||||
if (isFalse) {
|
||||
value = String(false);
|
||||
}
|
||||
}
|
||||
|
||||
const fuse = new Fuse(entries, {
|
||||
keys: [{ name: field, weight: 1 }],
|
||||
includeScore: true,
|
||||
@ -984,8 +1004,39 @@ function nullWorldInfo() {
|
||||
toastr.info('Create or import a new World Info file first.', 'World Info is not set', { timeOut: 10000, preventDuplicates: true });
|
||||
}
|
||||
|
||||
function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
updateEditor = (navigation) => displayWorldEntries(name, data, navigation);
|
||||
/** @type {Select2Option[]} Cache all keys as selectable dropdown option */
|
||||
const worldEntryKeyOptionsCache = [];
|
||||
|
||||
/**
|
||||
* Update the cache and all select options for the keys with new values to display
|
||||
* @param {string[]|Select2Option[]} keyOptions - An array of options to update
|
||||
* @param {object} options - Optional arguments
|
||||
* @param {boolean?} [options.remove=false] - Whether the option was removed, so the count should be reduced - otherwise it'll be increased
|
||||
* @param {boolean?} [options.reset=false] - Whether the cache should be reset. Reset will also not trigger update of the controls, as we expect them to be redrawn anyway
|
||||
*/
|
||||
function updateWorldEntryKeyOptionsCache(keyOptions, { remove = false, reset = false } = {}) {
|
||||
if (!keyOptions.length) return;
|
||||
/** @type {Select2Option[]} */
|
||||
const options = keyOptions.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||
if (reset) worldEntryKeyOptionsCache.length = 0;
|
||||
options.forEach(option => {
|
||||
// Update the cache list
|
||||
let cachedEntry = worldEntryKeyOptionsCache.find(x => x.id == option.id);
|
||||
if (cachedEntry) {
|
||||
cachedEntry.count += !remove ? 1 : -1;
|
||||
} else if (!remove) {
|
||||
worldEntryKeyOptionsCache.push(option);
|
||||
cachedEntry = option;
|
||||
cachedEntry.count = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by count DESC and then alphabetically
|
||||
worldEntryKeyOptionsCache.sort((a, b) => b.count - a.count || a.text.localeCompare(b.text));
|
||||
}
|
||||
|
||||
function displayWorldEntries(name, data, navigation = navigation_option.none, flashOnNav = true) {
|
||||
updateEditor = (navigation, flashOnNav = true) => displayWorldEntries(name, data, navigation, flashOnNav);
|
||||
|
||||
const worldEntriesList = $('#world_popup_entries_list');
|
||||
|
||||
@ -1020,6 +1071,10 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
||||
entriesArray = sortEntries(entriesArray);
|
||||
|
||||
// Cache keys
|
||||
const keys = entriesArray.flatMap(entry => [...entry.key, ...entry.keysecondary]);
|
||||
updateWorldEntryKeyOptionsCache(keys, { reset: true });
|
||||
|
||||
// Run the callback for printing this
|
||||
typeof callback === 'function' && callback(entriesArray);
|
||||
return entriesArray;
|
||||
@ -1036,7 +1091,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
$('#world_info_pagination').pagination({
|
||||
dataSource: getDataArray,
|
||||
pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,
|
||||
sizeChangerOptions: [10, 25, 50, 100],
|
||||
sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
|
||||
showSizeChanger: true,
|
||||
pageRange: 1,
|
||||
pageNumber: startPage,
|
||||
@ -1114,7 +1169,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
const parentOffset = element.parent().offset();
|
||||
const scrollOffset = elementOffset.top - parentOffset.top;
|
||||
$('#WorldInfo').scrollTop(scrollOffset);
|
||||
flashHighlight(element);
|
||||
if (flashOnNav) flashHighlight(element);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1202,9 +1257,10 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
}
|
||||
|
||||
worldEntriesList.sortable({
|
||||
items: '.world_entry',
|
||||
delay: getSortableDelay(),
|
||||
handle: '.drag-handle',
|
||||
stop: async function (event, ui) {
|
||||
stop: async function (_event, _ui) {
|
||||
const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid');
|
||||
const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0;
|
||||
$('#world_popup_entries_list .world_entry').each(function (index) {
|
||||
@ -1234,6 +1290,7 @@ const originalDataKeyMap = {
|
||||
'displayIndex': 'extensions.display_index',
|
||||
'excludeRecursion': 'extensions.exclude_recursion',
|
||||
'preventRecursion': 'extensions.prevent_recursion',
|
||||
'delayUntilRecursion': 'extensions.delay_until_recursion',
|
||||
'selectiveLogic': 'selectiveLogic',
|
||||
'comment': 'comment',
|
||||
'constant': 'constant',
|
||||
@ -1299,6 +1356,139 @@ function deleteOriginalDataValue(data, uid) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {import('./utils.js').Select2Option} Select2Option */
|
||||
|
||||
/**
|
||||
* Splits a given input string that contains one or more keywords or regexes, separated by commas.
|
||||
*
|
||||
* Each part can be a valid regex following the pattern `/myregex/flags` with optional flags. Commmas inside the regex are allowed, slashes have to be escaped like this: `\/`
|
||||
* If a regex doesn't stand alone, it is not treated as a regex.
|
||||
*
|
||||
* @param {string} input - One or multiple keywords or regexes, separated by commas
|
||||
* @returns {string[]} An array of keywords and regexes
|
||||
*/
|
||||
function splitKeywordsAndRegexes(input) {
|
||||
/** @type {string[]} */
|
||||
let keywordsAndRegexes = [];
|
||||
|
||||
// We can make this easy. Instead of writing another function to find and parse regexes,
|
||||
// we gonna utilize the custom tokenizer that also handles the input.
|
||||
// No need for validation here
|
||||
const addFindCallback = (/** @type {Select2Option} */ item) => {
|
||||
keywordsAndRegexes.push(item.text);
|
||||
};
|
||||
|
||||
const { term } = customTokenizer({ _type: 'custom_call', term: input }, undefined, addFindCallback);
|
||||
const finalTerm = term.trim();
|
||||
if (finalTerm) {
|
||||
addFindCallback({ id: getSelect2OptionId(finalTerm), text: finalTerm });
|
||||
}
|
||||
|
||||
return keywordsAndRegexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizer parsing input and splitting it into keywords and regexes
|
||||
*
|
||||
* @param {{_type: string, term: string}} input - The typed input
|
||||
* @param {{options: object}} _selection - The selection even object (?)
|
||||
* @param {function(Select2Option):void} callback - The original callback function to call if an item should be inserted
|
||||
* @returns {{term: string}} - The remaining part that is untokenized in the textbox
|
||||
*/
|
||||
function customTokenizer(input, _selection, callback) {
|
||||
let current = input.term;
|
||||
|
||||
// Go over the input and check the current state, if we can get a token
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
let char = current[i];
|
||||
|
||||
// If a comma is typed, we tokenize the input.
|
||||
// unless we are inside a possible regex, which would allow commas inside
|
||||
if (char === ',') {
|
||||
// We take everything up till now and consider this a token
|
||||
const token = current.slice(0, i).trim();
|
||||
|
||||
// Now how we test if this is a valid regex? And not a finished one, but a half-finished one?
|
||||
// Easy, if someone typed a comma it can't be a delimiter escape.
|
||||
// So we just check if this opening with a slash, and if so, we "close" the regex and try to parse it.
|
||||
// So if we are inside a valid regex, we can't take the token now, we continue processing until the regex is closed,
|
||||
// or this is not a valid regex anymore
|
||||
if (token.startsWith('/') && isValidRegex(token + '/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// So now the comma really means the token is done.
|
||||
// We take the token up till now, and insert it. Empty will be skipped.
|
||||
if (token) {
|
||||
const isRegex = isValidRegex(token);
|
||||
|
||||
// Last chance to check for valid regex again. Because it might have been valid while typing, but now is not valid anymore and contains commas we need to split.
|
||||
if (token.startsWith('/') && !isRegex) {
|
||||
const tokens = token.split(',').map(x => x.trim());
|
||||
tokens.forEach(x => callback({ id: getSelect2OptionId(x), text: x }));
|
||||
} else {
|
||||
callback({ id: getSelect2OptionId(token), text: token });
|
||||
}
|
||||
}
|
||||
|
||||
// Now remove the token from the current input, and the comma too
|
||||
current = current.slice(i + 1);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// At the end, just return the left-over input
|
||||
return { term: current };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string is a valid slash-delimited regex, that can be parsed and executed
|
||||
*
|
||||
* This is a wrapper around `parseRegexFromString`
|
||||
*
|
||||
* @param {string} input - A delimited regex string
|
||||
* @returns {boolean} Whether this would be a valid regex that can be parsed and executed
|
||||
*/
|
||||
function isValidRegex(input) {
|
||||
return parseRegexFromString(input) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a real regex object from a slash-delimited regex string
|
||||
*
|
||||
* This function works with `/` as delimiter, and each occurance of it inside the regex has to be escaped.
|
||||
* Flags are optional, but can only be valid flags supported by JavaScript's `RegExp` (`g`, `i`, `m`, `s`, `u`, `y`).
|
||||
*
|
||||
* @param {string} input - A delimited regex string
|
||||
* @returns {RegExp|null} The regex object, or null if not a valid regex
|
||||
*/
|
||||
function parseRegexFromString(input) {
|
||||
// Extracting the regex pattern and flags
|
||||
let match = input.match(/^\/([\w\W]+?)\/([gimsuy]*)$/);
|
||||
if (!match) {
|
||||
return null; // Not a valid regex format
|
||||
}
|
||||
|
||||
let [, pattern, flags] = match;
|
||||
|
||||
// If we find any unescaped slash delimiter, we also exit out.
|
||||
// JS doesn't care about delimiters inside regex patterns, but for this to be a valid regex outside of our implementation,
|
||||
// we have to make sure that our delimiter is correctly escaped. Or every other engine would fail.
|
||||
if (pattern.match(/(^|[^\\])\//)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now we need to actually unescape the slash delimiters, because JS doesn't care about delimiters
|
||||
pattern = pattern.replace('\\/', '/');
|
||||
|
||||
// Then we return the regex. If it fails, it was invalid syntax.
|
||||
try {
|
||||
return new RegExp(pattern, flags);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getWorldEntry(name, data, entry) {
|
||||
if (!data.entries[entry.uid]) {
|
||||
return;
|
||||
@ -1308,28 +1498,125 @@ function getWorldEntry(name, data, entry) {
|
||||
template.data('uid', entry.uid);
|
||||
template.attr('uid', entry.uid);
|
||||
|
||||
// Init default state of WI Key toggle (=> true)
|
||||
if (typeof power_user.wi_key_input_plaintext === 'undefined') power_user.wi_key_input_plaintext = true;
|
||||
|
||||
/** Function to build the keys input controls @param {string} entryPropName @param {string} originalDataValueName */
|
||||
function enableKeysInput(entryPropName, originalDataValueName) {
|
||||
const isFancyInput = !isMobile() && !power_user.wi_key_input_plaintext;
|
||||
const input = isFancyInput ? template.find(`select[name="${entryPropName}"]`) : template.find(`textarea[name="${entryPropName}"]`);
|
||||
input.data('uid', entry.uid);
|
||||
input.on('click', function (event) {
|
||||
// Prevent closing the drawer on clicking the input
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
function templateStyling(/** @type {Select2Option} */ item, { searchStyle = false } = {}) {
|
||||
const content = $('<span>').addClass('item').text(item.text).attr('title', `${item.text}\n\nClick to edit`);
|
||||
const isRegex = isValidRegex(item.text);
|
||||
if (isRegex) {
|
||||
content.html(highlightRegex(item.text));
|
||||
content.addClass('regex_item').prepend($('<span>').addClass('regex_icon').text('•*').attr('title', 'Regex'));
|
||||
}
|
||||
|
||||
if (searchStyle && item.count) {
|
||||
// Build a wrapping element
|
||||
const wrapper = $('<span>').addClass('result_block')
|
||||
.append(content);
|
||||
wrapper.append($('<span>').addClass('item_count').text(item.count).attr('title', `Used as a key ${item.count} ${item.count != 1 ? 'times' : 'time'} in this lorebook`));
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
if (isFancyInput) {
|
||||
input.select2({
|
||||
ajax: dynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
|
||||
tags: true,
|
||||
tokenSeparators: [','],
|
||||
tokenizer: customTokenizer,
|
||||
placeholder: input.attr('placeholder'),
|
||||
templateResult: item => templateStyling(item, { searchStyle: true }),
|
||||
templateSelection: item => templateStyling(item),
|
||||
});
|
||||
input.on('change', function (_, { skipReset, noSave } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
/** @type {string[]} */
|
||||
const keys = ($(this).select2('data')).map(x => x.text);
|
||||
|
||||
!skipReset && resetScrollHeight(this);
|
||||
if (!noSave) {
|
||||
data.entries[uid][entryPropName] = keys;
|
||||
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
|
||||
saveWorldInfo(name, data);
|
||||
}
|
||||
});
|
||||
input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
|
||||
input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
|
||||
|
||||
select2ChoiceClickSubscribe(input, target => {
|
||||
const key = $(target).text();
|
||||
console.debug('Editing WI key', key);
|
||||
|
||||
// Remove the current key from the actual selection
|
||||
const selected = input.val();
|
||||
if (!Array.isArray(selected)) return;
|
||||
var index = selected.indexOf(getSelect2OptionId(key));
|
||||
if (index > -1) selected.splice(index, 1);
|
||||
input.val(selected).trigger('change');
|
||||
// Manually update the cache, that change event is not gonna trigger it
|
||||
updateWorldEntryKeyOptionsCache([key], { remove: true });
|
||||
|
||||
// We need to "hack" the actual text input into the currently open textarea
|
||||
input.next('span.select2-container').find('textarea')
|
||||
.val(key).trigger('input');
|
||||
}, { openDrawer: true });
|
||||
|
||||
select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
|
||||
}
|
||||
else {
|
||||
// Compatibility with mobile devices. On mobile we need a text input field, not a select option control, so we need its own event handlers
|
||||
template.find(`select[name="${entryPropName}"]`).hide();
|
||||
input.show();
|
||||
|
||||
input.on('input', function (_, { skipReset, noSave } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
const value = String($(this).val());
|
||||
!skipReset && resetScrollHeight(this);
|
||||
if (!noSave) {
|
||||
data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
|
||||
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
|
||||
saveWorldInfo(name, data);
|
||||
}
|
||||
});
|
||||
input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
|
||||
}
|
||||
return { isFancy: isFancyInput, control: input };
|
||||
}
|
||||
|
||||
// key
|
||||
const keyInput = template.find('textarea[name="key"]');
|
||||
keyInput.data('uid', entry.uid);
|
||||
keyInput.on('click', function (event) {
|
||||
// Prevent closing the drawer on clicking the input
|
||||
event.stopPropagation();
|
||||
});
|
||||
const keyInput = enableKeysInput('key', 'keys');
|
||||
|
||||
keyInput.on('input', function (_, { skipReset } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
const value = String($(this).val());
|
||||
!skipReset && resetScrollHeight(this);
|
||||
data.entries[uid].key = value
|
||||
.split(',')
|
||||
.map((x) => x.trim())
|
||||
.filter((x) => x);
|
||||
// keysecondary
|
||||
const keySecondaryInput = enableKeysInput('keysecondary', 'secondary_keys');
|
||||
|
||||
setOriginalDataValue(data, uid, 'keys', data.entries[uid].key);
|
||||
saveWorldInfo(name, data);
|
||||
// draw key input switch button
|
||||
template.find('.switch_input_type_icon').on('click', function () {
|
||||
power_user.wi_key_input_plaintext = !power_user.wi_key_input_plaintext;
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Just redraw the panel
|
||||
const uid = ($(this).parents('.world_entry')).data('uid');
|
||||
updateEditor(uid, false);
|
||||
|
||||
$(`.world_entry[uid="${uid}"] .inline-drawer-icon`).trigger('click');
|
||||
// setTimeout(() => {
|
||||
// }, debounce_timeout.standard);
|
||||
}).each((_, icon) => {
|
||||
$(icon).attr('title', $(icon).data(power_user.wi_key_input_plaintext ? 'tooltip-on' : 'tooltip-off'));
|
||||
$(icon).text($(icon).data(power_user.wi_key_input_plaintext ? 'icon-on' : 'icon-off'));
|
||||
});
|
||||
keyInput.val(entry.key.join(', ')).trigger('input', { skipReset: true });
|
||||
//initScrollHeight(keyInput);
|
||||
|
||||
// logic AND/NOT
|
||||
const selectiveLogicDropdown = template.find('select[name="entryLogicType"]');
|
||||
@ -1458,25 +1745,6 @@ function getWorldEntry(name, data, entry) {
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
|
||||
// keysecondary
|
||||
const keySecondaryInput = template.find('textarea[name="keysecondary"]');
|
||||
keySecondaryInput.data('uid', entry.uid);
|
||||
keySecondaryInput.on('input', function (_, { skipReset } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
const value = String($(this).val());
|
||||
!skipReset && resetScrollHeight(this);
|
||||
data.entries[uid].keysecondary = value
|
||||
.split(',')
|
||||
.map((x) => x.trim())
|
||||
.filter((x) => x);
|
||||
|
||||
setOriginalDataValue(data, uid, 'secondary_keys', data.entries[uid].keysecondary);
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
|
||||
keySecondaryInput.val(entry.keysecondary.join(', ')).trigger('input', { skipReset: true });
|
||||
//initScrollHeight(keySecondaryInput);
|
||||
|
||||
// comment
|
||||
const commentInput = template.find('textarea[name="comment"]');
|
||||
const commentToggle = template.find('input[name="addMemo"]');
|
||||
@ -1539,8 +1807,8 @@ function getWorldEntry(name, data, entry) {
|
||||
if (counter.data('first-run')) {
|
||||
counter.data('first-run', false);
|
||||
countTokensDebounced(counter, contentInput.val());
|
||||
initScrollHeight(keyInput);
|
||||
initScrollHeight(keySecondaryInput);
|
||||
if (!keyInput.isFancy) initScrollHeight(keyInput.control);
|
||||
if (!keySecondaryInput.isFancy) initScrollHeight(keySecondaryInput.control);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1563,11 +1831,11 @@ function getWorldEntry(name, data, entry) {
|
||||
.closest('.world_entry')
|
||||
.find('.keysecondarytextpole');
|
||||
|
||||
const keyprimarytextpole = $(this)
|
||||
const keyprimaryselect = $(this)
|
||||
.closest('.world_entry')
|
||||
.find('.keyprimarytextpole');
|
||||
.find('.keyprimaryselect');
|
||||
|
||||
const keyprimaryHeight = keyprimarytextpole.outerHeight();
|
||||
const keyprimaryHeight = keyprimaryselect.outerHeight();
|
||||
keysecondarytextpole.css('height', keyprimaryHeight + 'px');
|
||||
|
||||
value ? keysecondary.show() : keysecondary.hide();
|
||||
@ -1619,7 +1887,7 @@ function getWorldEntry(name, data, entry) {
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
groupInput.val(entry.group ?? '').trigger('input');
|
||||
setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data)), 1);
|
||||
setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data), { allowMultiple: true }), 1);
|
||||
|
||||
// inclusion priority
|
||||
const groupOverrideInput = template.find('input[name="groupOverride"]');
|
||||
@ -1891,6 +2159,18 @@ function getWorldEntry(name, data, entry) {
|
||||
});
|
||||
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
|
||||
|
||||
// delay until recursion
|
||||
const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]');
|
||||
delayUntilRecursionInput.data('uid', entry.uid);
|
||||
delayUntilRecursionInput.on('input', function () {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).prop('checked');
|
||||
data.entries[uid].delayUntilRecursion = value;
|
||||
setOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input');
|
||||
|
||||
// duplicate button
|
||||
const duplicateButton = template.find('.duplicate_entry_button');
|
||||
duplicateButton.data('uid', entry.uid);
|
||||
@ -2029,11 +2309,15 @@ function getWorldEntry(name, data, entry) {
|
||||
* @returns {(input: any, output: any) => any} Callback function for the autocomplete
|
||||
*/
|
||||
function getInclusionGroupCallback(data) {
|
||||
return function (input, output) {
|
||||
return function (control, input, output) {
|
||||
const uid = $(control).data('uid');
|
||||
const thisGroups = String($(control).val()).split(/,\s*/).filter(x => x).map(x => x.toLowerCase());
|
||||
const groups = new Set();
|
||||
for (const entry of Object.values(data.entries)) {
|
||||
// Skip the groups of this entry, because auto-complete should only suggest the ones that are already available on other entries
|
||||
if (entry.uid == uid) continue;
|
||||
if (entry.group) {
|
||||
groups.add(String(entry.group));
|
||||
entry.group.split(/,\s*/).filter(x => x).forEach(x => groups.add(x));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2041,20 +2325,19 @@ function getInclusionGroupCallback(data) {
|
||||
haystack.sort((a, b) => a.localeCompare(b));
|
||||
const needle = input.term.toLowerCase();
|
||||
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
|
||||
const result = haystack.filter(x => x.toLowerCase().includes(needle));
|
||||
|
||||
if (input.term && !hasExactMatch) {
|
||||
result.unshift(input.term);
|
||||
}
|
||||
const result = haystack.filter(x => x.toLowerCase().includes(needle) && (!thisGroups.includes(x) || hasExactMatch && thisGroups.filter(g => g == x).length == 1));
|
||||
|
||||
output(result);
|
||||
};
|
||||
}
|
||||
|
||||
function getAutomationIdCallback(data) {
|
||||
return function (input, output) {
|
||||
return function (control, input, output) {
|
||||
const uid = $(control).data('uid');
|
||||
const ids = new Set();
|
||||
for (const entry of Object.values(data.entries)) {
|
||||
// Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries
|
||||
if (entry.uid == uid) continue;
|
||||
if (entry.automationId) {
|
||||
ids.add(String(entry.automationId));
|
||||
}
|
||||
@ -2070,36 +2353,53 @@ function getAutomationIdCallback(data) {
|
||||
const haystack = Array.from(ids);
|
||||
haystack.sort((a, b) => a.localeCompare(b));
|
||||
const needle = input.term.toLowerCase();
|
||||
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
|
||||
const result = haystack.filter(x => x.toLowerCase().includes(needle));
|
||||
|
||||
if (input.term && !hasExactMatch) {
|
||||
result.unshift(input.term);
|
||||
}
|
||||
|
||||
output(result);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an autocomplete for the inclusion group.
|
||||
* @param {JQuery<HTMLElement>} input Input element to attach the autocomplete to
|
||||
* @param {(input: any, output: any) => any} callback Source data callbacks
|
||||
* @param {JQuery<HTMLElement>} input - Input element to attach the autocomplete to
|
||||
* @param {(control: JQuery<HTMLElement>, input: any, output: any) => any} callback - Source data callbacks
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {boolean} [options.allowMultiple=false] - Whether to allow multiple comma-separated values
|
||||
*/
|
||||
function createEntryInputAutocomplete(input, callback) {
|
||||
function createEntryInputAutocomplete(input, callback, { allowMultiple = false } = {}) {
|
||||
const handleSelect = (event, ui) => {
|
||||
// Prevent default autocomplete select, so we can manually set the value
|
||||
event.preventDefault();
|
||||
if (!allowMultiple) {
|
||||
$(input).val(ui.item.value).trigger('input').trigger('blur');
|
||||
} else {
|
||||
var terms = String($(input).val()).split(/,\s*/);
|
||||
terms.pop(); // remove the current input
|
||||
terms.push(ui.item.value); // add the selected item
|
||||
$(input).val(terms.filter(x => x).join(', ')).trigger('input').trigger('blur');
|
||||
}
|
||||
};
|
||||
|
||||
$(input).autocomplete({
|
||||
minLength: 0,
|
||||
source: callback,
|
||||
select: function (event, ui) {
|
||||
$(input).val(ui.item.value).trigger('input').trigger('blur');
|
||||
source: function (request, response) {
|
||||
if (!allowMultiple) {
|
||||
callback(input, request, response);
|
||||
} else {
|
||||
const term = request.term.split(/,\s*/).pop();
|
||||
request.term = term;
|
||||
callback(input, request, response);
|
||||
}
|
||||
},
|
||||
select: handleSelect,
|
||||
});
|
||||
|
||||
$(input).on('focus click', function () {
|
||||
$(input).autocomplete('search', String($(input).val()));
|
||||
$(input).autocomplete('search', allowMultiple ? String($(input).val()).split(/,\s*/).pop() : $(input).val());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Duplicated a WI entry by copying all of its properties and assigning a new uid
|
||||
* @param {*} data - The data of the book
|
||||
@ -2152,6 +2452,8 @@ const newEntryTemplate = {
|
||||
position: 0,
|
||||
disable: false,
|
||||
excludeRecursion: false,
|
||||
preventRecursion: false,
|
||||
delayUntilRecursion: false,
|
||||
probability: 100,
|
||||
useProbability: true,
|
||||
depth: DEFAULT_DEPTH,
|
||||
@ -2166,7 +2468,7 @@ const newEntryTemplate = {
|
||||
role: 0,
|
||||
};
|
||||
|
||||
function createWorldInfoEntry(name, data) {
|
||||
function createWorldInfoEntry(_name, data) {
|
||||
const newUid = getFreeWorldEntryUid(data);
|
||||
|
||||
if (!Number.isInteger(newUid)) {
|
||||
@ -2519,7 +2821,7 @@ async function checkWorldInfo(chat, maxContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) {
|
||||
if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion) || (count == 1 && entry.delayUntilRecursion)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -2792,10 +3094,12 @@ function filterGroupsByScoring(groups, buffer, removeEntry) {
|
||||
function filterByInclusionGroups(newEntries, allActivatedEntries, buffer) {
|
||||
console.debug('-- INCLUSION GROUP CHECKS BEGIN --');
|
||||
const grouped = newEntries.filter(x => x.group).reduce((acc, item) => {
|
||||
if (!acc[item.group]) {
|
||||
acc[item.group] = [];
|
||||
}
|
||||
acc[item.group].push(item);
|
||||
item.group.split(/,\s*/).filter(x => x).forEach(group => {
|
||||
if (!acc[group]) {
|
||||
acc[group] = [];
|
||||
}
|
||||
acc[group].push(item);
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@ -2887,9 +3191,10 @@ function convertAgnaiMemoryBook(inputObj) {
|
||||
disable: !entry.enabled,
|
||||
addMemo: !!entry.name,
|
||||
excludeRecursion: false,
|
||||
delayUntilRecursion: false,
|
||||
displayIndex: index,
|
||||
probability: null,
|
||||
useProbability: false,
|
||||
probability: 100,
|
||||
useProbability: true,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
groupWeight: DEFAULT_WEIGHT,
|
||||
@ -2925,9 +3230,10 @@ function convertRisuLorebook(inputObj) {
|
||||
disable: false,
|
||||
addMemo: true,
|
||||
excludeRecursion: false,
|
||||
delayUntilRecursion: false,
|
||||
displayIndex: index,
|
||||
probability: entry.activationPercent ?? null,
|
||||
useProbability: entry.activationPercent ?? false,
|
||||
probability: entry.activationPercent ?? 100,
|
||||
useProbability: entry.activationPercent ?? true,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
groupWeight: DEFAULT_WEIGHT,
|
||||
@ -2968,9 +3274,10 @@ function convertNovelLorebook(inputObj) {
|
||||
disable: !entry.enabled,
|
||||
addMemo: addMemo,
|
||||
excludeRecursion: false,
|
||||
delayUntilRecursion: false,
|
||||
displayIndex: index,
|
||||
probability: null,
|
||||
useProbability: false,
|
||||
probability: 100,
|
||||
useProbability: true,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
groupWeight: DEFAULT_WEIGHT,
|
||||
@ -3008,11 +3315,12 @@ function convertCharacterBook(characterBook) {
|
||||
position: entry.extensions?.position ?? (entry.position === 'before_char' ? world_info_position.before : world_info_position.after),
|
||||
excludeRecursion: entry.extensions?.exclude_recursion ?? false,
|
||||
preventRecursion: entry.extensions?.prevent_recursion ?? false,
|
||||
delayUntilRecursion: entry.extensions?.delay_until_recursion ?? false,
|
||||
disable: !entry.enabled,
|
||||
addMemo: entry.comment ? true : false,
|
||||
displayIndex: entry.extensions?.display_index ?? index,
|
||||
probability: entry.extensions?.probability ?? null,
|
||||
useProbability: entry.extensions?.useProbability ?? false,
|
||||
probability: entry.extensions?.probability ?? 100,
|
||||
useProbability: entry.extensions?.useProbability ?? true,
|
||||
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
|
||||
selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY,
|
||||
group: entry.extensions?.group ?? '',
|
||||
@ -3261,7 +3569,7 @@ export async function importWorldInfo(file) {
|
||||
toastr.info(`World Info "${data.name}" imported successfully!`);
|
||||
}
|
||||
},
|
||||
error: (jqXHR, exception) => { },
|
||||
error: (_jqXHR, _exception) => { },
|
||||
});
|
||||
}
|
||||
|
||||
@ -3468,21 +3776,14 @@ jQuery(() => {
|
||||
});
|
||||
|
||||
// Subscribe world loading to the select2 multiselect items (We need to target the specific select2 control)
|
||||
$('#world_info + span.select2-container').on('click', function (event) {
|
||||
if ($(event.target).hasClass('select2-selection__choice__display')) {
|
||||
event.preventDefault();
|
||||
|
||||
// select2 still bubbles the event to open the dropdown. So we close it here
|
||||
$('#world_info').select2('close');
|
||||
|
||||
const name = $(event.target).text();
|
||||
const selectedIndex = world_names.indexOf(name);
|
||||
if (selectedIndex !== -1) {
|
||||
$('#world_editor_select').val(selectedIndex).trigger('change');
|
||||
console.log('Quick selection of world', name);
|
||||
}
|
||||
select2ChoiceClickSubscribe($('#world_info'), target => {
|
||||
const name = $(target).text();
|
||||
const selectedIndex = world_names.indexOf(name);
|
||||
if (selectedIndex !== -1) {
|
||||
$('#world_editor_select').val(selectedIndex).trigger('change');
|
||||
console.log('Quick selection of world', name);
|
||||
}
|
||||
});
|
||||
}, { buttonStyle: true, closeDrawer: true });
|
||||
}
|
||||
|
||||
$('#WorldInfo').on('scroll', () => {
|
||||
|
@ -129,7 +129,7 @@ body {
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
/*defaults as 100%, then reassigned via JS as pixels, will work on PC and Android*/
|
||||
height: calc(var(--doc-height) - 1px);
|
||||
/*height: calc(var(--doc-height) - 1px);*/
|
||||
background-color: var(--greyCAIbg);
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
@ -698,7 +698,7 @@ body .panelControlBar {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
&.paused {
|
||||
&.script_paused {
|
||||
#rightSendForm > div:not(.mes_send).stscript_btn {
|
||||
&.stscript_pause {
|
||||
display: none;
|
||||
@ -872,7 +872,8 @@ body .panelControlBar {
|
||||
}
|
||||
|
||||
#chat .mes.selected{
|
||||
background-color: rgb(from var(--SmartThemeQuoteColor) r g b / .5);
|
||||
/* background-color: rgb(from var(--SmartThemeQuoteColor) r g b / .5); */
|
||||
background-color: rgb(102, 0, 0);
|
||||
}
|
||||
|
||||
.mes q:before,
|
||||
@ -1112,8 +1113,8 @@ select {
|
||||
}
|
||||
|
||||
#send_textarea {
|
||||
min-height: calc(var(--bottomFormBlockSize) + 3px);
|
||||
height: calc(var(--bottomFormBlockSize) + 3px);
|
||||
min-height: calc(var(--bottomFormBlockSize) + 2px);
|
||||
height: calc(var(--bottomFormBlockSize) + 2px);
|
||||
max-height: 50vh;
|
||||
max-height: 50svh;
|
||||
word-wrap: break-word;
|
||||
@ -2067,6 +2068,7 @@ input[type="file"] {
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bulk_select_checkbox {
|
||||
@ -2373,16 +2375,16 @@ input[type=search]::-webkit-search-cancel-button {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
border-radius: 50em;
|
||||
background: url('/img/times-circle.svg') no-repeat 50% 50%;
|
||||
background-color: var(--SmartThemeBodyColor);
|
||||
mask: url('/img/times-circle.svg') no-repeat 50% 50%;
|
||||
background-size: contain;
|
||||
backdrop-filter: invert(1) contrast(9);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=search]:focus::-webkit-search-cancel-button {
|
||||
opacity: .3;
|
||||
opacity: .5;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@ -2813,7 +2815,7 @@ grammarly-extension {
|
||||
#result_info_text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1;
|
||||
line-height: 0.9;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@ -4883,3 +4885,12 @@ body:not(.movingUI) .drawer-content.maximized {
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS styles using a consistent pastel color palette */
|
||||
.regex-brackets { color: #FFB347; } /* Pastel Orange */
|
||||
.regex-special { color: #B0E0E6; } /* Powder Blue */
|
||||
.regex-quantifier { color: #DDA0DD; } /* Plum */
|
||||
.regex-operator { color: #FFB6C1; } /* Light Pink */
|
||||
.regex-flags { color: #98FB98; } /* Pale Green */
|
||||
.regex-delimiter { font-weight: bold; color: #FF6961; } /* Pastel Red */
|
||||
.regex-highlight { color: #FAF8F6; } /* Pastel White */
|
||||
|
@ -75,6 +75,24 @@ function getFiles(dir, files = []) {
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the asset folders exist.
|
||||
* @param {import('../users').UserDirectoryList} directories - The user's directories
|
||||
*/
|
||||
function ensureFoldersExist(directories) {
|
||||
const folderPath = path.join(directories.assets);
|
||||
|
||||
for (const category of VALID_CATEGORIES) {
|
||||
const assetCategoryPath = path.join(folderPath, category);
|
||||
if (fs.existsSync(assetCategoryPath) && !fs.statSync(assetCategoryPath).isDirectory()) {
|
||||
fs.unlinkSync(assetCategoryPath);
|
||||
}
|
||||
if (!fs.existsSync(assetCategoryPath)) {
|
||||
fs.mkdirSync(assetCategoryPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
@ -92,15 +110,7 @@ router.post('/get', jsonParser, async (request, response) => {
|
||||
try {
|
||||
if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) {
|
||||
|
||||
for (const category of VALID_CATEGORIES) {
|
||||
const assetCategoryPath = path.join(folderPath, category);
|
||||
if (fs.existsSync(assetCategoryPath) && !fs.statSync(assetCategoryPath).isDirectory()) {
|
||||
fs.unlinkSync(assetCategoryPath);
|
||||
}
|
||||
if (!fs.existsSync(assetCategoryPath)) {
|
||||
fs.mkdirSync(assetCategoryPath);
|
||||
}
|
||||
}
|
||||
ensureFoldersExist(request.user.directories);
|
||||
|
||||
const folders = fs.readdirSync(folderPath, { withFileTypes: true })
|
||||
.filter(file => file.isDirectory());
|
||||
@ -193,6 +203,7 @@ router.post('/download', jsonParser, async (request, response) => {
|
||||
}
|
||||
|
||||
// Validate filename
|
||||
ensureFoldersExist(request.user.directories);
|
||||
const validation = validateAssetFileName(request.body.filename);
|
||||
if (validation.error)
|
||||
return response.status(400).send(validation.message);
|
||||
|
@ -254,7 +254,7 @@ async function sendMakerSuiteRequest(request, response) {
|
||||
};
|
||||
|
||||
function getGeminiBody() {
|
||||
const should_use_system_prompt = model === 'gemini-1.5-pro-latest' && request.body.use_makersuite_sysprompt;
|
||||
const should_use_system_prompt = ['gemini-1.5-flash-latest', 'gemini-1.5-pro-latest'].includes(model) && request.body.use_makersuite_sysprompt;
|
||||
const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, request.body.char_name, request.body.user_name);
|
||||
let body = {
|
||||
contents: prompt.contents,
|
||||
|
@ -436,6 +436,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
||||
group_override: entry.groupOverride ?? false,
|
||||
group_weight: entry.groupWeight ?? null,
|
||||
prevent_recursion: entry.preventRecursion ?? false,
|
||||
delay_until_recursion: entry.delayUntilRecursion ?? false,
|
||||
scan_depth: entry.scanDepth ?? null,
|
||||
match_whole_words: entry.matchWholeWords ?? null,
|
||||
use_group_scoring: entry.useGroupScoring ?? false,
|
||||
|
@ -7,7 +7,9 @@ const { getConfigValue, color } = require('../util');
|
||||
const { jsonParser } = require('../express-common');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const contentDirectory = path.join(process.cwd(), 'default/content');
|
||||
const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold');
|
||||
const contentIndexPath = path.join(contentDirectory, 'index.json');
|
||||
const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');
|
||||
const characterCardParser = require('../character-card-parser.js');
|
||||
|
||||
const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []);
|
||||
@ -16,6 +18,8 @@ const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDo
|
||||
* @typedef {Object} ContentItem
|
||||
* @property {string} filename
|
||||
* @property {string} type
|
||||
* @property {string} [name]
|
||||
* @property {string|null} [folder]
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -48,9 +52,7 @@ const CONTENT_TYPES = {
|
||||
*/
|
||||
function getDefaultPresets(directories) {
|
||||
try {
|
||||
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8');
|
||||
const contentIndex = JSON.parse(contentIndexText);
|
||||
|
||||
const contentIndex = getContentIndex();
|
||||
const presets = [];
|
||||
|
||||
for (const contentItem of contentIndex) {
|
||||
@ -112,8 +114,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
|
||||
continue;
|
||||
}
|
||||
|
||||
contentLog.push(contentItem.filename);
|
||||
const contentPath = path.join(contentDirectory, contentItem.filename);
|
||||
if (!contentItem.folder) {
|
||||
console.log(`Content file ${contentItem.filename} has no parent folder`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentPath = path.join(contentItem.folder, contentItem.filename);
|
||||
|
||||
if (!fs.existsSync(contentPath)) {
|
||||
console.log(`Content file ${contentItem.filename} is missing`);
|
||||
@ -129,6 +135,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
|
||||
|
||||
const basePath = path.parse(contentItem.filename).base;
|
||||
const targetPath = path.join(contentTarget, basePath);
|
||||
contentLog.push(contentItem.filename);
|
||||
|
||||
if (fs.existsSync(targetPath)) {
|
||||
console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`);
|
||||
@ -157,8 +164,7 @@ async function checkForNewContent(directoriesList, forceCategories = []) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8');
|
||||
const contentIndex = JSON.parse(contentIndexText);
|
||||
const contentIndex = getContentIndex();
|
||||
let anyContentAdded = false;
|
||||
|
||||
for (const directories of directoriesList) {
|
||||
@ -179,6 +185,38 @@ async function checkForNewContent(directoriesList, forceCategories = []) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets combined content index from the content and scaffold directories.
|
||||
* @returns {ContentItem[]} Array of content index
|
||||
*/
|
||||
function getContentIndex() {
|
||||
const result = [];
|
||||
|
||||
if (fs.existsSync(scaffoldIndexPath)) {
|
||||
const scaffoldIndexText = fs.readFileSync(scaffoldIndexPath, 'utf8');
|
||||
const scaffoldIndex = JSON.parse(scaffoldIndexText);
|
||||
if (Array.isArray(scaffoldIndex)) {
|
||||
scaffoldIndex.forEach((item) => {
|
||||
item.folder = scaffoldDirectory;
|
||||
});
|
||||
result.push(...scaffoldIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(contentIndexPath)) {
|
||||
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8');
|
||||
const contentIndex = JSON.parse(contentIndexText);
|
||||
if (Array.isArray(contentIndex)) {
|
||||
contentIndex.forEach((item) => {
|
||||
item.folder = contentDirectory;
|
||||
});
|
||||
result.push(...contentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target directory for the specified asset type.
|
||||
* @param {ContentType} type Asset type
|
||||
@ -351,7 +389,7 @@ function parseChubUrl(str) {
|
||||
let domainIndex = -1;
|
||||
|
||||
splitStr.forEach((part, index) => {
|
||||
if (part === 'www.chub.ai' || part === 'chub.ai') {
|
||||
if (part === 'www.chub.ai' || part === 'chub.ai' || part === 'www.characterhub.org' || part === 'characterhub.org') {
|
||||
domainIndex = index;
|
||||
}
|
||||
});
|
||||
@ -521,7 +559,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
|
||||
let result;
|
||||
let type;
|
||||
|
||||
const isChub = host.includes('chub.ai');
|
||||
const isChub = host.includes('chub.ai') || host.includes('characterhub.org');
|
||||
const isJannnyContent = host.includes('janitorai');
|
||||
const isPygmalionContent = host.includes('pygmalion.chat');
|
||||
const isAICharacterCardsContent = host.includes('aicharactercards.com');
|
||||
|
@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const { SentencePieceProcessor } = require('@agnai/sentencepiece-js');
|
||||
const tiktoken = require('@dqbd/tiktoken');
|
||||
const tiktoken = require('tiktoken');
|
||||
const { Tokenizer } = require('@agnai/web-tokenizers');
|
||||
const { convertClaudePrompt, convertGooglePrompt } = require('../prompt-converters');
|
||||
const { readSecret, SECRET_KEYS } = require('./secrets');
|
||||
@ -15,7 +15,7 @@ const { setAdditionalHeaders } = require('../additional-headers');
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {{[key: string]: import("@dqbd/tiktoken").Tiktoken}} Tokenizers cache
|
||||
* @type {{[key: string]: import('tiktoken').Tiktoken}} Tokenizers cache
|
||||
*/
|
||||
const tokenizersCache = {};
|
||||
|
||||
@ -262,6 +262,10 @@ function getWebTokenizersChunks(tokenizer, ids) {
|
||||
* @returns {string} Tokenizer model to use
|
||||
*/
|
||||
function getTokenizerModel(requestModel) {
|
||||
if (requestModel.includes('gpt-4o')) {
|
||||
return 'gpt-4o';
|
||||
}
|
||||
|
||||
if (requestModel.includes('gpt-4-32k')) {
|
||||
return 'gpt-4-32k';
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user