Merge branch 'staging' into modelsearch

This commit is contained in:
Cohee 2024-06-23 19:05:00 +03:00
commit f2d64a7d08
135 changed files with 8794 additions and 3705 deletions

View File

@ -1,7 +1,6 @@
name: Update i18n data name: Update i18n data
on: on: workflow_dispatch
workflow_dispatch:
jobs: jobs:
build: build:

View File

@ -476,7 +476,11 @@
"type": "context" "type": "context"
}, },
{ {
"filename": "presets/context/DreamGen Role-Play V1.json", "filename": "presets/context/DreamGen Role-Play V1 ChatML.json",
"type": "context"
},
{
"filename": "presets/context/DreamGen Role-Play V1 Llama3.json",
"type": "context" "type": "context"
}, },
{ {
@ -556,7 +560,11 @@
"type": "instruct" "type": "instruct"
}, },
{ {
"filename": "presets/instruct/DreamGen Role-Play V1.json", "filename": "presets/instruct/DreamGen Role-Play V1 ChatML.json",
"type": "instruct"
},
{
"filename": "presets/instruct/DreamGen Role-Play V1 Llama3.json",
"type": "instruct" "type": "instruct"
}, },
{ {

View File

@ -8,5 +8,5 @@
"trim_sentences": true, "trim_sentences": true,
"include_newline": false, "include_newline": false,
"single_line": false, "single_line": false,
"name": "DreamGen Role-Play V1" "name": "DreamGen Role-Play V1 ChatML"
} }

View File

@ -0,0 +1,12 @@
{
"story_string": "<|start_header_id|>system<|end_header_id|>\n\n{{#if system}}{{system}}\n\n\n{{/if}}## Overall plot description:\n\n{{#if scenario}}{{scenario}}{{else}}Conversation between {{char}} and {{user}}.{{/if}}{{#if wiBefore}}\n\n{{wiBefore}}{{/if}}\n\n\n## Characters:\n\n### {{char}}\n\n{{#if description}}{{description}}\n\n{{/if}}{{#if personality}}{{personality}}\n\n{{/if}}### {{user}}\n\n{{#if persona}}{{persona}}{{else}}{{user}} is the protagonist of the role-play.{{/if}}{{#if wiAfter}}\n\n{{wiAfter}}{{/if}}{{#if mesExamples}}\n\n{{mesExamples}}{{/if}}",
"example_separator": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nWrite an example narrative / conversation that is not part of the main story.",
"chat_start": "<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\nStart the role-play between {{char}} and {{user}}.",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": false,
"trim_sentences": true,
"include_newline": false,
"single_line": false,
"name": "DreamGen Role-Play V1 Llama3"
}

View File

@ -20,5 +20,5 @@
"user_alignment_message": "", "user_alignment_message": "",
"system_same_as_user": true, "system_same_as_user": true,
"last_system_sequence": "", "last_system_sequence": "",
"name": "DreamGen Role-Play V1" "name": "DreamGen Role-Play V1 ChatML"
} }

View File

@ -0,0 +1,18 @@
{
"system_prompt": "You are an intelligent, skilled, versatile writer.\n\nYour task is to write a role-play based on the information below.",
"input_sequence": "<|eot_id|>\n<|start_header_id|>writer character: {{user}}<|end_header_id|>\n\n",
"output_sequence": "<|eot_id|>\n<|start_header_id|>writer character: {{char}}<|end_header_id|>\n\n",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": false,
"activation_regex": "",
"skip_examples": false,
"name": "DreamGen Role-Play V1 Llama3"
}

View File

@ -1,7 +1,7 @@
{ {
"name": "Cappuccino", "name": "Cappuccino",
"blur_strength": 3, "blur_strength": 3,
"main_text_color": "rgba(255, 255, 255, 1)", "main_text_color": "rgba(235, 235, 235, 1)",
"italics_text_color": "rgba(230, 210, 190, 1)", "italics_text_color": "rgba(230, 210, 190, 1)",
"underline_text_color": "rgba(205, 180, 160, 1)", "underline_text_color": "rgba(205, 180, 160, 1)",
"quote_text_color": "rgba(165, 140, 115, 1)", "quote_text_color": "rgba(165, 140, 115, 1)",

122
public/css/animations.css Normal file
View File

@ -0,0 +1,122 @@
/* Fade animations with opacity */
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* Pop animations with opacity and vertical scaling */
@keyframes pop-in {
0% {
opacity: 0;
transform: scaleY(0);
}
/* Make the scaling faster on pop-in, otherwise it looks a bit weird */
33% {
transform: scaleY(1);
}
100% {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes pop-out {
0% {
opacity: 1;
transform: scaleY(1);
}
100% {
opacity: 0;
transform: scaleY(0);
}
}
/* Flashing for highlighting animation */
@keyframes flash {
20%,
60%,
100% {
opacity: 1;
}
0%,
40%,
80% {
opacity: 0.2;
}
}
/* Pulsing highlight, slightly resizing the element */
@keyframes pulse {
from {
transform: scale(1);
filter: brightness(1.1);
}
to {
transform: scale(1.01);
filter: brightness(1.3);
}
}
/* Ellipsis animation */
@keyframes ellipsis {
0% {
content: ""
}
25% {
content: "."
}
50% {
content: ".."
}
75% {
content: "..."
}
}
/* HEINOUS */
@keyframes infinite-spinning {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* STscript animation */
@keyframes script_progress_pulse {
0%,
100% {
border-top-color: var(--progColor);
}
50% {
border-top-color: var(--progFlashColor);
}
}

View File

@ -98,7 +98,7 @@
font-weight: bold; font-weight: bold;
} }
.logprobs_top_candidate:not([disabled]):hover, .logprobs_top_candidate:not([disabled]):focus { .logprobs_top_candidate:not([disabled]):hover {
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
} }

147
public/css/popup.css Normal file
View File

@ -0,0 +1,147 @@
dialog {
color: var(--SmartThemeBodyColor);
}
/* Closed state of the dialog */
.popup {
width: 500px;
text-align: center;
box-shadow: 0px 0px 14px var(--black70a);
border: 1px solid var(--SmartThemeBorderColor);
padding: 4px 14px;
background-color: var(--SmartThemeBlurTintColor);
border-radius: 10px;
display: flex;
flex-direction: column;
/* Overflow visible so elements (like toasts) can appear outside of the dialog. '.popup-body' is hiding overflow for the real content. */
overflow: visible;
/* Fix weird animation issue with font-scaling during popup open */
backface-visibility: hidden;
transform: translateZ(0);
-webkit-font-smoothing: subpixel-antialiased;
}
.popup .popup-body {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
height: 100%;
padding: 1px;
}
.popup .popup-content {
margin-top: 10px;
padding: 0 8px;
overflow: hidden;
flex-grow: 1;
}
.popup .popup-content h3:first-child {
/* No double spacing for the first heading needed, the .popup-content already has margin */
margin-top: 0px;
}
.popup.vertical_scrolling_dialogue_popup .popup-content {
overflow-y: auto;
}
.popup.horizontal_scrolling_dialogue_popup .popup-content {
overflow-x: auto;
}
/* Opening animation */
.popup[opening] {
animation: pop-in var(--animation-duration-slow) ease-in-out;
}
.popup[opening]::backdrop {
animation: fade-in var(--animation-duration-slow) ease-in-out;
}
/* Open state of the dialog */
.popup[open] {
color: var(--SmartThemeBodyColor);
}
.popup[open]::backdrop {
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
background-color: var(--black30a);
}
/* Closing animation */
.popup[closing] {
animation: pop-out var(--animation-duration-slow) ease-in-out;
}
.popup[closing]::backdrop {
animation: fade-out var(--animation-duration-slow) ease-in-out;
}
.popup #toast-container {
/* Fix toastr in dialogs by actually placing it at the top of the screen via transform */
height: 100svh;
top: calc(50% + var(--topBarBlockSize));
left: 50%;
transform: translate(-50%, -50%);
/* Fix text align, popups are centered by default. toasts should not. */
text-align: left;
}
.popup-input {
margin-top: 10px;
}
.popup-controls {
margin-top: 10px;
display: flex;
align-self: center;
gap: 20px;
}
.menu_button.menu_button_default {
box-shadow: 0 0 5px var(--white20a);
}
.menu_button.popup-button-ok {
background-color: var(--crimson70a);
}
.menu_button.popup-button-ok:hover {
background-color: var(--crimson-hover);
}
.menu_button.popup-button-custom {
/* Custom buttons should not scale to smallest size, otherwise they will always break to multiline */
width: unset;
}
.popup-controls .menu_button {
/* Fix weird animation issue with fonts on brightness filter */
backface-visibility: hidden;
transform: translateZ(0);
-webkit-font-smoothing: subpixel-antialiased;
}
.popup-controls .menu_button:hover:focus-visible {
filter: brightness(1.3) saturate(1.3);
}
.popup .popup-button-close {
position: absolute;
top: -6px;
right: -6px;
width: 24px;
height: 24px;
font-size: 20px;
padding: 2px 3px 3px 2px;
filter: brightness(0.8);
/* Fix weird animation issue with font-scaling during popup open */
backface-visibility: hidden;
}

View File

@ -23,6 +23,14 @@
opacity: 0.8; opacity: 0.8;
} }
.select2-selection--single .select2-selection__placeholder {
color: var(--SmartThemeEmColor);
}
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
color: var(--SmartThemeEmColor);
}
.select2-container .select2-selection--single .select2-selection__rendered { .select2-container .select2-selection--single .select2-selection__rendered {
color: var(--SmartThemeBodyColor); color: var(--SmartThemeBodyColor);
line-height: revert; line-height: revert;
@ -49,7 +57,7 @@
color: var(--SmartThemeBodyColor); color: var(--SmartThemeBodyColor);
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px; border-radius: 7px;
font-family: "Noto Sans", "Noto Color Emoji", sans-serif; font-family: var(--mainFontFamily);
padding: 3px 5px; padding: 3px 5px;
} }
@ -77,7 +85,7 @@
color: var(--SmartThemeBodyColor); color: var(--SmartThemeBodyColor);
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
border-radius: 7px; border-radius: 7px;
font-family: "Noto Sans", "Noto Color Emoji", sans-serif; font-family: var(--mainFontFamily);
padding: 3px 5px; padding: 3px 5px;
} }
@ -173,8 +181,9 @@
} }
.select2-selection__choice__display { .select2-selection__choice__display {
/* Fix weird alignment on the left side */ /* Fix weird alignment of the inside block */
margin-left: 1px; margin-left: 3px;
margin-right: 1px;
} }
/* Styling for choice remove icon */ /* Styling for choice remove icon */
@ -194,6 +203,7 @@ span.select2.select2-container .select2-selection__choice__remove:hover {
.select2_choice_clickable+span.select2-container .select2-selection__choice__display { .select2_choice_clickable+span.select2-container .select2-selection__choice__display {
cursor: pointer; cursor: pointer;
} }
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display { .select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display {
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
@ -211,7 +221,9 @@ span.select2.select2-container .select2-selection__choice__remove:hover {
.select2_multi_sameline+span.select2-container .select2-selection--multiple { .select2_multi_sameline+span.select2-container .select2-selection--multiple {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
}.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline { }
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
/* Allow search placeholder to take up all space if needed */ /* Allow search placeholder to take up all space if needed */
flex-grow: 1; flex-grow: 1;
} }

View File

@ -220,7 +220,7 @@
} }
.monospace { .monospace {
font-family: monospace; font-family: var(--monoFontFamily);
} }
.expander { .expander {
@ -292,6 +292,14 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.inline-flex {
display: inline-flex;
}
.inline-block {
display: inline-block;
}
.alignitemscenter, .alignitemscenter,
.alignItemsCenter { .alignItemsCenter {
align-items: center; align-items: center;
@ -348,6 +356,10 @@
margin-right: 5px; margin-right: 5px;
} }
.margin-r2 {
margin-right: 2px;
}
.flex0 { .flex0 {
flex: 0; flex: 0;
} }

View File

@ -14,7 +14,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 10px; gap: 6px;
margin-bottom: 5px; margin-bottom: 5px;
} }
@ -27,8 +27,19 @@
flex: 1; flex: 1;
} }
.tag_view_color_picker {
position: relative;
}
.tag_view_color_picker .link_icon {
position: absolute;
top: 50%;
right: 0px;
opacity: 0.5;
}
.tag_delete { .tag_delete {
padding-right: 0; padding: 2px 4px;
color: var(--SmartThemeBodyColor) !important; color: var(--SmartThemeBodyColor) !important;
} }
@ -108,6 +119,14 @@
opacity: 0.6; opacity: 0.6;
} }
#tagList .tag:has(.tag_remove:hover) {
opacity: 1;
}
#tagList .tag:has(.tag_remove:hover) .tag_name {
opacity: 0.6;
}
.tags.tags_inline { .tags.tags_inline {
opacity: 0.6; opacity: 0.6;
column-gap: 0.2rem; column-gap: 0.2rem;
@ -164,6 +183,7 @@
.tag.selected { .tag.selected {
opacity: 1 !important; opacity: 1 !important;
filter: none !important; filter: none !important;
border: 1px solid lightgreen;
} }
.tag.excluded { .tag.excluded {

View File

@ -18,6 +18,7 @@
<link rel="preload" as="style" href="style.css"> <link rel="preload" as="style" href="style.css">
<link rel="manifest" crossorigin="use-credentials" href="manifest.json"> <link rel="manifest" crossorigin="use-credentials" href="manifest.json">
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet"> <link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
<link href="webfonts/NotoSansMono/stylesheet.css" rel="stylesheet">
<link href="css/fontawesome.min.css" rel="stylesheet"> <link href="css/fontawesome.min.css" rel="stylesheet">
<link href="css/solid.min.css" rel="stylesheet"> <link href="css/solid.min.css" rel="stylesheet">
<link href="css/brands.min.css" rel="stylesheet"> <link href="css/brands.min.css" rel="stylesheet">
@ -32,7 +33,6 @@
<link rel="apple-touch-icon" sizes="144x144" href="img/apple-icon-144x144.png" /> <link rel="apple-touch-icon" sizes="144x144" href="img/apple-icon-144x144.png" />
<link rel="stylesheet" type="text/css" href="style.css"> <link rel="stylesheet" type="text/css" href="style.css">
<link rel="stylesheet" type="text/css" href="css/st-tailwind.css"> <link rel="stylesheet" type="text/css" href="css/st-tailwind.css">
<link rel="stylesheet" type="text/css" href="css/tags.css">
<link rel="stylesheet" type="text/css" href="css/rm-groups.css"> <link rel="stylesheet" type="text/css" href="css/rm-groups.css">
<link rel="stylesheet" type="text/css" href="css/group-avatars.css"> <link rel="stylesheet" type="text/css" href="css/group-avatars.css">
<link rel="stylesheet" type="text/css" href="css/toggle-dependent.css"> <link rel="stylesheet" type="text/css" href="css/toggle-dependent.css">
@ -72,8 +72,8 @@
<div id="lm_button_panel_pin_div" title="Locked = AI Configuration panel will stay open" data-i18n="[title]AI Configuration panel will stay open"> <div id="lm_button_panel_pin_div" title="Locked = AI Configuration panel will stay open" data-i18n="[title]AI Configuration panel will stay open">
<input type="checkbox" id="lm_button_panel_pin"> <input type="checkbox" id="lm_button_panel_pin">
<label for="lm_button_panel_pin"> <label for="lm_button_panel_pin">
<div class="unchecked fa-solid fa-unlock "></div> <div class="unchecked fa-solid fa-unlock right_menu_button"></div>
<div class="checked fa-solid fa-lock "></div> <div class="checked fa-solid fa-lock right_menu_button"></div>
</label> </label>
</div> </div>
<div id="clickSlidersTips" data-i18n="clickslidertips" class="toggle-description wide100p editable-slider-notification"> <div id="clickSlidersTips" data-i18n="clickslidertips" class="toggle-description wide100p editable-slider-notification">
@ -1753,8 +1753,8 @@
<input id="openai_function_calling" type="checkbox" /> <input id="openai_function_calling" type="checkbox" />
<span data-i18n="Enable function calling">Enable function calling</span> <span data-i18n="Enable function calling">Enable function calling</span>
<div class="flexBasis100p toggle-description justifyLeft"> <div class="flexBasis100p toggle-description justifyLeft">
Allows using <a href="https://platform.openai.com/docs/guides/function-calling" target="_blank">function tools</a>. <span data-i18n="enable_functions_desc_1">Allows using </span><a href="https://platform.openai.com/docs/guides/function-calling" target="_blank" data-i18n="enable_functions_desc_2">function tools</a>.
Can be utilized by various extensions to provide additional functionality. <span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>
</div> </div>
</label> </label>
</div> </div>
@ -1863,7 +1863,7 @@
<div id="openai_logit_bias_new_entry" class="menu_button wide100p flex-container justifyCenter" data-i18n="Add bias entry"> <div id="openai_logit_bias_new_entry" class="menu_button wide100p flex-container justifyCenter" data-i18n="Add bias entry">
Add bias entry Add bias entry
</div> </div>
<div class="openai_logit_bias_list"></div> <div class="openai_logit_bias_list" no_items_text="No items" data-i18n="[no_items_text]openai_logit_bias_no_items"></div>
<div class="m-t-1"> <div class="m-t-1">
<small> <small>
<i class="fa-solid fa-lightbulb"></i> <i class="fa-solid fa-lightbulb"></i>
@ -1887,7 +1887,7 @@
</div> </div>
<div id="sys-settings-button" class="drawer"> <div id="sys-settings-button" class="drawer">
<div class="drawer-toggle drawer-header"> <div class="drawer-toggle drawer-header">
<div id="API-status-top" class="drawer-icon fa-solid fa-plug-circle-exclamation closedIcon" title="API Connections" data-i18n="[title]API Connections"></div> <div id="API-status-top" class="drawer-icon fa-solid fa-plug-circle-exclamation closedIcon" title="API Connections" data-i18n="[title]API Connections;[no_connection_text]api_no_connection" no_connection_text="No connection..."></div>
</div> </div>
<div id="rm_api_block" class="drawer-content closedDrawer"> <div id="rm_api_block" class="drawer-content closedDrawer">
<h3 class="margin0" id="title_api">API</h3> <h3 class="margin0" id="title_api">API</h3>
@ -2357,7 +2357,7 @@
<select id="chat_completion_source"> <select id="chat_completion_source">
<optgroup> <optgroup>
<option value="openai">OpenAI</option> <option value="openai">OpenAI</option>
<option value="custom">Custom (OpenAI-compatible)</option> <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
</optgroup> </optgroup>
<optgroup> <optgroup>
<option value="ai21">AI21</option> <option value="ai21">AI21</option>
@ -2551,6 +2551,7 @@
<h4 data-i18n="Claude Model">Claude Model</h4> <h4 data-i18n="Claude Model">Claude Model</h4>
<select id="model_claude_select"> <select id="model_claude_select">
<optgroup label="Versions"> <optgroup label="Versions">
<option value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
<option value="claude-3-opus-20240229">claude-3-opus-20240229</option> <option value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option> <option value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option value="claude-3-haiku-20240307">claude-3-haiku-20240307</option> <option value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
@ -2856,16 +2857,16 @@
<form id="custom_form" data-source="custom"> <form id="custom_form" data-source="custom">
<h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4> <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
<div class="flex-container"> <div class="flex-container">
<input id="custom_api_url_text" class="text_pole wide100p" maxlength="5000" value="" autocomplete="off" placeholder="Example: http://localhost:1234/v1"> <input id="custom_api_url_text" class="text_pole wide100p" maxlength="5000" value="" autocomplete="off" data-i18n="[placeholder]Example: http://localhost:1234/v1" placeholder="Example: http://localhost:1234/v1">
</div> </div>
<div> <div>
<small> <small>
Doesn't work? Try adding <code>/v1</code> at the end of the URL! <span data-i18n="Doesn't work? Try adding">Doesn't work? Try adding</span> <code>/v1</code> <span data-i18n="at the end of the URL!">at the end of the URL!</span>
</small> </small>
</div> </div>
<h4> <h4>
<span data-i18n="Custom API Key">Custom API Key</span> <span data-i18n="Custom API Key">Custom API Key</span>
<small>(Optional)</small> <small data-i18n="(Optional)">(Optional)</small>
</h4> </h4>
<div class="flex-container"> <div class="flex-container">
<input id="api_key_custom" name="api_key_custom" class="text_pole flex1" maxlength="5000" value="" type="text" autocomplete="off"> <input id="api_key_custom" name="api_key_custom" class="text_pole flex1" maxlength="5000" value="" type="text" autocomplete="off">
@ -2874,9 +2875,9 @@
<div data-for="api_key_custom" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page."> <div data-for="api_key_custom" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
For privacy reasons, your API key will be hidden after you reload the page. For privacy reasons, your API key will be hidden after you reload the page.
</div> </div>
<h4>Enter a Model ID</h4> <h4 data-i18n="Enter a Model ID">Enter a Model ID</h4>
<div class="flex-container"> <div class="flex-container">
<input list="model_custom_select_fill" id="custom_model_id" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" placeholder="Example: gpt-3.5-turbo"> <input list="model_custom_select_fill" id="custom_model_id" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-i18n="[placeholder]Example: gpt-3.5-turbo" placeholder="Example: gpt-3.5-turbo">
<datalist id="model_custom_select_fill" class="text_pole model_custom_select"></datalist> <datalist id="model_custom_select_fill" class="text_pole model_custom_select"></datalist>
</div> </div>
<h4 data-i18n="Available Models">Available Models</h4> <h4 data-i18n="Available Models">Available Models</h4>
@ -2885,14 +2886,14 @@
</div> </div>
<h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4> <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
<select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API."> <select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">
<option value="">None</option> <option data-i18n="prompt_post_processing_none" value="">None</option>
<option value="claude">Claude</option> <option value="claude">Claude</option>
</select> </select>
</form> </form>
<div class="flex-container flex"> <div class="flex-container flex">
<div id="api_button_openai" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect">Connect</div> <div id="api_button_openai" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect">Connect</div>
<div class="api_loading menu_button" data-i18n="Cancel">Cancel</div> <div class="api_loading menu_button" data-i18n="Cancel">Cancel</div>
<div data-source="custom" id="customize_additional_parameters" class="menu_button menu_button_icon">Additional Parameters</div> <div data-source="custom" id="customize_additional_parameters" class="menu_button menu_button_icon" data-i18n="Additional Parameters">Additional Parameters</div>
<div data-source="openrouter" class="menu_button menu_button_icon openrouter_authorize" title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai" data-i18n="Authorize;[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">Authorize</div> <div data-source="openrouter" class="menu_button menu_button_icon openrouter_authorize" title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai" data-i18n="Authorize;[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">Authorize</div>
<div id="test_api_button" class="menu_button menu_button_icon" title="Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!" data-i18n="[title]Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!"><span data-i18n="Test Message">Test Message</span></div> <div id="test_api_button" class="menu_button menu_button_icon" title="Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!" data-i18n="[title]Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!"><span data-i18n="Test Message">Test Message</span></div>
</div> </div>
@ -3923,13 +3924,22 @@
<h4 data-i18n="Character Handling"> <h4 data-i18n="Character Handling">
Character Handling Character Handling
</h4> </h4>
<div title="If set in the advanced character definitions, this field will be displayed in the characters list." data-i18n="[title]If set in the advanced character definitions, this field will be displayed in the characters list."> <div class="flex-container alignitemscenter" title="If set in the advanced character definitions, this field will be displayed in the characters list." data-i18n="[title]If set in the advanced character definitions, this field will be displayed in the characters list.">
<label for="aux_field"><small data-i18n="Char List Subheader">Char List Subheader</small></label> <label for="aux_field"><small data-i18n="Char List Subheader">Char List Subheader</small></label>
<select id="aux_field"> <select id="aux_field" class="widthNatural flex1 margin0">
<option data-i18n="Character Version" value="character_version">Character Version</option> <option data-i18n="Character Version" value="character_version">Character Version</option>
<option data-i18n="Created by" value="creator">Created by</option> <option data-i18n="Created by" value="creator">Created by</option>
</select> </select>
</div> </div>
<div data-newbie-hidden class="flex-container alignitemscenter" title="Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog." data-i18n="[title]Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.">
<label for="tag_import_setting"><small data-i18n="Import Card Tags">Import Card Tags</small></label>
<select id="tag_import_setting" class="widthNatural flex1 margin0">
<option data-i18n="Ask" value="1">Ask</option>
<option data-i18n="None" value="2">None</option>
<option data-i18n="All" value="3">All</option>
<option data-i18n="Existing" value="4">Existing</option>
</select>
</div>
<label data-newbie-hidden class="checkbox_label" for="fuzzy_search_checkbox" title="Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring." data-i18n="[title]Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring"> <label data-newbie-hidden class="checkbox_label" for="fuzzy_search_checkbox" title="Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring." data-i18n="[title]Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring">
<input id="fuzzy_search_checkbox" type="checkbox" /> <input id="fuzzy_search_checkbox" type="checkbox" />
<small data-i18n="Advanced Character Search">Advanced Character Search</small> <small data-i18n="Advanced Character Search">Advanced Character Search</small>
@ -3950,10 +3960,6 @@
<input id="show_card_avatar_urls" type="checkbox" /> <input id="show_card_avatar_urls" type="checkbox" />
<small data-i18n="Show avatar filenames">Show avatar filenames</small> <small data-i18n="Show avatar filenames">Show avatar filenames</small>
</label> </label>
<label data-newbie-hidden class="checkbox_label" for="import_card_tags" title="Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored." data-i18n="[title]Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored">
<input id="import_card_tags" type="checkbox" />
<small data-i18n="Import Card Tags">Import Card Tags</small>
</label>
<label data-newbie-hidden class="checkbox_label" for="spoiler_free_mode" title="Hide character definitions from the editor panel behind a spoiler button." data-i18n="[title]Hide character definitions from the editor panel behind a spoiler button"> <label data-newbie-hidden class="checkbox_label" for="spoiler_free_mode" title="Hide character definitions from the editor panel behind a spoiler button." data-i18n="[title]Hide character definitions from the editor panel behind a spoiler button">
<input id="spoiler_free_mode" type="checkbox" /> <input id="spoiler_free_mode" type="checkbox" />
<small data-i18n="Spoiler Free Mode">Spoiler Free Mode</small> <small data-i18n="Spoiler Free Mode">Spoiler Free Mode</small>
@ -4247,7 +4253,7 @@
<div name="STscriptToggles"> <div name="STscriptToggles">
<h4 data-i18n="STscript Settings">STscript Settings</h4> <h4 data-i18n="STscript Settings">STscript Settings</h4>
<div title="Sets default flags for the STscript parser." data-i18n="[title]Sets default flags for the STscript parser."> <div title="Sets default flags for the STscript parser." data-i18n="[title]Sets default flags for the STscript parser.">
<label data-i18n="Parser Flags"><small>Parser Flags</small></label> <label><small data-i18n="Parser Flags">Parser Flags</small></label>
<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 delimiting characters to be escaped with a backslash, and backslashes to be escaped as well."> <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 delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.">
<input id="stscript_parser_flag_strict_escaping" type="checkbox" /> <input id="stscript_parser_flag_strict_escaping" type="checkbox" />
<span data-i18n="STRICT_ESCAPING"><small>STRICT_ESCAPING</small></span> <span data-i18n="STRICT_ESCAPING"><small>STRICT_ESCAPING</small></span>
@ -4476,14 +4482,14 @@
<div id="rm_button_panel_pin_div" class="alignitemsflexstart" title="Locked = Character Management panel will stay open" data-i18n="[title]Locked = Character Management panel will stay open"> <div id="rm_button_panel_pin_div" class="alignitemsflexstart" title="Locked = Character Management panel will stay open" data-i18n="[title]Locked = Character Management panel will stay open">
<input type="checkbox" id="rm_button_panel_pin"> <input type="checkbox" id="rm_button_panel_pin">
<label for="rm_button_panel_pin"> <label for="rm_button_panel_pin">
<div class="fa-solid unchecked fa-unlock" alt=""></div> <div class="fa-solid unchecked fa-unlock right_menu_button" alt=""></div>
<div class="fa-solid checked fa-lock" alt=""></div> <div class="fa-solid checked fa-lock right_menu_button" alt=""></div>
</label> </label>
</div> </div>
<div class="right_menu_button fa-solid fa-list-ul" id="rm_button_characters" title="Select/Create Characters" data-i18n="[title]Select/Create Characters"></div> <div class="right_menu_button fa-solid fa-list-ul" id="rm_button_characters" title="Select/Create Characters" data-i18n="[title]Select/Create Characters"></div>
</div> </div>
<div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p"> <div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p">
<div class="hotswap avatars_inline flex-container expander" data-i18n="[no_favs]Favorite characters to add them to HotSwaps" no_favs="Favorite characters to add them to HotSwaps"></div> <div class="hotswap avatars_inline flex-container scroll-reset-container expander" data-i18n="[no_favs]Favorite characters to add them to HotSwaps" no_favs="Favorite characters to add them to HotSwaps"></div>
</div> </div>
</div> </div>
<hr> <hr>
@ -4491,7 +4497,7 @@
<div id="rm_PinAndTabs"> <div id="rm_PinAndTabs">
<div id="right-nav-panel-tabs" class=""> <div id="right-nav-panel-tabs" class="">
<div id="rm_button_selected_ch"> <div id="rm_button_selected_ch">
<h2></h2> <h2 class="interactable"></h2>
</div> </div>
<div id="result_info" class="flex-container" style="display: none;"> <div id="result_info" class="flex-container" style="display: none;">
<div id="result_info_text" title="Token counts may be inaccurate and provided just for reference." data-i18n="[title]Token counts may be inaccurate and provided just for reference."> <div id="result_info_text" title="Token counts may be inaccurate and provided just for reference." data-i18n="[title]Token counts may be inaccurate and provided just for reference.">
@ -4715,7 +4721,7 @@
<div class="flex1 flexGap5" title="Inserted before each part of the joined fields." data-i18n="[title]Inserted before each part of the joined fields."> <div class="flex1 flexGap5" title="Inserted before each part of the joined fields." data-i18n="[title]Inserted before each part of the joined fields.">
<label for="rm_group_generation_mode_join_prefix" class="flexnowrap width100p whitespacenowrap"> <label for="rm_group_generation_mode_join_prefix" class="flexnowrap width100p whitespacenowrap">
<span data-i18n="Join Prefix">Join Prefix</span> <span data-i18n="Join Prefix">Join Prefix</span>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)" title="When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)"> <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)" title="When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)">
</div> </div>
</label> </label>
<textarea id="rm_group_generation_mode_join_prefix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea> <textarea id="rm_group_generation_mode_join_prefix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
@ -4723,7 +4729,7 @@
<div class="flex1 flexGap5" title="Inserted after each part of the joined fields." data-i18n="[title]Inserted after each part of the joined fields."> <div class="flex1 flexGap5" title="Inserted after each part of the joined fields." data-i18n="[title]Inserted after each part of the joined fields.">
<label for="rm_group_generation_mode_join_suffix" class="flexnowrap width100p whitespacenowrap"> <label for="rm_group_generation_mode_join_suffix" class="flexnowrap width100p whitespacenowrap">
<span data-i18n="Join Suffix">Join Suffix</span> <span data-i18n="Join Suffix">Join Suffix</span>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)" title="When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)"> <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)" title="When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)">
</div> </div>
</label> </label>
<textarea id="rm_group_generation_mode_join_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea> <textarea id="rm_group_generation_mode_join_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
@ -4842,21 +4848,20 @@
</div> </div>
</div> </div>
<!-- various fullscreen popups --> <!-- various fullscreen popups -->
<template id="shadow_popup_template" data-i18n="[popup_text_save]popup_text_save;[popup_text_yes]popup_text_yes;[popup_text_no]popup_text_no;[popup_text_cancel]popup_text_cancel;[popup_text_import]popup_text_import" popup_text_save="Save" popup_text_yes="Yes" popup_text_no="No" popup_text_cancel="Cancel" popup_text_import="Import"> <!-- localization data holder for popups --> <template id="popup_template" data-i18n="[popup-button-save]popup-button-save;[popup-button-yes]popup-button-yes;[popup-button-no]popup-button-no;[popup-button-cancel]popup-button-cancel;[popup-button-import]popup-button-import" popup-button-save="Save" popup-button-yes="Yes" popup-button-no="No" popup-button-cancel="Cancel" popup-button-import="Import"> <!-- localization data holder for popups -->
<div class="shadow_popup"> <dialog class="popup">
<div class="dialogue_popup"> <div class="popup-body">
<div class="dialogue_popup_holder"> <div class="popup-content">
<div class="dialogue_popup_text"> <h3 class="popup-header">text</h3>
<h3 class="margin0">text</h3> </div>
</div> <textarea class="popup-input text_pole result-control" rows="1" data-result="1"></textarea>
<textarea class="dialogue_popup_input text_pole" rows="1"></textarea> <div class="popup-controls">
<div class="dialogue_popup_controls"> <div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>
<div class="dialogue_popup_ok menu_button" data-i18n="Delete">Delete</div> <div class="popup-button-cancel menu_button result-control" data-result="0" data-i18n="Cancel">Cancel</div>
<div class="dialogue_popup_cancel menu_button" data-i18n="Cancel">Cancel</div>
</div>
</div> </div>
</div> </div>
</div> <div class="popup-button-close right_menu_button fa-solid fa-circle-xmark" data-result="0" title="Close popup" data-i18n="[title]Close popup"></div>
</dialog>
</template> </template>
<div id="shadow_popup"> <div id="shadow_popup">
<div id="dialogue_popup"> <div id="dialogue_popup">
@ -4866,15 +4871,15 @@
</div> </div>
<textarea id="dialogue_popup_input" class="text_pole" rows="1"></textarea> <textarea id="dialogue_popup_input" class="text_pole" rows="1"></textarea>
<div id="dialogue_popup_controls"> <div id="dialogue_popup_controls">
<div id="dialogue_popup_ok" class="menu_button" data-i18n="Delete">Delete</div> <div id="dialogue_popup_ok" class="menu_button" data-i18n="Delete" data-result="1">Delete</div>
<div id="dialogue_popup_cancel" class="menu_button" data-i18n="Cancel">Cancel</div> <div id="dialogue_popup_cancel" class="menu_button" data-i18n="Cancel" data-result="0">Cancel</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="character_popup" class="flex-container flexFlowColumn flexNoGap"> <div id="character_popup" class="flex-container flexFlowColumn flexNoGap">
<div id="character_popup_text"> <div id="character_popup_text">
<h3 id="character_popup_text_h3" class="margin0"></h3> <span data-i18n="Advanced Defininitions">- Advanced <h3 id="character_popup-button-h3" class="margin0"></h3> <span data-i18n="Advanced Defininitions">- Advanced
Definitions</span> Definitions</span>
</div> </div>
<hr class="margin-bot-10px"> <hr class="margin-bot-10px">
@ -5193,8 +5198,8 @@
<div title="Tag as folder" class="tag_as_folder fa-solid fa-folder-open right_menu_button" data-i18n="[title]Use tag as folder"> <div title="Tag as folder" class="tag_as_folder fa-solid fa-folder-open right_menu_button" data-i18n="[title]Use tag as folder">
<span class="tag_folder_indicator"></span> <span class="tag_folder_indicator"></span>
</div> </div>
<div class="tagColorPickerHolder"></div> <div class="tag_view_color_picker" data-value="color"></div>
<div class="tagColorPicker2Holder"></div> <div class="tag_view_color_picker" data-value="color2"></div>
<div class="tag_view_name" contenteditable="true"></div> <div class="tag_view_name" contenteditable="true"></div>
<div class="tag_view_counter"><span class="tag_view_counter_value"></span>&nbsp;entries</div> <div class="tag_view_counter"><span class="tag_view_counter_value"></span>&nbsp;entries</div>
<div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div> <div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div>
@ -5416,15 +5421,15 @@
<div class="flex-container justifySpaceBetween"> <div class="flex-container justifySpaceBetween">
<small for="group"> <small for="group">
<span data-i18n="Inclusion Group">Inclusion Group</span> <span data-i18n="Inclusion Group">Inclusion Group</span>
<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.&#13;Supports multiple comma-separated groups.&#13;&#13;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.&#13;&#13;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.&#13;Supports multiple comma-separated groups.&#13;&#13;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> <span class="fa-solid fa-circle-question note-link-span"></span>
</a> </a>
</small> </small>
<label class="checkbox_label flexNoGap margin-r5" for="groupOverride"> <label class="checkbox_label flexNoGap margin-r5" for="groupOverride">
<input type="checkbox" name="groupOverride" /> <input type="checkbox" name="groupOverride" />
<span> <span>
<small data-i18n="Prioritize Inclusion" title="Prioritize this entry: When checked, this entry is prioritized out of all selections.&#13;If multiple are prioritized, the one with the highest 'Order' is chosen.&#13;" data-i18n="[title]Prioritize this entry: When checked, this entry is prioritized out of all selections.&#13;If multiple are prioritized, the one with the highest 'Order' is chosen."> <small title="Prioritize this entry: When checked, this entry is prioritized out of all selections.&#13;If multiple are prioritized, the one with the highest 'Order' is chosen.&#13;" data-i18n="[title]Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.">
Prioritize Inclusion <span data-i18n="Prioritize">Prioritize</span>
<div class="fa-solid fa-circle-info opacity50p"></div> <div class="fa-solid fa-circle-info opacity50p"></div>
</small> </small>
</span> </span>
@ -5467,7 +5472,7 @@
</div> </div>
</div> </div>
<div id="character_template" class="template_element"> <div id="character_template" class="template_element">
<div class="character_select flex-container wide100p alignitemsflexstart" chid="" id=""> <div class="character_select entity_block flex-container wide100p alignitemsflexstart" chid="" id="">
<div class="avatar" title=""> <div class="avatar" title="">
<img src=""> <img src="">
</div> </div>
@ -5499,7 +5504,6 @@
</form> </form>
</div> </div>
</div> </div>
</div>
<div id="logit_bias_template" class="template_element"> <div id="logit_bias_template" class="template_element">
<div class="logit_bias_form"> <div class="logit_bias_form">
<input class="logit_bias_text text_pole" data-i18n="[placeholder]Type here..." placeholder="type here..." /> <input class="logit_bias_text text_pole" data-i18n="[placeholder]Type here..." placeholder="type here..." />
@ -5617,21 +5621,21 @@
</div> </div>
</div> </div>
<div class="mes_buttons"> <div class="mes_buttons">
<div title="Message Actions" class="extraMesButtonsHint fa-solid fa-ellipsis" data-i18n="[title]Message Actions"></div> <div title="Message Actions" class="mes_button extraMesButtonsHint fa-solid fa-ellipsis" data-i18n="[title]Message Actions"></div>
<div class="extraMesButtons"> <div class="extraMesButtons">
<div title="Translate message" class="mes_translate fa-solid fa-language" data-i18n="[title]Translate message"></div> <div title="Translate message" class="mes_button mes_translate fa-solid fa-language" data-i18n="[title]Translate message"></div>
<div title="Generate Image" class="sd_message_gen fa-solid fa-paintbrush" data-i18n="[title]Generate Image"></div> <div title="Generate Image" class="mes_button sd_message_gen fa-solid fa-paintbrush" data-i18n="[title]Generate Image"></div>
<div title="Narrate" class="mes_narrate fa-solid fa-bullhorn" data-i18n="[title]Narrate"></div> <div title="Narrate" class="mes_button mes_narrate fa-solid fa-bullhorn" data-i18n="[title]Narrate"></div>
<div title="Prompt" class="mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt" style="display: none;"></div> <div title="Prompt" class="mes_button mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt" style="display: none;"></div>
<div title="Exclude message from prompts" class="mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div> <div title="Exclude message from prompts" class="mes_button mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div>
<div title="Include message in prompts" class="mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div> <div title="Include message in prompts" class="mes_button mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div>
<div title="Embed file or image" class="mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div> <div title="Embed file or image" class="mes_button mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div>
<div title="Create checkpoint" class="mes_create_bookmark fa-regular fa-solid fa-flag-checkered" data-i18n="[title]Create checkpoint"></div> <div title="Create checkpoint" class="mes_button mes_create_bookmark fa-regular fa-solid fa-flag-checkered" data-i18n="[title]Create checkpoint"></div>
<div title="Create branch" class="mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div> <div title="Create branch" class="mes_button mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
<div title="Copy" class="mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div> <div title="Copy" class="mes_button mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
</div> </div>
<div title="Open checkpoint chat" class="mes_bookmark fa-solid fa-flag" data-i18n="[title]Open checkpoint chat"></div> <div title="Open checkpoint chat" class="mes_button mes_bookmark fa-solid fa-flag" data-i18n="[title]Open checkpoint chat"></div>
<div title="Edit" class="mes_edit fa-solid fa-pencil " data-i18n="[title]Edit"></div> <div title="Edit" class="mes_button mes_edit fa-solid fa-pencil " data-i18n="[title]Edit"></div>
</div> </div>
<div class="mes_edit_buttons"> <div class="mes_edit_buttons">
<div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div> <div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
@ -5769,7 +5773,7 @@
</div> </div>
</div> </div>
<div id="group_list_template" class="template_element"> <div id="group_list_template" class="template_element">
<div class="group_select flex-container wide100p alignitemsflexstart"> <div class="group_select entity_block flex-container wide100p alignitemsflexstart">
<div class="avatar"> <div class="avatar">
<img src=""> <img src="">
</div> </div>
@ -5787,7 +5791,7 @@
</div> </div>
</div> </div>
<div id="bogus_folder_template" class="template_element"> <div id="bogus_folder_template" class="template_element">
<div class="bogus_folder_select flex-container wide100p alignitemsflexstart"> <div class="bogus_folder_select entity_block flex-container wide100p alignitemsflexstart">
<div class="avatar flex alignitemscenter textAlignCenter"> <div class="avatar flex alignitemscenter textAlignCenter">
<i class="bogus_folder_icon fa-solid fa-xl"></i> <i class="bogus_folder_icon fa-solid fa-xl"></i>
</div> </div>
@ -5833,8 +5837,8 @@
</small> </small>
<hr> <hr>
<div class="alternate_greetings_list flexFlowColumn flex-container wide100p"> <div class="alternate_greetings_list flexFlowColumn flex-container wide100p">
<strong class="alternate_grettings_hint margin-bot-10px" data-i18n="Alternate Greetings Hint"> <strong class="alternate_grettings_hint margin-bot-10px">
Click the <i class="fa-solid fa-plus"></i> button to get started! <span data-i18n="alternate_greetings_hint_1">Click the</span> <i class="fa-solid fa-plus"></i> <span data-i18n="alternate_greetings_hint_2">button to get started!</span>
</strong> </strong>
</div> </div>
</div> </div>
@ -5842,7 +5846,7 @@
<div id="alternate_greeting_form_template" class="template_element"> <div id="alternate_greeting_form_template" class="template_element">
<div class="alternate_greeting"> <div class="alternate_greeting">
<div class="title_restorable"> <div class="title_restorable">
<strong>Alternate Greeting #<span class="greeting_index"></span></strong> <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
<div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div> <div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>
</div> </div>
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text" maxlength="50000" value="" autocomplete="off" rows="16"></textarea> <textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text" maxlength="50000" value="" autocomplete="off" rows="16"></textarea>
@ -6251,7 +6255,7 @@
</form> </form>
<div id="nonQRFormItems"> <div id="nonQRFormItems">
<div id="leftSendForm" class="alignContentCenter"> <div id="leftSendForm" class="alignContentCenter">
<div id="options_button" class="fa-solid fa-bars"></div> <div id="options_button" class="fa-solid fa-bars interactable"></div>
</div> </div>
<textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea> <textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea>
<div id="rightSendForm" class="alignContentCenter"> <div id="rightSendForm" class="alignContentCenter">
@ -6267,8 +6271,8 @@
<div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request"> <div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
<i class="fa-solid fa-circle-stop"></i> <i class="fa-solid fa-circle-stop"></i>
</div> </div>
<div id="mes_continue" class="fa-fw fa-solid fa-arrow-right displayNone" title="Continue the last message" data-i18n="[title]Continue the last message"></div> <div id="mes_continue" class="fa-fw fa-solid fa-arrow-right interactable displayNone" title="Continue the last message" data-i18n="[title]Continue the last message"></div>
<div id="send_but" class="fa-solid fa-paper-plane displayNone" title="Send a message" data-i18n="[title]Send a message"></div> <div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
</div> </div>
</div> </div>
</div> </div>
@ -6432,15 +6436,6 @@
<script type="module" src="scripts/setting-search.js"></script> <script type="module" src="scripts/setting-search.js"></script>
<script type="module" src="scripts/server-history.js"></script> <script type="module" src="scripts/server-history.js"></script>
<script type="module" src="script.js"></script> <script type="module" src="script.js"></script>
<script>
// Configure toast library:
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
toastr.options.timeOut = 4000; // How long the toast will display without user interaction
toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it
toastr.options.progressBar = true; // Visually indicate how long before a toast expires.
toastr.options.closeButton = true; // enable a close button
toastr.options.positionClass = "toast-top-center"; // Where to position the toast container
</script>
<script> <script>
window.addEventListener('load', (event) => { window.addEventListener('load', (event) => {
const documentHeight = () => { const documentHeight = () => {

View File

@ -3,6 +3,7 @@
"checkJs": true, "checkJs": true,
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node",
"allowUmdGlobalAccess": true, "allowUmdGlobalAccess": true,
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "انضم إلى بطاقات الشخصيات (بما في ذلك البطاقات الصامتة)", "Join character cards (include muted)": "انضم إلى بطاقات الشخصيات (بما في ذلك البطاقات الصامتة)",
"Inserted before each part of the joined fields.": "تم إدراجه قبل كل جزء من الحقول المرتبطة.", "Inserted before each part of the joined fields.": "تم إدراجه قبل كل جزء من الحقول المرتبطة.",
"Join Prefix": "الانضمام إلى البادئة", "Join Prefix": "الانضمام إلى البادئة",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "عند تحديد \"الانضمام إلى بطاقات الأحرف\"، يتم ضم كافة الحقول الخاصة بالأحرف معًا.\rوهذا يعني أنه في سلسلة القصة على سبيل المثال، سيتم دمج جميع أوصاف الشخصيات في نص واحد كبير.\rإذا كنت تريد فصل هذه الحقول، فيمكنك تحديد بادئة أو لاحقة هنا.\r\rتدعم هذه القيمة وحدات الماكرو العادية وستستبدل أيضًا {{char}} باسم الحرف ذي الصلة و<FIELDNAME> باسم الجزء (على سبيل المثال: الوصف، والشخصية، والسيناريو، وما إلى ذلك)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "عند تحديد \"الانضمام إلى بطاقات الأحرف\"، يتم ضم كافة الحقول الخاصة بالأحرف معًا.\rوهذا يعني أنه في سلسلة القصة على سبيل المثال، سيتم دمج جميع أوصاف الشخصيات في نص واحد كبير.\rإذا كنت تريد فصل هذه الحقول، فيمكنك تحديد بادئة أو لاحقة هنا.\r\rتدعم هذه القيمة وحدات الماكرو العادية وستستبدل أيضًا {{char}} باسم الحرف ذي الصلة و<FIELDNAME> باسم الجزء (على سبيل المثال: الوصف، والشخصية، والسيناريو، وما إلى ذلك)",
"Inserted after each part of the joined fields.": "يتم إدراجه بعد كل جزء من الحقول المرتبطة.", "Inserted after each part of the joined fields.": "يتم إدراجه بعد كل جزء من الحقول المرتبطة.",
"Join Suffix": "انضم إلى لاحقة", "Join Suffix": "انضم إلى لاحقة",
"Set a group chat scenario": "تعيين سيناريو للمحادثة الجماعية", "Set a group chat scenario": "تعيين سيناريو للمحادثة الجماعية",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "تحرير الشخصيات جميعها", "Bulk_edit_characters": "تحرير الشخصيات جميعها",
"Bulk select all characters": "تحديد كافة الشخصيات بالجملة", "Bulk select all characters": "تحديد كافة الشخصيات بالجملة",
"Bulk delete characters": "حذف الشخصيات جميعها", "Bulk delete characters": "حذف الشخصيات جميعها",
"popup_text_save": "يحفظ", "popup-button-save": "يحفظ",
"popup_text_yes": "نعم", "popup-button-yes": "نعم",
"popup_text_no": "لا", "popup-button-no": "لا",
"popup_text_cancel": "يلغي", "popup-button-cancel": "يلغي",
"popup_text_import": "يستورد", "popup-button-import": "يستورد",
"Advanced Defininitions": "تعريفات متقدمة", "Advanced Defininitions": "تعريفات متقدمة",
"Prompt Overrides": "التجاوزات السريعة", "Prompt Overrides": "التجاوزات السريعة",
"(For Chat Completion and Instruct Mode)": "(لاستكمال الدردشة ووضع التعليمات)", "(For Chat Completion and Instruct Mode)": "(لاستكمال الدردشة ووضع التعليمات)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "استبعاد الشخصيات", "Character Exclusion": "استبعاد الشخصيات",
"-- Characters not found --": "-- الشخصيات غير موجودة --", "-- Characters not found --": "-- الشخصيات غير موجودة --",
"Inclusion Group": "مجموعة الإدراج", "Inclusion Group": "مجموعة الإدراج",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "تضمن مجموعات التضمين تنشيط إدخال واحد فقط من المجموعة في المرة الواحدة، إذا تم تشغيل عدة إدخالات.\rيدعم مجموعات متعددة مفصولة بفواصل.\r\rالتوثيق: معلومات العالم - مجموعة الإدماج", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "تضمن مجموعات التضمين تنشيط إدخال واحد فقط من المجموعة في المرة الواحدة، إذا تم تشغيل عدة إدخالات.\rيدعم مجموعات متعددة مفصولة بفواصل.\r\rالتوثيق: معلومات العالم - مجموعة الإدماج",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "إعطاء الأولوية لهذا الإدخال: عند تحديده، يتم إعطاء الأولوية لهذا الإدخال من بين جميع التحديدات.\rإذا تم تحديد الأولوية لأكثر من مجموعة، فسيتم اختيار \"الترتيب\" الأعلى.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "إعطاء الأولوية لهذا الإدخال: عند تحديده، يتم إعطاء الأولوية لهذا الإدخال من بين جميع التحديدات.\rإذا تم تحديد الأولوية لأكثر من مجموعة، فسيتم اختيار \"الترتيب\" الأعلى.",
"Only one entry with the same label will be activated": "سيتم تنشيط مدخل واحد فقط بنفس العلامة", "Only one entry with the same label will be activated": "سيتم تنشيط مدخل واحد فقط بنفس العلامة",
"A relative likelihood of entry activation within the group": "احتمال نسبي لتفعيل الدخول داخل المجموعة", "A relative likelihood of entry activation within the group": "احتمال نسبي لتفعيل الدخول داخل المجموعة",
"Group Weight": "وزن المجموعة", "Group Weight": "وزن المجموعة",
@ -1436,7 +1436,7 @@
"Reset Code:": "إعادة تعيين الرمز:", "Reset Code:": "إعادة تعيين الرمز:",
"Want to update?": "هل ترغب في التحديث؟", "Want to update?": "هل ترغب في التحديث؟",
"How to start chatting?": "كيف تبدأ في المحادثة؟", "How to start chatting?": "كيف تبدأ في المحادثة؟",
"Click ": "انقر", "Click _space": "انقر",
"and select a": "وحدد أ", "and select a": "وحدد أ",
"Chat API": "واجهة برمجة تطبيقات الدردشة", "Chat API": "واجهة برمجة تطبيقات الدردشة",
"and pick a character.": "واختر شخصية.", "and pick a character.": "واختر شخصية.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Charakterkarten beitreten (auch stummgeschaltet)", "Join character cards (include muted)": "Charakterkarten beitreten (auch stummgeschaltet)",
"Inserted before each part of the joined fields.": "Wird vor jedem Teil der verbundenen Felder eingefügt.", "Inserted before each part of the joined fields.": "Wird vor jedem Teil der verbundenen Felder eingefügt.",
"Join Prefix": "Präfix beitreten", "Join Prefix": "Präfix beitreten",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Wenn „Charakterkarten zusammenfügen“ ausgewählt ist, werden alle entsprechenden Felder der Charaktere zusammengefügt. Das bedeutet, dass im Story-String beispielsweise alle Charakterbeschreibungen zu einem großen Text zusammengefügt werden. Wenn Sie möchten, dass diese Felder getrennt werden, können Sie hier ein Präfix oder Suffix definieren. Dieser Wert unterstützt normale Makros und ersetzt außerdem {{char}} durch den Namen des entsprechenden Charakters und <FIELDNAME> durch den Namen des Teils (z. B.: Beschreibung, Persönlichkeit, Szenario usw.).", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Wenn „Charakterkarten zusammenfügen“ ausgewählt ist, werden alle entsprechenden Felder der Charaktere zusammengefügt. Das bedeutet, dass im Story-String beispielsweise alle Charakterbeschreibungen zu einem großen Text zusammengefügt werden. Wenn Sie möchten, dass diese Felder getrennt werden, können Sie hier ein Präfix oder Suffix definieren. Dieser Wert unterstützt normale Makros und ersetzt außerdem {{char}} durch den Namen des entsprechenden Charakters und <FIELDNAME> durch den Namen des Teils (z. B.: Beschreibung, Persönlichkeit, Szenario usw.).",
"Inserted after each part of the joined fields.": "Wird nach jedem Teil der verbundenen Felder eingefügt.", "Inserted after each part of the joined fields.": "Wird nach jedem Teil der verbundenen Felder eingefügt.",
"Join Suffix": "Join-Suffix", "Join Suffix": "Join-Suffix",
"Set a group chat scenario": "Setze ein Gruppenchat-Szenario", "Set a group chat scenario": "Setze ein Gruppenchat-Szenario",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Massenbearbeitung von Charakteren", "Bulk_edit_characters": "Massenbearbeitung von Charakteren",
"Bulk select all characters": "Massenauswahl aller Zeichen", "Bulk select all characters": "Massenauswahl aller Zeichen",
"Bulk delete characters": "Massenlöschung von Charakteren", "Bulk delete characters": "Massenlöschung von Charakteren",
"popup_text_save": "Speichern", "popup-button-save": "Speichern",
"popup_text_yes": "Ja", "popup-button-yes": "Ja",
"popup_text_no": "NEIN", "popup-button-no": "NEIN",
"popup_text_cancel": "Stornieren", "popup-button-cancel": "Stornieren",
"popup_text_import": "Importieren", "popup-button-import": "Importieren",
"Advanced Defininitions": "Erweiterte Definitionen", "Advanced Defininitions": "Erweiterte Definitionen",
"Prompt Overrides": "Eingabeaufforderungsüberschreibungen", "Prompt Overrides": "Eingabeaufforderungsüberschreibungen",
"(For Chat Completion and Instruct Mode)": "(Für Chat-Abschluss und Anweisungsmodus)", "(For Chat Completion and Instruct Mode)": "(Für Chat-Abschluss und Anweisungsmodus)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Charakterausschluss", "Character Exclusion": "Charakterausschluss",
"-- Characters not found --": "-- Charaktere nicht gefunden --", "-- Characters not found --": "-- Charaktere nicht gefunden --",
"Inclusion Group": "Einschlussgruppe", "Inclusion Group": "Einschlussgruppe",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "Inklusionsgruppen stellen sicher, dass immer nur ein Eintrag aus einer Gruppe aktiviert wird, wenn mehrere ausgelöst werden. Unterstützt mehrere durch Kommas getrennte Gruppen. Dokumentation: World Info - Inclusion Group", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Inklusionsgruppen stellen sicher, dass immer nur ein Eintrag aus einer Gruppe aktiviert wird, wenn mehrere ausgelöst werden. Unterstützt mehrere durch Kommas getrennte Gruppen. Dokumentation: World Info - Inclusion Group",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Diesen Eintrag priorisieren: Wenn diese Option aktiviert ist, wird dieser Eintrag aus allen Auswahlen priorisiert. Wenn mehrere priorisiert sind, wird der Eintrag mit der höchsten „Reihenfolge“ ausgewählt.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Diesen Eintrag priorisieren: Wenn diese Option aktiviert ist, wird dieser Eintrag aus allen Auswahlen priorisiert. Wenn mehrere priorisiert sind, wird der Eintrag mit der höchsten „Reihenfolge“ ausgewählt.",
"Only one entry with the same label will be activated": "Nur ein Eintrag mit demselben Label wird aktiviert", "Only one entry with the same label will be activated": "Nur ein Eintrag mit demselben Label wird aktiviert",
"A relative likelihood of entry activation within the group": "Eine relative Wahrscheinlichkeit der Eintrittsaktivierung innerhalb der Gruppe", "A relative likelihood of entry activation within the group": "Eine relative Wahrscheinlichkeit der Eintrittsaktivierung innerhalb der Gruppe",
"Group Weight": "Gruppengewicht", "Group Weight": "Gruppengewicht",
@ -1436,7 +1436,7 @@
"Reset Code:": "Code zurücksetzen:", "Reset Code:": "Code zurücksetzen:",
"Want to update?": "Möchten Sie aktualisieren?", "Want to update?": "Möchten Sie aktualisieren?",
"How to start chatting?": "Wie fange ich an zu chatten?", "How to start chatting?": "Wie fange ich an zu chatten?",
"Click ": "Klicken", "Click _space": "Klicken",
"and select a": "und wähle eine", "and select a": "und wähle eine",
"Chat API": "Chat-API", "Chat API": "Chat-API",
"and pick a character.": "und wähle einen Charakter aus.", "and pick a character.": "und wähle einen Charakter aus.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Unirse a tarjetas de personajes (incluidas silenciadas)", "Join character cards (include muted)": "Unirse a tarjetas de personajes (incluidas silenciadas)",
"Inserted before each part of the joined fields.": "Insertado antes de cada parte de los campos unidos.", "Inserted before each part of the joined fields.": "Insertado antes de cada parte de los campos unidos.",
"Join Prefix": "Unirse al prefijo", "Join Prefix": "Unirse al prefijo",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Cuando se selecciona 'Unir tarjetas de personajes', todos los campos respectivos de los personajes se unen.\rEsto significa que en la cadena de la historia, por ejemplo, todas las descripciones de los personajes se unirán en un texto grande.\rSi desea que esos campos estén separados, puede definir un prefijo o sufijo aquí.\r\rEste valor admite macros normales y también reemplazará {{char}} con el nombre del personaje relevante y <FIELDNAME> con el nombre de la parte (por ejemplo: descripción, personalidad, escenario, etc.)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Cuando se selecciona 'Unir tarjetas de personajes', todos los campos respectivos de los personajes se unen.\rEsto significa que en la cadena de la historia, por ejemplo, todas las descripciones de los personajes se unirán en un texto grande.\rSi desea que esos campos estén separados, puede definir un prefijo o sufijo aquí.\r\rEste valor admite macros normales y también reemplazará {{char}} con el nombre del personaje relevante y <FIELDNAME> con el nombre de la parte (por ejemplo: descripción, personalidad, escenario, etc.)",
"Inserted after each part of the joined fields.": "Insertado después de cada parte de los campos unidos.", "Inserted after each part of the joined fields.": "Insertado después de cada parte de los campos unidos.",
"Join Suffix": "Unirse al sufijo", "Join Suffix": "Unirse al sufijo",
"Set a group chat scenario": "Establecer un escenario de chat grupal", "Set a group chat scenario": "Establecer un escenario de chat grupal",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Editar personajes masivamente", "Bulk_edit_characters": "Editar personajes masivamente",
"Bulk select all characters": "Seleccionar de forma masiva todos los personajes", "Bulk select all characters": "Seleccionar de forma masiva todos los personajes",
"Bulk delete characters": "Eliminar personajes masivamente", "Bulk delete characters": "Eliminar personajes masivamente",
"popup_text_save": "Ahorrar", "popup-button-save": "Ahorrar",
"popup_text_yes": "Sí", "popup-button-yes": "Sí",
"popup_text_no": "No", "popup-button-no": "No",
"popup_text_cancel": "Cancelar", "popup-button-cancel": "Cancelar",
"popup_text_import": "Importar", "popup-button-import": "Importar",
"Advanced Defininitions": "Definiciones avanzadas", "Advanced Defininitions": "Definiciones avanzadas",
"Prompt Overrides": "Anulaciones rápidas", "Prompt Overrides": "Anulaciones rápidas",
"(For Chat Completion and Instruct Mode)": "(Para completar el chat y el modo de instrucción)", "(For Chat Completion and Instruct Mode)": "(Para completar el chat y el modo de instrucción)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Exclusión de Personaje", "Character Exclusion": "Exclusión de Personaje",
"-- Characters not found --": "-- Personajes no encontrados --", "-- Characters not found --": "-- Personajes no encontrados --",
"Inclusion Group": "Grupo de Inclusión", "Inclusion Group": "Grupo de Inclusión",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "Los grupos de inclusión garantizan que solo se active una entrada de un grupo a la vez, si se activan varias.\rAdmite múltiples grupos separados por comas.\r\rDocumentación: World Info - Grupo de Inclusión", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Los grupos de inclusión garantizan que solo se active una entrada de un grupo a la vez, si se activan varias.\rAdmite múltiples grupos separados por comas.\r\rDocumentación: World Info - Grupo de Inclusión",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Priorizar esta entrada: cuando está marcada, esta entrada tiene prioridad entre todas las selecciones.\rSi se priorizan varios, se elige el que tenga el 'Orden' más alto.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Priorizar esta entrada: cuando está marcada, esta entrada tiene prioridad entre todas las selecciones.\rSi se priorizan varios, se elige el que tenga el 'Orden' más alto.",
"Only one entry with the same label will be activated": "Solo se activará una entrada con la misma etiqueta", "Only one entry with the same label will be activated": "Solo se activará una entrada con la misma etiqueta",
"A relative likelihood of entry activation within the group": "Una probabilidad relativa de activación de entrada dentro del grupo.", "A relative likelihood of entry activation within the group": "Una probabilidad relativa de activación de entrada dentro del grupo.",
"Group Weight": "Peso del grupo", "Group Weight": "Peso del grupo",
@ -1436,7 +1436,7 @@
"Reset Code:": "Restablecer Código:", "Reset Code:": "Restablecer Código:",
"Want to update?": "¿Quieres actualizar?", "Want to update?": "¿Quieres actualizar?",
"How to start chatting?": "¿Cómo empezar a chatear?", "How to start chatting?": "¿Cómo empezar a chatear?",
"Click ": "Hacer clic", "Click _space": "Hacer clic",
"and select a": "y selecciona una", "and select a": "y selecciona una",
"Chat API": " API de chat", "Chat API": " API de chat",
"and pick a character.": "y elige un personaje.", "and pick a character.": "y elige un personaje.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Rejoignez les cartes de personnage (y compris en sourdine)", "Join character cards (include muted)": "Rejoignez les cartes de personnage (y compris en sourdine)",
"Inserted before each part of the joined fields.": "Inséré avant chaque partie des champs joints.", "Inserted before each part of the joined fields.": "Inséré avant chaque partie des champs joints.",
"Join Prefix": "Rejoindre le préfixe", "Join Prefix": "Rejoindre le préfixe",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Lorsque « Rejoindre les cartes de personnage » est sélectionné, tous les champs respectifs des personnages sont réunis.\rCela signifie que dans la chaîne d'histoire, par exemple, toutes les descriptions des personnages seront réunies en un seul grand texte.\rSi vous souhaitez que ces champs soient séparés, vous pouvez définir ici un préfixe ou un suffixe.\r\rCette valeur prend en charge les macros normales et remplacera également {{char}} par le nom du caractère concerné et <FIELDNAME> par le nom de la pièce (par exemple : description, personnalité, scénario, etc.)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Lorsque « Rejoindre les cartes de personnage » est sélectionné, tous les champs respectifs des personnages sont réunis.\rCela signifie que dans la chaîne d'histoire, par exemple, toutes les descriptions des personnages seront réunies en un seul grand texte.\rSi vous souhaitez que ces champs soient séparés, vous pouvez définir ici un préfixe ou un suffixe.\r\rCette valeur prend en charge les macros normales et remplacera également {{char}} par le nom du caractère concerné et <FIELDNAME> par le nom de la pièce (par exemple : description, personnalité, scénario, etc.)",
"Inserted after each part of the joined fields.": "Inséré après chaque partie des champs joints.", "Inserted after each part of the joined fields.": "Inséré après chaque partie des champs joints.",
"Join Suffix": "Rejoindre le suffixe", "Join Suffix": "Rejoindre le suffixe",
"Set a group chat scenario": "Définir un scénario de discussion de groupe", "Set a group chat scenario": "Définir un scénario de discussion de groupe",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Édition en masse des personnages", "Bulk_edit_characters": "Édition en masse des personnages",
"Bulk select all characters": "Sélection groupée de tous les caractères", "Bulk select all characters": "Sélection groupée de tous les caractères",
"Bulk delete characters": "Suppression en masse des personnages", "Bulk delete characters": "Suppression en masse des personnages",
"popup_text_save": "Sauvegarder", "popup-button-save": "Sauvegarder",
"popup_text_yes": "Oui", "popup-button-yes": "Oui",
"popup_text_no": "Non", "popup-button-no": "Non",
"popup_text_cancel": "Annuler", "popup-button-cancel": "Annuler",
"popup_text_import": "Importer", "popup-button-import": "Importer",
"Advanced Defininitions": "Définitions avancées", "Advanced Defininitions": "Définitions avancées",
"Prompt Overrides": "Remplacements d'invite", "Prompt Overrides": "Remplacements d'invite",
"(For Chat Completion and Instruct Mode)": "(Pour l'achèvement du chat et le mode instruction)", "(For Chat Completion and Instruct Mode)": "(Pour l'achèvement du chat et le mode instruction)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Exclusion de personnage", "Character Exclusion": "Exclusion de personnage",
"-- Characters not found --": "-- Personnages non trouvés --", "-- Characters not found --": "-- Personnages non trouvés --",
"Inclusion Group": "Groupe d'inclusion", "Inclusion Group": "Groupe d'inclusion",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "Les groupes d'inclusion garantissent qu'une seule entrée d'un groupe est activée à la fois, si plusieurs sont déclenchées.\rPrend en charge plusieurs groupes séparés par des virgules.\r\rDocumentation : Info Monde - Groupe Inclusion", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Les groupes d'inclusion garantissent qu'une seule entrée d'un groupe est activée à la fois, si plusieurs sont déclenchées.\rPrend en charge plusieurs groupes séparés par des virgules.\r\rDocumentation : Info Monde - Groupe Inclusion",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Prioriser cette entrée : Lorsque cette case est cochée, cette entrée est prioritaire parmi toutes les sélections.\rSi plusieurs sont prioritaires, celui avec l'ordre le plus élevé est choisi.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Prioriser cette entrée : Lorsque cette case est cochée, cette entrée est prioritaire parmi toutes les sélections.\rSi plusieurs sont prioritaires, celui avec l'ordre le plus élevé est choisi.",
"Only one entry with the same label will be activated": "Seule une entrée avec la même étiquette sera activée", "Only one entry with the same label will be activated": "Seule une entrée avec la même étiquette sera activée",
"A relative likelihood of entry activation within the group": "Une probabilité relative dactivation dentrée au sein du groupe", "A relative likelihood of entry activation within the group": "Une probabilité relative dactivation dentrée au sein du groupe",
"Group Weight": "Poids du groupe", "Group Weight": "Poids du groupe",
@ -1436,7 +1436,7 @@
"Reset Code:": "Code de réinitialisation :", "Reset Code:": "Code de réinitialisation :",
"Want to update?": "Envie de mettre à jour ?", "Want to update?": "Envie de mettre à jour ?",
"How to start chatting?": "Comment commencer à discuter ?", "How to start chatting?": "Comment commencer à discuter ?",
"Click ": "Cliquez sur", "Click _space": "Cliquez sur",
"and select a": "et sélectionnez un", "and select a": "et sélectionnez un",
"Chat API": "API de chat", "Chat API": "API de chat",
"and pick a character.": "et choisissez un personnage.", "and pick a character.": "et choisissez un personnage.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Tengdu persónuspjöld (innifalin þögguð)", "Join character cards (include muted)": "Tengdu persónuspjöld (innifalin þögguð)",
"Inserted before each part of the joined fields.": "Sett inn fyrir hvern hluta sameinaðra reita.", "Inserted before each part of the joined fields.": "Sett inn fyrir hvern hluta sameinaðra reita.",
"Join Prefix": "Skráðu þig í Forskeyti", "Join Prefix": "Skráðu þig í Forskeyti",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Þegar 'Tengdu persónukort' er valið er verið að tengja saman alla viðkomandi reitir persónanna.\rÞetta þýðir að í sögustrengnum verða til dæmis allar persónulýsingar sameinaðar í einn stóran texta.\rEf þú vilt að þessir reitir séu aðskildir geturðu skilgreint forskeyti eða viðskeyti hér.\r\rÞetta gildi styður venjulega fjölva og mun einnig skipta út {{char}} fyrir viðkomandi bleikjunafni og <FIELDNAME> fyrir nafn hlutans (t.d.: lýsing, persónuleiki, atburðarás osfrv.)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Þegar 'Tengdu persónukort' er valið er verið að tengja saman alla viðkomandi reitir persónanna.\rÞetta þýðir að í sögustrengnum verða til dæmis allar persónulýsingar sameinaðar í einn stóran texta.\rEf þú vilt að þessir reitir séu aðskildir geturðu skilgreint forskeyti eða viðskeyti hér.\r\rÞetta gildi styður venjulega fjölva og mun einnig skipta út {{char}} fyrir viðkomandi bleikjunafni og <FIELDNAME> fyrir nafn hlutans (t.d.: lýsing, persónuleiki, atburðarás osfrv.)",
"Inserted after each part of the joined fields.": "Sett inn á eftir hverjum hluta sameinaðra reita.", "Inserted after each part of the joined fields.": "Sett inn á eftir hverjum hluta sameinaðra reita.",
"Join Suffix": "Skráðu þig í viðskeyti", "Join Suffix": "Skráðu þig í viðskeyti",
"Set a group chat scenario": "Setja hópspjallsskipulag", "Set a group chat scenario": "Setja hópspjallsskipulag",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Breyta mörgum persónum í einu", "Bulk_edit_characters": "Breyta mörgum persónum í einu",
"Bulk select all characters": "Velja alla stafi í magni", "Bulk select all characters": "Velja alla stafi í magni",
"Bulk delete characters": "Eyða mörgum persónum í einu", "Bulk delete characters": "Eyða mörgum persónum í einu",
"popup_text_save": "Vista", "popup-button-save": "Vista",
"popup_text_yes": "Já", "popup-button-yes": "Já",
"popup_text_no": "Nei", "popup-button-no": "Nei",
"popup_text_cancel": "Hætta við", "popup-button-cancel": "Hætta við",
"popup_text_import": "Flytja inn", "popup-button-import": "Flytja inn",
"Advanced Defininitions": "Ítarleg skilgreiningar", "Advanced Defininitions": "Ítarleg skilgreiningar",
"Prompt Overrides": "Hnekkja hvetjandi", "Prompt Overrides": "Hnekkja hvetjandi",
"(For Chat Completion and Instruct Mode)": "(Til að ljúka spjalli og leiðbeiningarham)", "(For Chat Completion and Instruct Mode)": "(Til að ljúka spjalli og leiðbeiningarham)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Persónuúteslutningur", "Character Exclusion": "Persónuúteslutningur",
"-- Characters not found --": "-- Persónur finnast ekki --", "-- Characters not found --": "-- Persónur finnast ekki --",
"Inclusion Group": "Innifólgur Hópur", "Inclusion Group": "Innifólgur Hópur",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "Inntökuhópar tryggja að aðeins ein færsla úr hópi sé virkjuð í einu, ef margar eru ræstar.\rStyður marga hópa aðskilda með kommum.\r\rSkjöl: World Info - Inclusion Group", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Inntökuhópar tryggja að aðeins ein færsla úr hópi sé virkjuð í einu, ef margar eru ræstar.\rStyður marga hópa aðskilda með kommum.\r\rSkjöl: World Info - Inclusion Group",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Forgangsraða þessari færslu: Þegar hakað er við þá er þessari færslu forgangsraðað úr öllu vali.\rEf mörgum er forgangsraðað er sá sem hefur hæstu 'pöntun' valin.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Forgangsraða þessari færslu: Þegar hakað er við þá er þessari færslu forgangsraðað úr öllu vali.\rEf mörgum er forgangsraðað er sá sem hefur hæstu 'pöntun' valin.",
"Only one entry with the same label will be activated": "Aðeins ein skrá með sömu merki verður virk", "Only one entry with the same label will be activated": "Aðeins ein skrá með sömu merki verður virk",
"A relative likelihood of entry activation within the group": "Hlutfallslegar líkur á inngönguvirkjun innan hópsins", "A relative likelihood of entry activation within the group": "Hlutfallslegar líkur á inngönguvirkjun innan hópsins",
"Group Weight": "Þyngd hópa", "Group Weight": "Þyngd hópa",
@ -1436,7 +1436,7 @@
"Reset Code:": "Endurstilla kóða:", "Reset Code:": "Endurstilla kóða:",
"Want to update?": "Viltu uppfæra?", "Want to update?": "Viltu uppfæra?",
"How to start chatting?": "Hvernig á að byrja að spjalla?", "How to start chatting?": "Hvernig á að byrja að spjalla?",
"Click ": "Smellur", "Click _space": "Smellur",
"and select a": "og veldu", "and select a": "og veldu",
"Chat API": "Spjall API", "Chat API": "Spjall API",
"and pick a character.": "og veldu karakter.", "and pick a character.": "og veldu karakter.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Unisci le carte dei personaggi (includi disattivate)", "Join character cards (include muted)": "Unisci le carte dei personaggi (includi disattivate)",
"Inserted before each part of the joined fields.": "Inserito prima di ogni parte dei campi uniti.", "Inserted before each part of the joined fields.": "Inserito prima di ogni parte dei campi uniti.",
"Join Prefix": "Unisciti al prefisso", "Join Prefix": "Unisciti al prefisso",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Quando viene selezionato \"Unisci carte personaggio\", tutti i rispettivi campi dei personaggi verranno uniti.\rCiò significa che nella stringa della storia, ad esempio, tutte le descrizioni dei personaggi verranno unite in un unico grande testo.\rSe desideri che questi campi siano separati, puoi definire un prefisso o un suffisso qui.\r\rQuesto valore supporta le macro normali e sostituirà anche {{char}} con il nome del carattere rilevante e <FIELDNAME> con il nome della parte (es.: descrizione, personalità, scenario, ecc.)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Quando viene selezionato \"Unisci carte personaggio\", tutti i rispettivi campi dei personaggi verranno uniti.\rCiò significa che nella stringa della storia, ad esempio, tutte le descrizioni dei personaggi verranno unite in un unico grande testo.\rSe desideri che questi campi siano separati, puoi definire un prefisso o un suffisso qui.\r\rQuesto valore supporta le macro normali e sostituirà anche {{char}} con il nome del carattere rilevante e <FIELDNAME> con il nome della parte (es.: descrizione, personalità, scenario, ecc.)",
"Inserted after each part of the joined fields.": "Inserito dopo ogni parte dei campi uniti.", "Inserted after each part of the joined fields.": "Inserito dopo ogni parte dei campi uniti.",
"Join Suffix": "Unisciti al suffisso", "Join Suffix": "Unisciti al suffisso",
"Set a group chat scenario": "Imposta uno scenario di chat di gruppo", "Set a group chat scenario": "Imposta uno scenario di chat di gruppo",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Modifica personaggi in blocco", "Bulk_edit_characters": "Modifica personaggi in blocco",
"Bulk select all characters": "Seleziona in blocco tutti i personaggi", "Bulk select all characters": "Seleziona in blocco tutti i personaggi",
"Bulk delete characters": "Elimina personaggi in blocco", "Bulk delete characters": "Elimina personaggi in blocco",
"popup_text_save": "Salva", "popup-button-save": "Salva",
"popup_text_yes": "SÌ", "popup-button-yes": "SÌ",
"popup_text_no": "NO", "popup-button-no": "NO",
"popup_text_cancel": "Annulla", "popup-button-cancel": "Annulla",
"popup_text_import": "Importare", "popup-button-import": "Importare",
"Advanced Defininitions": "Definizioni avanzate", "Advanced Defininitions": "Definizioni avanzate",
"Prompt Overrides": "Sostituzioni richieste", "Prompt Overrides": "Sostituzioni richieste",
"(For Chat Completion and Instruct Mode)": "(Per il completamento della chat e la modalità istruzione)", "(For Chat Completion and Instruct Mode)": "(Per il completamento della chat e la modalità istruzione)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Esclusione Personaggio", "Character Exclusion": "Esclusione Personaggio",
"-- Characters not found --": "-- Personaggi non trovati --", "-- Characters not found --": "-- Personaggi non trovati --",
"Inclusion Group": "Gruppo di Inclusione", "Inclusion Group": "Gruppo di Inclusione",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "I gruppi di inclusione garantiscono che venga attivata solo una voce di un gruppo alla volta, se ne vengono attivate più.\rSupporta più gruppi separati da virgole.\r\rDocumentazione: World Info Gruppo Inclusione", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "I gruppi di inclusione garantiscono che venga attivata solo una voce di un gruppo alla volta, se ne vengono attivate più.\rSupporta più gruppi separati da virgole.\r\rDocumentazione: World Info Gruppo Inclusione",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Dai priorità a questa voce: se selezionata, questa voce ha la priorità tra tutte le selezioni. Se vengono date priorità a più voci, viene scelta quella con l'\"Ordine\" più alto.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Dai priorità a questa voce: se selezionata, questa voce ha la priorità tra tutte le selezioni. Se vengono date priorità a più voci, viene scelta quella con l'\"Ordine\" più alto.",
"Only one entry with the same label will be activated": "Sarà attivato solo un unico ingresso con lo stesso etichetta", "Only one entry with the same label will be activated": "Sarà attivato solo un unico ingresso con lo stesso etichetta",
"A relative likelihood of entry activation within the group": "Una probabilità relativa di attivazione dell'ingresso all'interno del gruppo", "A relative likelihood of entry activation within the group": "Una probabilità relativa di attivazione dell'ingresso all'interno del gruppo",
"Group Weight": "Peso del gruppo", "Group Weight": "Peso del gruppo",
@ -1436,7 +1436,7 @@
"Reset Code:": "Codice di ripristino:", "Reset Code:": "Codice di ripristino:",
"Want to update?": "Vuoi aggiornare?", "Want to update?": "Vuoi aggiornare?",
"How to start chatting?": "Come iniziare a chattare?", "How to start chatting?": "Come iniziare a chattare?",
"Click ": "Clic", "Click _space": "Clic",
"and select a": " e seleziona un", "and select a": " e seleziona un",
"Chat API": " API di chat", "Chat API": " API di chat",
"and pick a character.": "e scegli un personaggio.", "and pick a character.": "e scegli un personaggio.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "キャラクターカードに参加(ミュート含む)", "Join character cards (include muted)": "キャラクターカードに参加(ミュート含む)",
"Inserted before each part of the joined fields.": "結合されたフィールドの各部分の前に挿入されます。", "Inserted before each part of the joined fields.": "結合されたフィールドの各部分の前に挿入されます。",
"Join Prefix": "結合プレフィックス", "Join Prefix": "結合プレフィックス",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "「キャラクターカードを結合」を選択すると、キャラクターのそれぞれのフィールドがすべて結合されます。つまり、たとえばストーリー文字列では、すべてのキャラクターの説明が 1 つの大きなテキストに結合されます。これらのフィールドを分離したい場合は、ここでプレフィックスまたはサフィックスを定義できます。この値は通常のマクロをサポートし、{{char}} を関連するキャラクターの名前に置き換え、<FIELDNAME> をパーツの名前 (例: 説明、性格、シナリオなど) に置き換えます。", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "「キャラクターカードを結合」を選択すると、キャラクターのそれぞれのフィールドがすべて結合されます。つまり、たとえばストーリー文字列では、すべてのキャラクターの説明が 1 つの大きなテキストに結合されます。これらのフィールドを分離したい場合は、ここでプレフィックスまたはサフィックスを定義できます。この値は通常のマクロをサポートし、{{char}} を関連するキャラクターの名前に置き換え、<FIELDNAME> をパーツの名前 (例: 説明、性格、シナリオなど) に置き換えます。",
"Inserted after each part of the joined fields.": "結合されたフィールドの各部分の後に挿入されます。", "Inserted after each part of the joined fields.": "結合されたフィールドの各部分の後に挿入されます。",
"Join Suffix": "結合サフィックス", "Join Suffix": "結合サフィックス",
"Set a group chat scenario": "グループチャットのシナリオを設定", "Set a group chat scenario": "グループチャットのシナリオを設定",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "キャラクターを一括編集", "Bulk_edit_characters": "キャラクターを一括編集",
"Bulk select all characters": "すべての文字を一括選択", "Bulk select all characters": "すべての文字を一括選択",
"Bulk delete characters": "キャラクターを一括削除", "Bulk delete characters": "キャラクターを一括削除",
"popup_text_save": "保存", "popup-button-save": "保存",
"popup_text_yes": "はい", "popup-button-yes": "はい",
"popup_text_no": "いいえ", "popup-button-no": "いいえ",
"popup_text_cancel": "キャンセル", "popup-button-cancel": "キャンセル",
"popup_text_import": "輸入", "popup-button-import": "輸入",
"Advanced Defininitions": "高度な定義", "Advanced Defininitions": "高度な定義",
"Prompt Overrides": "プロンプトのオーバーライド", "Prompt Overrides": "プロンプトのオーバーライド",
"(For Chat Completion and Instruct Mode)": "(チャット補完と指示モード用)", "(For Chat Completion and Instruct Mode)": "(チャット補完と指示モード用)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "キャラクターの除外", "Character Exclusion": "キャラクターの除外",
"-- Characters not found --": "-- キャラクターが見つかりません --", "-- Characters not found --": "-- キャラクターが見つかりません --",
"Inclusion Group": "含蓋グループ", "Inclusion Group": "含蓋グループ",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "包含グループにより、複数のエントリがトリガーされた場合、一度に 1 つのグループから 1 つのエントリのみがアクティブ化されます。カンマで区切られた複数のグループをサポートします。ドキュメント: World Info - 包含グループ", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "包含グループにより、複数のエントリがトリガーされた場合、一度に 1 つのグループから 1 つのエントリのみがアクティブ化されます。カンマで区切られた複数のグループをサポートします。ドキュメント: World Info - 包含グループ",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "このエントリを優先する: チェックすると、このエントリがすべての選択項目の中で優先されます。複数のエントリを優先する場合は、「順序」が最も高いエントリが選択されます。", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "このエントリを優先する: チェックすると、このエントリがすべての選択項目の中で優先されます。複数のエントリを優先する場合は、「順序」が最も高いエントリが選択されます。",
"Only one entry with the same label will be activated": "同じラベルのエントリが1つだけ有効になります", "Only one entry with the same label will be activated": "同じラベルのエントリが1つだけ有効になります",
"A relative likelihood of entry activation within the group": "グループ内でのエントリー活性化の相対的な可能性", "A relative likelihood of entry activation within the group": "グループ内でのエントリー活性化の相対的な可能性",
"Group Weight": "グループの重み", "Group Weight": "グループの重み",
@ -1436,7 +1436,7 @@
"Reset Code:": "リセットコード:", "Reset Code:": "リセットコード:",
"Want to update?": "更新しますか?", "Want to update?": "更新しますか?",
"How to start chatting?": "チャットを開始する方法は?", "How to start chatting?": "チャットを開始する方法は?",
"Click ": "クリック", "Click _space": "クリック",
"and select a": "そして選択します ", "and select a": "そして選択します ",
"Chat API": "チャットAPI", "Chat API": "チャットAPI",
"and pick a character.": "キャラクターを選択します。", "and pick a character.": "キャラクターを選択します。",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "캐릭터 카드 가입(뮤트 포함)", "Join character cards (include muted)": "캐릭터 카드 가입(뮤트 포함)",
"Inserted before each part of the joined fields.": "결합된 필드의 각 부분 앞에 삽입됩니다.", "Inserted before each part of the joined fields.": "결합된 필드의 각 부분 앞에 삽입됩니다.",
"Join Prefix": "접두사 가입", "Join Prefix": "접두사 가입",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "'캐릭터 카드 합치기'를 선택하면 캐릭터의 모든 해당 필드가 합쳐집니다. 즉, 스토리 문자열에서 예를 들어 모든 캐릭터 설명이 하나의 큰 텍스트로 합쳐집니다. 이러한 필드를 분리하려면 여기에서 접두사나 접미사를 정의할 수 있습니다. 이 값은 일반 매크로를 지원하고 {{char}}를 해당 캐릭터의 이름으로, <FIELDNAME>을 해당 부분의 이름(예: 설명, 성격, 시나리오 등)으로 대체합니다.", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "'캐릭터 카드 합치기'를 선택하면 캐릭터의 모든 해당 필드가 합쳐집니다. 즉, 스토리 문자열에서 예를 들어 모든 캐릭터 설명이 하나의 큰 텍스트로 합쳐집니다. 이러한 필드를 분리하려면 여기에서 접두사나 접미사를 정의할 수 있습니다. 이 값은 일반 매크로를 지원하고 {{char}}를 해당 캐릭터의 이름으로, <FIELDNAME>을 해당 부분의 이름(예: 설명, 성격, 시나리오 등)으로 대체합니다.",
"Inserted after each part of the joined fields.": "결합된 필드의 각 부분 뒤에 삽입됩니다.", "Inserted after each part of the joined fields.": "결합된 필드의 각 부분 뒤에 삽입됩니다.",
"Join Suffix": "접미사 가입", "Join Suffix": "접미사 가입",
"Set a group chat scenario": "그룹 채팅 시나리오 설정", "Set a group chat scenario": "그룹 채팅 시나리오 설정",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "대량 캐릭터 편집", "Bulk_edit_characters": "대량 캐릭터 편집",
"Bulk select all characters": "모든 문자 일괄 선택", "Bulk select all characters": "모든 문자 일괄 선택",
"Bulk delete characters": "대량 캐릭터 삭제", "Bulk delete characters": "대량 캐릭터 삭제",
"popup_text_save": "구하다", "popup-button-save": "구하다",
"popup_text_yes": "예", "popup-button-yes": "예",
"popup_text_no": "아니요", "popup-button-no": "아니요",
"popup_text_cancel": "취소", "popup-button-cancel": "취소",
"popup_text_import": "수입", "popup-button-import": "수입",
"Advanced Defininitions": "고급 정의", "Advanced Defininitions": "고급 정의",
"Prompt Overrides": "프롬프트 무시", "Prompt Overrides": "프롬프트 무시",
"(For Chat Completion and Instruct Mode)": "(채팅 완료 및 지시 모드의 경우)", "(For Chat Completion and Instruct Mode)": "(채팅 완료 및 지시 모드의 경우)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "캐릭터 제외", "Character Exclusion": "캐릭터 제외",
"-- Characters not found --": "-- 캐릭터를 찾을 수 없음 --", "-- Characters not found --": "-- 캐릭터를 찾을 수 없음 --",
"Inclusion Group": "포함 그룹", "Inclusion Group": "포함 그룹",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "포함 그룹은 여러 항목이 트리거되는 경우 그룹에서 한 번에 하나의 항목만 활성화되도록 합니다.\r쉼표로 구분된 여러 그룹을 지원합니다.\r\r문서: 세계 정보 - 포함 그룹", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "포함 그룹은 여러 항목이 트리거되는 경우 그룹에서 한 번에 하나의 항목만 활성화되도록 합니다.\r쉼표로 구분된 여러 그룹을 지원합니다.\r\r문서: 세계 정보 - 포함 그룹",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "이 항목 우선순위 지정: 선택하면 모든 선택 항목 중에서 이 항목의 우선순위가 지정됩니다.\r여러 항목의 우선순위가 높은 경우 '순서'가 가장 높은 항목이 선택됩니다.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "이 항목 우선순위 지정: 선택하면 모든 선택 항목 중에서 이 항목의 우선순위가 지정됩니다.\r여러 항목의 우선순위가 높은 경우 '순서'가 가장 높은 항목이 선택됩니다.",
"Only one entry with the same label will be activated": "동일한 라벨을 가진 항목은 하나만 활성화됩니다", "Only one entry with the same label will be activated": "동일한 라벨을 가진 항목은 하나만 활성화됩니다",
"A relative likelihood of entry activation within the group": "그룹 내 항목 활성화의 상대적 가능성", "A relative likelihood of entry activation within the group": "그룹 내 항목 활성화의 상대적 가능성",
"Group Weight": "그룹 가중치", "Group Weight": "그룹 가중치",
@ -1436,7 +1436,7 @@
"Reset Code:": "재설정 코드:", "Reset Code:": "재설정 코드:",
"Want to update?": "업데이트 하시겠습니까?", "Want to update?": "업데이트 하시겠습니까?",
"How to start chatting?": "채팅을 시작하는 방법은?", "How to start chatting?": "채팅을 시작하는 방법은?",
"Click ": "딸깍 하는 소리", "Click _space": "딸깍 하는 소리",
"and select a": "그리고 선택하십시오", "and select a": "그리고 선택하십시오",
"Chat API": "채팅 API", "Chat API": "채팅 API",
"and pick a character.": "그리고 캐릭터를 선택하세요.", "and pick a character.": "그리고 캐릭터를 선택하세요.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Sluit je aan bij karakterkaarten (inclusief gedempt)", "Join character cards (include muted)": "Sluit je aan bij karakterkaarten (inclusief gedempt)",
"Inserted before each part of the joined fields.": "Ingevoegd vóór elk deel van de samengevoegde velden.", "Inserted before each part of the joined fields.": "Ingevoegd vóór elk deel van de samengevoegde velden.",
"Join Prefix": "Sluit je aan bij Voorvoegsel", "Join Prefix": "Sluit je aan bij Voorvoegsel",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Wanneer 'Verbind karakterkaarten' is geselecteerd, worden alle respectieve velden van de karakters samengevoegd.\rDit betekent dat in de verhaalreeks bijvoorbeeld alle karakterbeschrijvingen worden samengevoegd tot één grote tekst.\rAls u wilt dat deze velden gescheiden worden, kunt u hier een voor- of achtervoegsel definiëren.\r\rDeze waarde ondersteunt normale macro's en zal ook {{char}} vervangen door de relevante naam van het char en <FIELDNAME> door de naam van het onderdeel (bijvoorbeeld: beschrijving, persoonlijkheid, scenario, etc.)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Wanneer 'Verbind karakterkaarten' is geselecteerd, worden alle respectieve velden van de karakters samengevoegd.\rDit betekent dat in de verhaalreeks bijvoorbeeld alle karakterbeschrijvingen worden samengevoegd tot één grote tekst.\rAls u wilt dat deze velden gescheiden worden, kunt u hier een voor- of achtervoegsel definiëren.\r\rDeze waarde ondersteunt normale macro's en zal ook {{char}} vervangen door de relevante naam van het char en <FIELDNAME> door de naam van het onderdeel (bijvoorbeeld: beschrijving, persoonlijkheid, scenario, etc.)",
"Inserted after each part of the joined fields.": "Ingevoegd na elk deel van de samengevoegde velden.", "Inserted after each part of the joined fields.": "Ingevoegd na elk deel van de samengevoegde velden.",
"Join Suffix": "Sluit je aan bij het achtervoegsel", "Join Suffix": "Sluit je aan bij het achtervoegsel",
"Set a group chat scenario": "Stel een scenario voor groepschat in", "Set a group chat scenario": "Stel een scenario voor groepschat in",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Massaal bewerken personages", "Bulk_edit_characters": "Massaal bewerken personages",
"Bulk select all characters": "Selecteer alle tekens in bulk", "Bulk select all characters": "Selecteer alle tekens in bulk",
"Bulk delete characters": "Massaal verwijderen personages", "Bulk delete characters": "Massaal verwijderen personages",
"popup_text_save": "Redden", "popup-button-save": "Redden",
"popup_text_yes": "Ja", "popup-button-yes": "Ja",
"popup_text_no": "Nee", "popup-button-no": "Nee",
"popup_text_cancel": "Annuleren", "popup-button-cancel": "Annuleren",
"popup_text_import": "Importeren", "popup-button-import": "Importeren",
"Advanced Defininitions": "Geavanceerde definities", "Advanced Defininitions": "Geavanceerde definities",
"Prompt Overrides": "Prompt-overschrijvingen", "Prompt Overrides": "Prompt-overschrijvingen",
"(For Chat Completion and Instruct Mode)": "(Voor voltooiing van chat en instructiemodus)", "(For Chat Completion and Instruct Mode)": "(Voor voltooiing van chat en instructiemodus)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Personage uitsluiting", "Character Exclusion": "Personage uitsluiting",
"-- Characters not found --": "-- Personages niet gevonden --", "-- Characters not found --": "-- Personages niet gevonden --",
"Inclusion Group": "Insluitingsgroep", "Inclusion Group": "Insluitingsgroep",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "Inclusiegroepen zorgen ervoor dat slechts één item uit een groep tegelijk wordt geactiveerd, als er meerdere worden geactiveerd.\rOndersteunt meerdere door komma's gescheiden groepen.\r\rDocumentatie: World Info - Inclusion Group", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Inclusiegroepen zorgen ervoor dat slechts één item uit een groep tegelijk wordt geactiveerd, als er meerdere worden geactiveerd.\rOndersteunt meerdere door komma's gescheiden groepen.\r\rDocumentatie: World Info - Inclusion Group",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Geef prioriteit aan deze invoer: Als dit selectievakje is aangevinkt, krijgt deze invoer prioriteit boven alle selecties. Als er meerdere items prioriteit hebben, wordt de invoer met de hoogste 'volgorde' gekozen.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Geef prioriteit aan deze invoer: Als dit selectievakje is aangevinkt, krijgt deze invoer prioriteit boven alle selecties. Als er meerdere items prioriteit hebben, wordt de invoer met de hoogste 'volgorde' gekozen.",
"Only one entry with the same label will be activated": "Slechts één item met hetzelfde label zal worden geactiveerd", "Only one entry with the same label will be activated": "Slechts één item met hetzelfde label zal worden geactiveerd",
"A relative likelihood of entry activation within the group": "Een relatieve waarschijnlijkheid van activatie binnen de groep", "A relative likelihood of entry activation within the group": "Een relatieve waarschijnlijkheid van activatie binnen de groep",
"Group Weight": "Groepsgewicht", "Group Weight": "Groepsgewicht",
@ -1436,7 +1436,7 @@
"Reset Code:": "Reset code:", "Reset Code:": "Reset code:",
"Want to update?": "Wil je SillyTavern updaten?", "Want to update?": "Wil je SillyTavern updaten?",
"How to start chatting?": "Hoe begin je met chatten?", "How to start chatting?": "Hoe begin je met chatten?",
"Click ": "Klik", "Click _space": "Klik",
"and select a": " en selecteer een", "and select a": " en selecteer een",
"Chat API": " Chat-API", "Chat API": " Chat-API",
"and pick a character.": "en kies een personage.", "and pick a character.": "en kies een personage.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Junte-se a cartões de personagem (inclua silenciado)", "Join character cards (include muted)": "Junte-se a cartões de personagem (inclua silenciado)",
"Inserted before each part of the joined fields.": "Inserido antes de cada parte dos campos unidos.", "Inserted before each part of the joined fields.": "Inserido antes de cada parte dos campos unidos.",
"Join Prefix": "Prefixo de adesão", "Join Prefix": "Prefixo de adesão",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Quando 'Unir cartas de personagem' é selecionado, todos os respectivos campos dos personagens são unidos.\rIsto significa que na sequência da história, por exemplo, todas as descrições dos personagens serão unidas a um grande texto.\rSe quiser que esses campos sejam separados, você pode definir um prefixo ou sufixo aqui.\r\rEste valor suporta macros normais e também substituirá {{char}} pelo nome do char relevante e <FIELDNAME> pelo nome da parte (por exemplo: descrição, personalidade, cenário, etc.)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Quando 'Unir cartas de personagem' é selecionado, todos os respectivos campos dos personagens são unidos.\rIsto significa que na sequência da história, por exemplo, todas as descrições dos personagens serão unidas a um grande texto.\rSe quiser que esses campos sejam separados, você pode definir um prefixo ou sufixo aqui.\r\rEste valor suporta macros normais e também substituirá {{char}} pelo nome do char relevante e <FIELDNAME> pelo nome da parte (por exemplo: descrição, personalidade, cenário, etc.)",
"Inserted after each part of the joined fields.": "Inserido após cada parte dos campos unidos.", "Inserted after each part of the joined fields.": "Inserido após cada parte dos campos unidos.",
"Join Suffix": "Junte-se ao sufixo", "Join Suffix": "Junte-se ao sufixo",
"Set a group chat scenario": "Definir um cenário de bate-papo em grupo", "Set a group chat scenario": "Definir um cenário de bate-papo em grupo",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Editar personagens em massa", "Bulk_edit_characters": "Editar personagens em massa",
"Bulk select all characters": "Selecione em massa todos os caracteres", "Bulk select all characters": "Selecione em massa todos os caracteres",
"Bulk delete characters": "Excluir personagens em massa", "Bulk delete characters": "Excluir personagens em massa",
"popup_text_save": "Salvar", "popup-button-save": "Salvar",
"popup_text_yes": "Sim", "popup-button-yes": "Sim",
"popup_text_no": "Não", "popup-button-no": "Não",
"popup_text_cancel": "Cancelar", "popup-button-cancel": "Cancelar",
"popup_text_import": "Importar", "popup-button-import": "Importar",
"Advanced Defininitions": "Definições Avançadas", "Advanced Defininitions": "Definições Avançadas",
"Prompt Overrides": "Substituições de prompt", "Prompt Overrides": "Substituições de prompt",
"(For Chat Completion and Instruct Mode)": "(Para conclusão de bate-papo e modo de instrução)", "(For Chat Completion and Instruct Mode)": "(Para conclusão de bate-papo e modo de instrução)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Exclusão de personagem", "Character Exclusion": "Exclusão de personagem",
"-- Characters not found --": "-- Personagens não encontrados --", "-- Characters not found --": "-- Personagens não encontrados --",
"Inclusion Group": "Grupo de inclusão", "Inclusion Group": "Grupo de inclusão",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "Os Grupos de Inclusão garantem que apenas uma entrada de um grupo seja ativada por vez, se várias forem acionadas.\rSuporta vários grupos separados por vírgula.\r\rDocumentação: World Info - Grupo de Inclusão", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Os Grupos de Inclusão garantem que apenas uma entrada de um grupo seja ativada por vez, se várias forem acionadas.\rSuporta vários grupos separados por vírgula.\r\rDocumentação: World Info - Grupo de Inclusão",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Priorizar esta entrada: quando marcada, esta entrada é priorizada entre todas as seleções.\rSe vários forem priorizados, aquele com a “Ordem” mais alta será escolhido.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Priorizar esta entrada: quando marcada, esta entrada é priorizada entre todas as seleções.\rSe vários forem priorizados, aquele com a “Ordem” mais alta será escolhido.",
"Only one entry with the same label will be activated": "Apenas uma entrada com o mesmo rótulo será ativada", "Only one entry with the same label will be activated": "Apenas uma entrada com o mesmo rótulo será ativada",
"A relative likelihood of entry activation within the group": "Uma probabilidade relativa de ativação de entrada dentro do grupo", "A relative likelihood of entry activation within the group": "Uma probabilidade relativa de ativação de entrada dentro do grupo",
"Group Weight": "Peso do grupo", "Group Weight": "Peso do grupo",
@ -1436,7 +1436,7 @@
"Reset Code:": "Reiniciar código:", "Reset Code:": "Reiniciar código:",
"Want to update?": "Quer atualizar?", "Want to update?": "Quer atualizar?",
"How to start chatting?": "Como começar a conversar?", "How to start chatting?": "Como começar a conversar?",
"Click ": "Clique", "Click _space": "Clique",
"and select a": "e selecione um", "and select a": "e selecione um",
"Chat API": "API de Chat", "Chat API": "API de Chat",
"and pick a character.": "e escolha um personagem.", "and pick a character.": "e escolha um personagem.",

File diff suppressed because it is too large Load Diff

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Приєднатися до карток персонажів (включно з приглушеними)", "Join character cards (include muted)": "Приєднатися до карток персонажів (включно з приглушеними)",
"Inserted before each part of the joined fields.": "Вставляється перед кожною частиною об’єднаних полів.", "Inserted before each part of the joined fields.": "Вставляється перед кожною частиною об’єднаних полів.",
"Join Prefix": "Префікс приєднання", "Join Prefix": "Префікс приєднання",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Якщо вибрано «Об’єднати картки персонажів», усі відповідні поля персонажів об’єднуються.\rЦе означає, що, наприклад, у рядку історії всі описи персонажів будуть об’єднані в один великий текст.\rЯкщо ви хочете, щоб ці поля були розділені, ви можете визначити тут префікс або суфікс.\r\rЦе значення підтримує звичайні макроси, а також замінює {{char}} на ім’я відповідного символу, а <FIELDNAME> — на назву частини (наприклад, опис, особистість, сценарій тощо).", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Якщо вибрано «Об’єднати картки персонажів», усі відповідні поля персонажів об’єднуються.\rЦе означає, що, наприклад, у рядку історії всі описи персонажів будуть об’єднані в один великий текст.\rЯкщо ви хочете, щоб ці поля були розділені, ви можете визначити тут префікс або суфікс.\r\rЦе значення підтримує звичайні макроси, а також замінює {{char}} на ім’я відповідного символу, а <FIELDNAME> — на назву частини (наприклад, опис, особистість, сценарій тощо).",
"Inserted after each part of the joined fields.": "Вставляється після кожної частини об’єднаних полів.", "Inserted after each part of the joined fields.": "Вставляється після кожної частини об’єднаних полів.",
"Join Suffix": "Суфікс приєднання", "Join Suffix": "Суфікс приєднання",
"Set a group chat scenario": "Встановити сценарій групового чату", "Set a group chat scenario": "Встановити сценарій групового чату",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Масове редагування персонажів", "Bulk_edit_characters": "Масове редагування персонажів",
"Bulk select all characters": "Масове виділення всіх символів", "Bulk select all characters": "Масове виділення всіх символів",
"Bulk delete characters": "Масове видалення персонажів", "Bulk delete characters": "Масове видалення персонажів",
"popup_text_save": "зберегти", "popup-button-save": "зберегти",
"popup_text_yes": "Так", "popup-button-yes": "Так",
"popup_text_no": "Немає", "popup-button-no": "Немає",
"popup_text_cancel": "Скасувати", "popup-button-cancel": "Скасувати",
"popup_text_import": "Імпорт", "popup-button-import": "Імпорт",
"Advanced Defininitions": "Розширені визначення", "Advanced Defininitions": "Розширені визначення",
"Prompt Overrides": "Перевизначення підказок", "Prompt Overrides": "Перевизначення підказок",
"(For Chat Completion and Instruct Mode)": "(Для завершення чату та режиму інструктажу)", "(For Chat Completion and Instruct Mode)": "(Для завершення чату та режиму інструктажу)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Виключення персонажів", "Character Exclusion": "Виключення персонажів",
"-- Characters not found --": "-- Персонажі не знайдені --", "-- Characters not found --": "-- Персонажі не знайдені --",
"Inclusion Group": "Група включення", "Inclusion Group": "Група включення",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "Групи включення гарантують, що одночасно активується лише один запис із групи, якщо їх активовано декілька.\rПідтримує кілька груп, розділених комами.\r\rДокументація: World Info - Inclusion Group", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Групи включення гарантують, що одночасно активується лише один запис із групи, якщо їх активовано декілька.\rПідтримує кілька груп, розділених комами.\r\rДокументація: World Info - Inclusion Group",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Пріоритезувати цей запис: якщо позначено, цей запис має пріоритет серед усіх вибраних.\rЯкщо кілька пріоритетів, вибирається той, який має найвищий «порядок».", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Пріоритезувати цей запис: якщо позначено, цей запис має пріоритет серед усіх вибраних.\rЯкщо кілька пріоритетів, вибирається той, який має найвищий «порядок».",
"Only one entry with the same label will be activated": "Буде активовано лише один запис з однією міткою", "Only one entry with the same label will be activated": "Буде активовано лише один запис з однією міткою",
"A relative likelihood of entry activation within the group": "Відносна ймовірність активації входу в групу", "A relative likelihood of entry activation within the group": "Відносна ймовірність активації входу в групу",
"Group Weight": "Вага групи", "Group Weight": "Вага групи",
@ -1436,7 +1436,7 @@
"Reset Code:": "Скинути код:", "Reset Code:": "Скинути код:",
"Want to update?": "Хочете оновити?", "Want to update?": "Хочете оновити?",
"How to start chatting?": "Як почати спілкування?", "How to start chatting?": "Як почати спілкування?",
"Click ": "Натисніть", "Click _space": "Натисніть",
"and select a": "і виберіть", "and select a": "і виберіть",
"Chat API": "API чату", "Chat API": "API чату",
"and pick a character.": "і виберіть персонажа.", "and pick a character.": "і виберіть персонажа.",

View File

@ -853,7 +853,7 @@
"Join character cards (include muted)": "Tham gia thẻ nhân vật (bao gồm cả tắt tiếng)", "Join character cards (include muted)": "Tham gia thẻ nhân vật (bao gồm cả tắt tiếng)",
"Inserted before each part of the joined fields.": "Được chèn trước mỗi phần của các trường đã nối.", "Inserted before each part of the joined fields.": "Được chèn trước mỗi phần của các trường đã nối.",
"Join Prefix": "Tham gia tiền tố", "Join Prefix": "Tham gia tiền tố",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Khi chọn 'Tham gia thẻ nhân vật', tất cả các trường tương ứng của các nhân vật sẽ được nối với nhau.\rĐiều này có nghĩa là trong chuỗi câu chuyện chẳng hạn, tất cả các mô tả nhân vật sẽ được nối thành một văn bản lớn.\rNếu muốn tách các trường đó, bạn có thể xác định tiền tố hoặc hậu tố tại đây.\r\rGiá trị này hỗ trợ các macro thông thường và cũng sẽ thay thế {{char}} bằng tên của char có liên quan và <FIELDNAME> bằng tên của phần đó (ví dụ: mô tả, tính cách, kịch bản, v.v.)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "Khi chọn 'Tham gia thẻ nhân vật', tất cả các trường tương ứng của các nhân vật sẽ được nối với nhau.\rĐiều này có nghĩa là trong chuỗi câu chuyện chẳng hạn, tất cả các mô tả nhân vật sẽ được nối thành một văn bản lớn.\rNếu muốn tách các trường đó, bạn có thể xác định tiền tố hoặc hậu tố tại đây.\r\rGiá trị này hỗ trợ các macro thông thường và cũng sẽ thay thế {{char}} bằng tên của char có liên quan và <FIELDNAME> bằng tên của phần đó (ví dụ: mô tả, tính cách, kịch bản, v.v.)",
"Inserted after each part of the joined fields.": "Được chèn sau mỗi phần của các trường đã nối.", "Inserted after each part of the joined fields.": "Được chèn sau mỗi phần của các trường đã nối.",
"Join Suffix": "Tham gia hậu tố", "Join Suffix": "Tham gia hậu tố",
"Set a group chat scenario": "Đặt một kịch bản trò chuyện nhóm", "Set a group chat scenario": "Đặt một kịch bản trò chuyện nhóm",
@ -885,11 +885,11 @@
"Bulk_edit_characters": "Chỉnh sửa nhân vật theo lô", "Bulk_edit_characters": "Chỉnh sửa nhân vật theo lô",
"Bulk select all characters": "Chọn hàng loạt tất cả các ký tự", "Bulk select all characters": "Chọn hàng loạt tất cả các ký tự",
"Bulk delete characters": "Xóa nhân vật theo lô", "Bulk delete characters": "Xóa nhân vật theo lô",
"popup_text_save": "Cứu", "popup-button-save": "Cứu",
"popup_text_yes": "Đúng", "popup-button-yes": "Đúng",
"popup_text_no": "KHÔNG", "popup-button-no": "KHÔNG",
"popup_text_cancel": "Hủy bỏ", "popup-button-cancel": "Hủy bỏ",
"popup_text_import": "Nhập khẩu", "popup-button-import": "Nhập khẩu",
"Advanced Defininitions": "Các Định nghĩa Nâng cao", "Advanced Defininitions": "Các Định nghĩa Nâng cao",
"Prompt Overrides": "Ghi đè nhắc nhở", "Prompt Overrides": "Ghi đè nhắc nhở",
"(For Chat Completion and Instruct Mode)": "(Đối với chế độ hoàn thành trò chuyện và hướng dẫn)", "(For Chat Completion and Instruct Mode)": "(Đối với chế độ hoàn thành trò chuyện và hướng dẫn)",
@ -1005,8 +1005,8 @@
"Character Exclusion": "Loại trừ Nhân vật", "Character Exclusion": "Loại trừ Nhân vật",
"-- Characters not found --": "-- Không tìm thấy Nhân vật --", "-- Characters not found --": "-- Không tìm thấy Nhân vật --",
"Inclusion Group": "Nhóm Bao gồm", "Inclusion Group": "Nhóm Bao gồm",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "Nhóm Bao gồm đảm bảo chỉ một mục từ một nhóm được kích hoạt tại một thời điểm, nếu nhiều mục được kích hoạt.\rHỗ trợ nhiều nhóm được phân tách bằng dấu phẩy.\r\rTài liệu: World Info - Inclusion Group", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Nhóm Bao gồm đảm bảo chỉ một mục từ một nhóm được kích hoạt tại một thời điểm, nếu nhiều mục được kích hoạt.\rHỗ trợ nhiều nhóm được phân tách bằng dấu phẩy.\r\rTài liệu: World Info - Inclusion Group",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "Ưu tiên mục nhập này: Khi được chọn, mục nhập này được ưu tiên trong số tất cả các lựa chọn.\rNếu nhiều ưu tiên được ưu tiên thì cái có 'Thứ tự' cao nhất sẽ được chọn.", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Ưu tiên mục nhập này: Khi được chọn, mục nhập này được ưu tiên trong số tất cả các lựa chọn.\rNếu nhiều ưu tiên được ưu tiên thì cái có 'Thứ tự' cao nhất sẽ được chọn.",
"Only one entry with the same label will be activated": "Chỉ một mục có cùng nhãn sẽ được kích hoạt", "Only one entry with the same label will be activated": "Chỉ một mục có cùng nhãn sẽ được kích hoạt",
"A relative likelihood of entry activation within the group": "Khả năng tương đối của việc kích hoạt mục nhập trong nhóm", "A relative likelihood of entry activation within the group": "Khả năng tương đối của việc kích hoạt mục nhập trong nhóm",
"Group Weight": "Trọng lượng nhóm", "Group Weight": "Trọng lượng nhóm",
@ -1436,7 +1436,7 @@
"Reset Code:": "Đặt lại mã:", "Reset Code:": "Đặt lại mã:",
"Want to update?": "Muốn cập nhật?", "Want to update?": "Muốn cập nhật?",
"How to start chatting?": "Làm thế nào để bắt đầu trò chuyện?", "How to start chatting?": "Làm thế nào để bắt đầu trò chuyện?",
"Click ": "Nhấp chuột", "Click _space": "Nhấp chuột",
"and select a": "và chọn một", "and select a": "và chọn một",
"Chat API": "API Trò chuyện", "Chat API": "API Trò chuyện",
"and pick a character.": "và chọn một nhân vật.", "and pick a character.": "và chọn một nhân vật.",

File diff suppressed because it is too large Load Diff

View File

@ -854,7 +854,7 @@
"Join character cards (include muted)": "加入角色人物卡(包括靜音)", "Join character cards (include muted)": "加入角色人物卡(包括靜音)",
"Inserted before each part of the joined fields.": "插入在合併欄位的每一部分之前。", "Inserted before each part of the joined fields.": "插入在合併欄位的每一部分之前。",
"Join Prefix": "加入前綴", "Join Prefix": "加入前綴",
"When 'Join character cards' is selected, all respective fields of the characters are being joined together.\rThis means that in the story string for example all character descriptions will be joined to one big text.\rIf you want those fields to be separated, you can define a prefix or suffix here.\r\rThis value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "當選擇“加入角色卡”時,角色的所有對應欄位將連接在一起。\r這意味著例如在故事字串中所有角色描述都將連接到一個大文字中。\r如果您希望分隔這些字段可以在此處定義前綴或後綴。\r\r該值支援普通宏並且還將 {{char}} 替換為相關字元的名稱,並將 <FIELDNAME> 替換為部分的名稱(例如:描述、個性、場景等)", "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "當選擇“加入角色卡”時,角色的所有對應欄位將連接在一起。\r這意味著例如在故事字串中所有角色描述都將連接到一個大文字中。\r如果您希望分隔這些字段可以在此處定義前綴或後綴。\r\r該值支援普通宏並且還將 {{char}} 替換為相關字元的名稱,並將 <FIELDNAME> 替換為部分的名稱(例如:描述、個性、場景等)",
"Inserted after each part of the joined fields.": "插入在合併欄位的每一部分之後。", "Inserted after each part of the joined fields.": "插入在合併欄位的每一部分之後。",
"Join Suffix": "加入後綴", "Join Suffix": "加入後綴",
"Set a group chat scenario": "設定群組聊天情境", "Set a group chat scenario": "設定群組聊天情境",
@ -886,11 +886,11 @@
"Bulk_edit_characters": "批量編輯角色人物\n\n點選以切換角色人物\nShift + 點選以選擇/取消選擇一範圍的角色人物\n右鍵以查看動作", "Bulk_edit_characters": "批量編輯角色人物\n\n點選以切換角色人物\nShift + 點選以選擇/取消選擇一範圍的角色人物\n右鍵以查看動作",
"Bulk select all characters": "全選所有角色人物", "Bulk select all characters": "全選所有角色人物",
"Bulk delete characters": "批量刪除角色人物", "Bulk delete characters": "批量刪除角色人物",
"popup_text_save": "儲存", "popup-button-save": "儲存",
"popup_text_yes": "是", "popup-button-yes": "是",
"popup_text_no": "否", "popup-button-no": "否",
"popup_text_cancel": "取消", "popup-button-cancel": "取消",
"popup_text_import": "匯入", "popup-button-import": "匯入",
"Advanced Defininitions": "- 進階定義", "Advanced Defininitions": "- 進階定義",
"Prompt Overrides": "提示詞覆寫", "Prompt Overrides": "提示詞覆寫",
"(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)", "(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)",
@ -1007,8 +1007,8 @@
"Character Exclusion": "角色人物排除", "Character Exclusion": "角色人物排除",
"-- Characters not found --": "-- 未找到角色人物 --", "-- Characters not found --": "-- 未找到角色人物 --",
"Inclusion Group": "包含的群組", "Inclusion Group": "包含的群組",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\r\rDocumentation: World Info - Inclusion Group": "如果觸發多個條目,包含群組可確保一次僅啟動一組中的一個條目。\r支援多個以逗號分隔的群組。\r\r文件世界資訊 - 包容性集團", "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "如果觸發多個條目,包含群組可確保一次僅啟動一組中的一個條目。\r支援多個以逗號分隔的群組。\r\r文件世界資訊 - 包容性集團",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.": "優先考慮此條目:選取後,此條目將在所有選擇中優先。\r如果有多個優先級則選擇「順序」最高的一個。", "Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "優先考慮此條目:選取後,此條目將在所有選擇中優先。\r如果有多個優先級則選擇「順序」最高的一個。",
"Only one entry with the same label will be activated": "僅會啟用具有相同標籤的一個條目", "Only one entry with the same label will be activated": "僅會啟用具有相同標籤的一個條目",
"A relative likelihood of entry activation within the group": "群組內條目啟用的相對可能性", "A relative likelihood of entry activation within the group": "群組內條目啟用的相對可能性",
"Group Weight": "群組權重", "Group Weight": "群組權重",
@ -1439,7 +1439,7 @@
"Reset Code:": "重設驗證碼:", "Reset Code:": "重設驗證碼:",
"Want to update?": "想要更新嗎?", "Want to update?": "想要更新嗎?",
"How to start chatting?": "如何開始聊天?", "How to start chatting?": "如何開始聊天?",
"Click ": "點選", "Click _space": "點選",
"and select a": "並選擇一個", "and select a": "並選擇一個",
"Chat API": "聊天 API", "Chat API": "聊天 API",
"and pick a character.": "並選擇一個角色人物。", "and pick a character.": "並選擇一個角色人物。",

View File

@ -26,6 +26,7 @@
<link rel="stylesheet" type="text/css" href="css/login.css"> <link rel="stylesheet" type="text/css" href="css/login.css">
<link rel="manifest" crossorigin="use-credentials" href="manifest.json"> <link rel="manifest" crossorigin="use-credentials" href="manifest.json">
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet"> <link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
<link href="webfonts/NotoSansMono/stylesheet.css" rel="stylesheet">
<!-- fontawesome webfonts--> <!-- fontawesome webfonts-->
<link href="css/fontawesome.min.css" rel="stylesheet"> <link href="css/fontawesome.min.css" rel="stylesheet">
<link href="css/solid.min.css" rel="stylesheet"> <link href="css/solid.min.css" rel="stylesheet">

View File

@ -154,7 +154,7 @@ import {
isValidUrl, isValidUrl,
ensureImageFormatSupported, ensureImageFormatSupported,
flashHighlight, flashHighlight,
checkOverwriteExistingData, isTrueBoolean,
} from './scripts/utils.js'; } from './scripts/utils.js';
import { debounce_timeout } from './scripts/constants.js'; import { debounce_timeout } from './scripts/constants.js';
@ -175,11 +175,12 @@ import {
createTagMapFromList, createTagMapFromList,
renameTagKey, renameTagKey,
importTags, importTags,
tag_filter_types, tag_filter_type,
compareTagsForSort, compareTagsForSort,
initTags, initTags,
applyTagsOnCharacterSelect, applyTagsOnCharacterSelect,
applyTagsOnGroupSelect, applyTagsOnGroupSelect,
tag_import_setting,
} from './scripts/tags.js'; } from './scripts/tags.js';
import { import {
SECRET_KEYS, SECRET_KEYS,
@ -227,14 +228,19 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de
import { initPresetManager } from './scripts/preset-manager.js'; import { initPresetManager } from './scripts/preset-manager.js';
import { evaluateMacros } from './scripts/macros.js'; import { evaluateMacros } from './scripts/macros.js';
import { currentUser, setUserControls } from './scripts/user.js'; import { currentUser, setUserControls } from './scripts/user.js';
import { POPUP_TYPE, callGenericPopup } from './scripts/popup.js'; import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js'; import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
import { ScraperManager } from './scripts/scrapers.js'; import { ScraperManager } from './scripts/scrapers.js';
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
import { SlashCommand } from './scripts/slash-commands/SlashCommand.js'; import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './scripts/slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js'; import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js'; import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
import { DragAndDropHandler } from './scripts/dragdrop.js';
import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
import { initDynamicStyles } from './scripts/dynamic-styles.js';
import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
import { enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
//exporting functions and vars for mods //exporting functions and vars for mods
export { export {
@ -263,6 +269,19 @@ showLoader();
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
document.getElementById('preloader').remove(); document.getElementById('preloader').remove();
// Configure toast library:
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
toastr.options.timeOut = 4000; // How long the toast will display without user interaction
toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it
toastr.options.progressBar = true; // Visually indicate how long before a toast expires.
toastr.options.closeButton = true; // enable a close button
toastr.options.positionClass = 'toast-top-center'; // Where to position the toast container
toastr.options.onHidden = () => {
// If we have any dialog still open, the last "hidden" toastr will remove the toastr-container. We need to keep it alive inside the dialog though
// so the toasts still show up inside there.
fixToastrForDialogs();
};
// Allow target="_blank" in links // Allow target="_blank" in links
DOMPurify.addHook('afterSanitizeAttributes', function (node) { DOMPurify.addHook('afterSanitizeAttributes', function (node) {
if ('target' in node) { if ('target' in node) {
@ -415,7 +434,9 @@ export const event_types = {
CHARACTER_MESSAGE_RENDERED: 'character_message_rendered', CHARACTER_MESSAGE_RENDERED: 'character_message_rendered',
FORCE_SET_BACKGROUND: 'force_set_background', FORCE_SET_BACKGROUND: 'force_set_background',
CHAT_DELETED: 'chat_deleted', CHAT_DELETED: 'chat_deleted',
CHAT_CREATED: 'chat_created',
GROUP_CHAT_DELETED: 'group_chat_deleted', GROUP_CHAT_DELETED: 'group_chat_deleted',
GROUP_CHAT_CREATED: 'group_chat_created',
GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts', GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts', GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
GROUP_MEMBER_DRAFTED: 'group_member_drafted', GROUP_MEMBER_DRAFTED: 'group_member_drafted',
@ -509,11 +530,15 @@ let is_delete_mode = false;
let fav_ch_checked = false; let fav_ch_checked = false;
let scrollLock = false; let scrollLock = false;
export let abortStatusCheck = new AbortController(); export let abortStatusCheck = new AbortController();
let charDragDropHandler = null;
/** @type {number} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
const durationSaveEdit = debounce_timeout.relaxed; export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed;
export const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit); /** @type {debounce_timeout} The debounce timeout used for printing. debounce_timeout.quick: 100 ms */
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit); export const DEFAULT_PRINT_TIMEOUT = debounce_timeout.quick;
export const saveSettingsDebounced = debounce(() => saveSettings(), DEFAULT_SAVE_EDIT_TIMEOUT);
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), DEFAULT_SAVE_EDIT_TIMEOUT);
/** /**
* Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds. * Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds.
@ -521,7 +546,7 @@ export const saveCharacterDebounced = debounce(() => $('#create_button').trigger
* *
* The printing will also always reprint all filter options of the global list, to keep them up to date. * The printing will also always reprint all filter options of the global list, to keep them up to date.
*/ */
export const printCharactersDebounced = debounce(() => { printCharacters(false); }, debounce_timeout.quick); export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT);
/** /**
* @enum {string} System message types * @enum {string} System message types
@ -744,8 +769,19 @@ const per_page_default = 50;
var is_advanced_char_open = false; var is_advanced_char_open = false;
export let menu_type = ''; //what is selected in the menu /**
* The type of the right menu
* @typedef {'characters' | 'character_edit' | 'create' | 'group_edit' | 'group_create' | '' } MenuType
*/
/**
* The type of the right menu that is currently open
* @type {MenuType}
*/
export let menu_type = '';
export let selected_button = ''; //which button pressed export let selected_button = ''; //which button pressed
//create pole save //create pole save
let create_save = { let create_save = {
name: '', name: '',
@ -874,6 +910,8 @@ async function firstLoadInit() {
await getSystemMessages(); await getSystemMessages();
sendSystemMessage(system_message_types.WELCOME); sendSystemMessage(system_message_types.WELCOME);
await getSettings(); await getSettings();
initKeyboard();
initDynamicStyles();
initTags(); initTags();
await getUserAvatars(true, user_avatar); await getUserAvatars(true, user_avatar);
await getCharacters(); await getCharacters();
@ -901,7 +939,7 @@ function cancelStatusCheck() {
export function displayOnlineStatus() { export function displayOnlineStatus() {
if (online_status == 'no_connection') { if (online_status == 'no_connection') {
$('.online_status_indicator').removeClass('success'); $('.online_status_indicator').removeClass('success');
$('.online_status_text').text('No connection...'); $('.online_status_text').text($('#API-status-top').attr('no_connection_text'));
} else { } else {
$('.online_status_indicator').addClass('success'); $('.online_status_indicator').addClass('success');
$('.online_status_text').text(online_status); $('.online_status_text').text(online_status);
@ -914,6 +952,8 @@ export function displayOnlineStatus() {
*/ */
export function setAnimationDuration(ms = null) { export function setAnimationDuration(ms = null) {
animation_duration = ms ?? ANIMATION_DURATION_DEFAULT; animation_duration = ms ?? ANIMATION_DURATION_DEFAULT;
// Set CSS variable to document
document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
} }
export function setActiveCharacter(entityOrKey) { export function setActiveCharacter(entityOrKey) {
@ -1309,8 +1349,8 @@ export async function printCharacters(fullRefresh = false) {
verifyCharactersSearchSortRule(); verifyCharactersSearchSortRule();
// We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date // We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date
printTagFilters(tag_filter_types.character); printTagFilters(tag_filter_type.character);
printTagFilters(tag_filter_types.group_member); printTagFilters(tag_filter_type.group_member);
// We are also always reprinting the lists on character/group edit window, as these ones doesn't get updated otherwise // We are also always reprinting the lists on character/group edit window, as these ones doesn't get updated otherwise
applyTagsOnCharacterSelect(); applyTagsOnCharacterSelect();
@ -1331,7 +1371,7 @@ export async function printCharacters(fullRefresh = false) {
nextText: '>', nextText: '>',
formatNavigator: PAGINATION_TEMPLATE, formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true, showNavigator: true,
callback: function (data) { callback: function (/** @type {Entity[]} */ data) {
$(listId).empty(); $(listId).empty();
if (power_user.bogus_folders && isBogusFolderOpen()) { if (power_user.bogus_folders && isBogusFolderOpen()) {
$(listId).append(getBackBlock()); $(listId).append(getBackBlock());
@ -1351,7 +1391,7 @@ export async function printCharacters(fullRefresh = false) {
displayCount++; displayCount++;
break; break;
case 'tag': case 'tag':
$(listId).append(getTagBlock(i.item, i.entities, i.hidden)); $(listId).append(getTagBlock(i.item, i.entities, i.hidden, i.isUseless));
break; break;
} }
} }
@ -1405,8 +1445,9 @@ function verifyCharactersSearchSortRule() {
* @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item * @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
* @property {string|number} id - The id * @property {string|number} id - The id
* @property {'character'|'group'|'tag'} 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 {Entity[]?} [entities=null] - An optional list of entities relevant for this item
* @property {number} [hidden] - An optional number representing how many hidden entities this entity contains * @property {number?} [hidden=null] - An optional number representing how many hidden entities this entity contains
* @property {boolean?} [isUseless=null] - Specifies if the entity is useless (not relevant, but should still be displayed for consistency) and should be displayed greyed out
*/ */
/** /**
@ -1501,6 +1542,15 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
} }
} }
// Final step, updating some properties after the last filter run
const nonTagEntitiesCount = entities.filter(entity => entity.type !== 'tag').length;
for (const entity of entities) {
if (entity.type === 'tag') {
if (entity.entities?.length == nonTagEntitiesCount) entity.isUseless = true;
}
}
// Sort before returning if requested
if (doSort) { if (doSort) {
sortEntitiesList(entities); sortEntitiesList(entities);
} }
@ -2074,7 +2124,7 @@ export function addCopyToCodeBlocks(messageElement) {
hljs.highlightElement(codeBlocks.get(i)); hljs.highlightElement(codeBlocks.get(i));
if (navigator.clipboard !== undefined) { if (navigator.clipboard !== undefined) {
const copyButton = document.createElement('i'); const copyButton = document.createElement('i');
copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy'); copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');
copyButton.title = 'Copy code'; copyButton.title = 'Copy code';
codeBlocks.get(i).appendChild(copyButton); codeBlocks.get(i).appendChild(copyButton);
copyButton.addEventListener('pointerup', function (event) { copyButton.addEventListener('pointerup', function (event) {
@ -2306,6 +2356,16 @@ export function scrollChatToBottom() {
} }
} }
/**
* Substitutes {{macro}} parameters in a string.
* @param {string} content - The string to substitute parameters in.
* @param {Record<string,any>} additionalMacro - Additional environment variables for substitution.
* @returns {string} The string with substituted parameters.
*/
export function substituteParamsExtended(content, additionalMacro = {}) {
return substituteParams(content, undefined, undefined, undefined, undefined, true, additionalMacro);
}
/** /**
* Substitutes {{macro}} parameters in a string. * Substitutes {{macro}} parameters in a string.
* @param {string} content - The string to substitute parameters in. * @param {string} content - The string to substitute parameters in.
@ -2314,9 +2374,14 @@ export function scrollChatToBottom() {
* @param {string} [_original] - The original message for {{original}} substitution. * @param {string} [_original] - The original message for {{original}} substitution.
* @param {string} [_group] - The group members list for {{group}} substitution. * @param {string} [_group] - The group members list for {{group}} substitution.
* @param {boolean} [_replaceCharacterCard] - Whether to replace character card macros. * @param {boolean} [_replaceCharacterCard] - Whether to replace character card macros.
* @param {Record<string,any>} [additionalMacro] - Additional environment variables for substitution.
* @returns {string} The string with substituted parameters. * @returns {string} The string with substituted parameters.
*/ */
export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) { export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}) {
if (!content) {
return '';
}
const environment = {}; const environment = {};
if (typeof _original === 'string') { if (typeof _original === 'string') {
@ -2366,6 +2431,10 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
environment.group = environment.charIfNotGroup = getGroupValue(); environment.group = environment.charIfNotGroup = getGroupValue();
environment.model = getGeneratingModel(); environment.model = getGeneratingModel();
if (additionalMacro && typeof additionalMacro === 'object') {
Object.assign(environment, additionalMacro);
}
return evaluateMacros(content, environment); return evaluateMacros(content, environment);
} }
@ -3389,7 +3458,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
* @returns {string[]} Examples array with block heading * @returns {string[]} Examples array with block heading
*/ */
function parseMesExamples(examplesStr) { function parseMesExamples(examplesStr) {
if (examplesStr.length === 0) { if (examplesStr.length === 0 || examplesStr === '<START>') {
return []; return [];
} }
@ -4662,7 +4731,7 @@ function addChatsSeparator(mesSendString) {
async function DupeChar() { async function DupeChar() {
if (!this_chid) { if (!this_chid) {
toastr.warning('You must first select a character to duplicate!'); toastr.warning('You must first select a character to duplicate!');
return; return '';
} }
const confirmMessage = ` const confirmMessage = `
@ -4673,7 +4742,7 @@ async function DupeChar() {
if (!confirm) { if (!confirm) {
console.log('User cancelled duplication'); console.log('User cancelled duplication');
return; return '';
} }
const body = { avatar_url: characters[this_chid].avatar }; const body = { avatar_url: characters[this_chid].avatar };
@ -4688,6 +4757,8 @@ async function DupeChar() {
await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path }); await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
getCharacters(); getCharacters();
} }
return '';
} }
export async function itemizedParams(itemizedPrompts, thisPromptSet) { export async function itemizedParams(itemizedPrompts, thisPromptSet) {
@ -5408,8 +5479,14 @@ export function resetChatState() {
characters.length = 0; characters.length = 0;
} }
/**
*
* @param {'characters' | 'character_edit' | 'create' | 'group_edit' | 'group_create'} value
*/
export function setMenuType(value) { export function setMenuType(value) {
menu_type = value; menu_type = value;
// Allow custom CSS to see which menu type is active
document.getElementById('right-nav-panel').dataset.menuType = menu_type;
} }
export function setExternalAbortController(controller) { export function setExternalAbortController(controller) {
@ -5448,7 +5525,7 @@ export async function renameCharacter(name = null, { silent = false, renameChats
} }
const oldAvatar = characters[this_chid].avatar; const oldAvatar = characters[this_chid].avatar;
const newValue = name || await callPopup('<h3>New name:</h3>', 'input', characters[this_chid].name); const newValue = name || await callGenericPopup('<h3>New name:</h3>', POPUP_TYPE.INPUT, characters[this_chid].name);
if (!newValue) { if (!newValue) {
toastr.warning('No character name provided.', 'Rename Character'); toastr.warning('No character name provided.', 'Rename Character');
@ -5696,7 +5773,7 @@ async function read_avatar_load(input) {
} }
await createOrEditCharacter(); await createOrEditCharacter();
await delay(durationSaveEdit); await delay(DEFAULT_SAVE_EDIT_TIMEOUT);
const formData = new FormData($('#form_create').get(0)); const formData = new FormData($('#form_create').get(0));
await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), { await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
@ -5740,7 +5817,7 @@ export function getThumbnailUrl(type, file) {
return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`; return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
} }
export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) { export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) {
if (empty) { if (empty) {
block.empty(); block.empty();
} }
@ -5775,8 +5852,8 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
avatarTemplate.attr('title', `[Group] ${entity.item.name}`); avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
} }
if (selectable) { if (interactable) {
avatarTemplate.addClass('selectable'); avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS);
avatarTemplate.toggleClass('character_select', entity.type === 'character'); avatarTemplate.toggleClass('character_select', entity.type === 'character');
avatarTemplate.toggleClass('group_select', entity.type === 'group'); avatarTemplate.toggleClass('group_select', entity.type === 'group');
} }
@ -5826,11 +5903,13 @@ export async function getChat() {
async function getChatResult() { async function getChatResult() {
name2 = characters[this_chid].name; name2 = characters[this_chid].name;
let freshChat = false;
if (chat.length === 0) { if (chat.length === 0) {
const message = getFirstMessage(); const message = getFirstMessage();
if (message.mes) { if (message.mes) {
chat.push(message); chat.push(message);
await saveChatConditional(); await saveChatConditional();
freshChat = true;
} }
} }
await loadItemizedPrompts(getCurrentChatId()); await loadItemizedPrompts(getCurrentChatId());
@ -5838,6 +5917,7 @@ async function getChatResult() {
select_selected_character(this_chid); select_selected_character(this_chid);
await eventSource.emit(event_types.CHAT_CHANGED, (getCurrentChatId())); await eventSource.emit(event_types.CHAT_CHANGED, (getCurrentChatId()));
if (freshChat) await eventSource.emit(event_types.CHAT_CREATED);
if (chat.length === 1) { if (chat.length === 1) {
const chat_id = (chat.length - 1); const chat_id = (chat.length - 1);
@ -6791,7 +6871,7 @@ export function select_selected_character(chid) {
//character select //character select
//console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')'); //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')');
select_rm_create(); select_rm_create();
menu_type = 'character_edit'; setMenuType('character_edit');
$('#delete_button').css('display', 'flex'); $('#delete_button').css('display', 'flex');
$('#export_button').css('display', 'flex'); $('#export_button').css('display', 'flex');
var display_name = characters[chid].name; var display_name = characters[chid].name;
@ -6813,7 +6893,7 @@ export function select_selected_character(chid) {
$('#add_avatar_button').val(''); $('#add_avatar_button').val('');
$('#character_popup_text_h3').text(characters[chid].name); $('#character_popup-button-h3').text(characters[chid].name);
$('#character_name_pole').val(characters[chid].name); $('#character_name_pole').val(characters[chid].name);
$('#description_textarea').val(characters[chid].description); $('#description_textarea').val(characters[chid].description);
$('#character_world').val(characters[chid].data?.extensions?.world || ''); $('#character_world').val(characters[chid].data?.extensions?.world || '');
@ -6867,7 +6947,7 @@ export function select_selected_character(chid) {
} }
function select_rm_create() { function select_rm_create() {
menu_type = 'create'; setMenuType('create');
//console.log('select_rm_Create() -- selected button: '+selected_button); //console.log('select_rm_Create() -- selected button: '+selected_button);
if (selected_button == 'create') { if (selected_button == 'create') {
@ -6890,7 +6970,7 @@ function select_rm_create() {
//create text poles //create text poles
$('#rm_button_back').css('display', ''); $('#rm_button_back').css('display', '');
$('#character_import_button').css('display', ''); $('#character_import_button').css('display', '');
$('#character_popup_text_h3').text('Create character'); $('#character_popup-button-h3').text('Create character');
$('#character_name_pole').val(create_save.name); $('#character_name_pole').val(create_save.name);
$('#description_textarea').val(create_save.description); $('#description_textarea').val(create_save.description);
$('#character_world').val(create_save.world); $('#character_world').val(create_save.world);
@ -6928,7 +7008,7 @@ function select_rm_create() {
function select_rm_characters() { function select_rm_characters() {
const doFullRefresh = menu_type === 'characters'; const doFullRefresh = menu_type === 'characters';
menu_type = 'characters'; setMenuType('characters');
selectRightMenuWithAnimation('rm_characters_block'); selectRightMenuWithAnimation('rm_characters_block');
printCharacters(doFullRefresh); printCharacters(doFullRefresh);
} }
@ -7036,10 +7116,10 @@ function onScenarioOverrideRemoveClick() {
* @param {string} type * @param {string} type
* @param {string} inputValue - Value to set the input to. * @param {string} inputValue - Value to set the input to.
* @param {PopupOptions} options - Options for the popup. * @param {PopupOptions} options - Options for the popup.
* @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup. * @typedef {{okButton?: string, rows?: number, wide?: boolean, wider?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup.
* @returns * @returns
*/ */
export function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) { export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
function getOkButtonText() { function getOkButtonText() {
if (['avatarToCrop'].includes(popup_type)) { if (['avatarToCrop'].includes(popup_type)) {
return okButton ?? 'Accept'; return okButton ?? 'Accept';
@ -7069,6 +7149,7 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, l
const $shadowPopup = $('#shadow_popup'); const $shadowPopup = $('#shadow_popup');
$dialoguePopup.toggleClass('wide_dialogue_popup', !!wide) $dialoguePopup.toggleClass('wide_dialogue_popup', !!wide)
.toggleClass('wider_dialogue_popup', !!wider)
.toggleClass('large_dialogue_popup', !!large) .toggleClass('large_dialogue_popup', !!large)
.toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling) .toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling)
.toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling); .toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
@ -7181,7 +7262,7 @@ export async function saveMetadata() {
export async function saveChatConditional() { export async function saveChatConditional() {
try { try {
await waitUntilCondition(() => !isChatSaving, durationSaveEdit, 100); await waitUntilCondition(() => !isChatSaving, DEFAULT_SAVE_EDIT_TIMEOUT, 100);
} catch { } catch {
console.warn('Timeout waiting for chat to save'); console.warn('Timeout waiting for chat to save');
return; return;
@ -7484,10 +7565,15 @@ function addAlternateGreeting(template, greeting, index, getArray) {
template.find('.alternate_greetings_list').append(greetingBlock); template.find('.alternate_greetings_list').append(greetingBlock);
} }
/**
* Creates or edits a character based on the form data.
* @param {Event} [e] Event that triggered the function call.
*/
async function createOrEditCharacter(e) { async function createOrEditCharacter(e) {
$('#rm_info_avatar').html(''); $('#rm_info_avatar').html('');
const formData = new FormData($('#form_create').get(0)); const formData = new FormData($('#form_create').get(0));
formData.set('fav', String(fav_ch_checked)); formData.set('fav', String(fav_ch_checked));
const isNewChat = e instanceof CustomEvent && e.type === 'newChat';
const rawFile = formData.get('avatar'); const rawFile = formData.get('avatar');
if (rawFile instanceof File) { if (rawFile instanceof File) {
@ -7558,7 +7644,7 @@ async function createOrEditCharacter(e) {
field.callback && field.callback(fieldValue); field.callback && field.callback(fieldValue);
}); });
$('#character_popup_text_h3').text('Create character'); $('#character_popup-button-h3').text('Create character');
create_save.avatar = ''; create_save.avatar = '';
@ -7630,6 +7716,7 @@ async function createOrEditCharacter(e) {
// Recreate the chat if it hasn't been used at least once (i.e. with continue). // Recreate the chat if it hasn't been used at least once (i.e. with continue).
const message = getFirstMessage(); const message = getFirstMessage();
const shouldRegenerateMessage = const shouldRegenerateMessage =
!isNewChat &&
message.mes && message.mes &&
!selected_group && !selected_group &&
!chat_metadata['tainted'] && !chat_metadata['tainted'] &&
@ -7691,6 +7778,8 @@ window['SillyTavern'].getContext = function () {
activateSendButtons, activateSendButtons,
deactivateSendButtons, deactivateSendButtons,
saveReply, saveReply,
substituteParams,
substituteParamsExtended,
registerSlashCommand: registerSlashCommand, registerSlashCommand: registerSlashCommand,
executeSlashCommands: executeSlashCommands, executeSlashCommands: executeSlashCommands,
timestampToMoment: timestampToMoment, timestampToMoment: timestampToMoment,
@ -8244,10 +8333,12 @@ async function selectInstructCallback(_, name) {
async function enableInstructCallback() { async function enableInstructCallback() {
$('#instruct_enabled').prop('checked', true).trigger('change'); $('#instruct_enabled').prop('checked', true).trigger('change');
return '';
} }
async function disableInstructCallback() { async function disableInstructCallback() {
$('#instruct_enabled').prop('checked', false).trigger('change'); $('#instruct_enabled').prop('checked', false).trigger('change');
return '';
} }
/** /**
@ -8390,7 +8481,7 @@ async function importCharacter(file, preserveFileName = false) {
await getCharacters(); await getCharacters();
select_rm_info('char_import', data.file_name, oldSelectedChar); select_rm_info('char_import', data.file_name, oldSelectedChar);
if (power_user.import_card_tags) { if (power_user.tag_import_setting !== tag_import_setting.NONE) {
let currentContext = getContext(); let currentContext = getContext();
let avatarFileName = `${data.file_name}.png`; let avatarFileName = `${data.file_name}.png`;
let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName); let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName);
@ -8421,9 +8512,30 @@ async function importFromURL(items, files) {
} }
} }
async function doImpersonate(_, prompt) { async function doImpersonate(args, prompt) {
$('#send_textarea').val(''); const options = prompt?.trim() ? { quiet_prompt: prompt.trim(), quietToLoud: true } : {};
$('#option_impersonate').trigger('click', { fromSlashCommand: true, additionalPrompt: prompt }); const shouldAwait = isTrueBoolean(args?.await);
const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
try {
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
} catch {
console.warn('Timeout waiting for generation unlock');
toastr.warning('Cannot run /impersonate command while the reply is being generated.');
return '';
}
// Prevent generate recursion
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('impersonate', options)), 1)));
}, 1));
if (shouldAwait) {
const innerPromise = await outerPromise;
await innerPromise;
}
return '';
} }
async function doDeleteChat() { async function doDeleteChat() {
@ -8432,23 +8544,25 @@ async function doDeleteChat() {
$(currentChatDeleteButton).trigger('click'); $(currentChatDeleteButton).trigger('click');
await delay(1); await delay(1);
$('#dialogue_popup_ok').trigger('click', { fromSlashCommand: true }); $('#dialogue_popup_ok').trigger('click', { fromSlashCommand: true });
return '';
} }
async function doRenameChat(_, chatName) { async function doRenameChat(_, chatName) {
if (!chatName) { if (!chatName) {
toastr.warning('Name must be provided as an argument to rename this chat.'); toastr.warning('Name must be provided as an argument to rename this chat.');
return; return '';
} }
const currentChatName = getCurrentChatId(); const currentChatName = getCurrentChatId();
if (!currentChatName) { if (!currentChatName) {
toastr.warning('No chat selected that can be renamed.'); toastr.warning('No chat selected that can be renamed.');
return; return '';
} }
await renameChat(currentChatName, chatName); await renameChat(currentChatName, chatName);
toastr.success(`Successfully renamed chat to: ${chatName}`); toastr.success(`Successfully renamed chat to: ${chatName}`);
return '';
} }
/** /**
@ -8522,6 +8636,7 @@ function doCharListDisplaySwitch() {
function doCloseChat() { function doCloseChat() {
$('#option_close_chat').trigger('click'); $('#option_close_chat').trigger('click');
return '';
} }
/** /**
@ -8617,6 +8732,7 @@ async function removeCharacterFromUI(name, avatar, reloadCharacters = true) {
function doTogglePanels() { function doTogglePanels() {
$('#option_settings').trigger('click'); $('#option_settings').trigger('click');
return '';
} }
function addDebugFunctions() { function addDebugFunctions() {
@ -8704,8 +8820,12 @@ jQuery(async function () {
await saveSettings(); await saveSettings();
await saveChatConditional(); await saveChatConditional();
toastr.success('Chat and settings saved.'); toastr.success('Chat and settings saved.');
return '';
} }
// Collect all unique API names in an array
const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'dupe', name: 'dupe',
callback: DupeChar, callback: DupeChar,
@ -8714,16 +8834,15 @@ jQuery(async function () {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'api', name: 'api',
callback: connectAPISlash, callback: connectAPISlash,
namedArgumentList: [],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'API to connect to', description: 'API to connect to',
[ARGUMENT_TYPE.STRING], typeList: [ARGUMENT_TYPE.STRING],
true, isRequired: false,
false, enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
null, new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIs.findIndex(x => x === selected)),
Object.keys(CONNECT_API_MAP), selected[0].toUpperCase() ?? enumIcons.default)),
), }),
], ],
helpString: ` helpString: `
<div> <div>
@ -8739,6 +8858,16 @@ jQuery(async function () {
name: 'impersonate', name: 'impersonate',
callback: doImpersonate, callback: doImpersonate,
aliases: ['imp'], aliases: ['imp'],
namedArgumentList: [
new SlashCommandNamedArgument(
'await',
'Whether to await for the triggered generation before continuing',
[ARGUMENT_TYPE.BOOLEAN],
false,
false,
'false',
),
],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(
'prompt', [ARGUMENT_TYPE.STRING], false, 'prompt', [ARGUMENT_TYPE.STRING], false,
@ -8748,6 +8877,9 @@ jQuery(async function () {
<div> <div>
Calls an impersonation response, with an optional additional prompt. Calls an impersonation response, with an optional additional prompt.
</div> </div>
<div>
If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing.
</div>
<div> <div>
<strong>Example:</strong> <strong>Example:</strong>
<ul> <ul>
@ -8799,11 +8931,12 @@ jQuery(async function () {
name: 'instruct', name: 'instruct',
callback: selectInstructCallback, callback: selectInstructCallback,
returns: 'current preset', returns: 'current preset',
namedArgumentList: [],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'name', [ARGUMENT_TYPE.STRING], false, description: 'instruct preset name',
), typeList: [ARGUMENT_TYPE.STRING],
enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -8834,15 +8967,20 @@ jQuery(async function () {
callback: selectContextCallback, callback: selectContextCallback,
returns: 'template name', returns: 'template name',
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'name', [ARGUMENT_TYPE.STRING], false, description: 'context preset name',
), typeList: [ARGUMENT_TYPE.STRING],
enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
}),
], ],
helpString: 'Selects context template by name. Gets the current template if no name is provided', helpString: 'Selects context template by name. Gets the current template if no name is provided',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'chat-manager', name: 'chat-manager',
callback: () => $('#option_select_chat').trigger('click'), callback: () => {
$('#option_select_chat').trigger('click');
return '';
},
aliases: ['chat-history', 'manage-chats'], aliases: ['chat-history', 'manage-chats'],
helpString: 'Opens the chat manager for the current character/group.', helpString: 'Opens the chat manager for the current character/group.',
})); }));
@ -8863,7 +9001,7 @@ jQuery(async function () {
$('#send_textarea').on('focusin focus click', () => { $('#send_textarea').on('focusin focus click', () => {
S_TAPreviouslyFocused = true; S_TAPreviouslyFocused = true;
}); });
$('#options_button, #send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => { $('#send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => {
if (S_TAPreviouslyFocused) { if (S_TAPreviouslyFocused) {
$('#send_textarea').focus(); $('#send_textarea').focus();
} }
@ -8919,7 +9057,6 @@ jQuery(async function () {
$('#rm_button_settings').click(function () { $('#rm_button_settings').click(function () {
selected_button = 'settings'; selected_button = 'settings';
menu_type = 'settings';
selectRightMenuWithAnimation('rm_api_block'); selectRightMenuWithAnimation('rm_api_block');
}); });
$('#rm_button_characters').click(function () { $('#rm_button_characters').click(function () {
@ -9111,7 +9248,7 @@ jQuery(async function () {
characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`; characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`;
$('#selected_chat_pole').val(characters[this_chid].chat); $('#selected_chat_pole').val(characters[this_chid].chat);
await getChat(); await getChat();
await createOrEditCharacter(); await createOrEditCharacter(new CustomEvent('newChat'));
if (isDelChatCheckbox) await delChat(chat_file_for_del + '.jsonl'); if (isDelChatCheckbox) await delChat(chat_file_for_del + '.jsonl');
} }
} }
@ -9373,7 +9510,7 @@ jQuery(async function () {
} }
function isMouseOverButtonOrMenu() { function isMouseOverButtonOrMenu() {
return menu.is(':hover') || button.is(':hover'); return menu.is(':hover, :focus-within') || button.is(':hover, :focus');
} }
button.on('click', function () { button.on('click', function () {
@ -10273,7 +10410,7 @@ jQuery(async function () {
'#character_cross', '#character_cross',
'#avatar-and-name-block', '#avatar-and-name-block',
'#shadow_popup', '#shadow_popup',
'.shadow_popup', '.popup',
'#world_popup', '#world_popup',
'.ui-widget', '.ui-widget',
'.text_pole', '.text_pole',
@ -10484,7 +10621,7 @@ jQuery(async function () {
} }
} break; } break;
case 'import_tags': { case 'import_tags': {
await importTags(characters[this_chid]); await importTags(characters[this_chid], { forceShow: true });
} break; } break;
/*case 'delete_button': /*case 'delete_button':
popup_type = "del_ch"; popup_type = "del_ch";
@ -10578,7 +10715,7 @@ jQuery(async function () {
const html = await renderTemplateAsync('importCharacters'); const html = await renderTemplateAsync('importCharacters');
/** @type {string?} */ /** @type {string?} */
const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { wider: true, okButton: $('#shadow_popup_template').attr('popup_text_import'), rows: 4 }); const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { wider: true, okButton: $('#popup_template').attr('popup-button-import'), rows: 4 });
if (!input) { if (!input) {
console.debug('Custom content import cancelled'); console.debug('Custom content import cancelled');
@ -10633,32 +10770,12 @@ jQuery(async function () {
} }
}); });
const $dropzone = $(document.body); charDragDropHandler = new DragAndDropHandler('body', async (files, event) => {
$dropzone.on('dragover', (event) => {
event.preventDefault();
event.stopPropagation();
$dropzone.addClass('dragover');
});
$dropzone.on('dragleave', (event) => {
event.preventDefault();
event.stopPropagation();
$dropzone.removeClass('dragover');
});
$dropzone.on('drop', async (event) => {
event.preventDefault();
event.stopPropagation();
$dropzone.removeClass('dragover');
const files = Array.from(event.originalEvent.dataTransfer.files);
if (!files.length) { if (!files.length) {
await importFromURL(event.originalEvent.dataTransfer.items, files); await importFromURL(event.originalEvent.dataTransfer.items, files);
} }
await processDroppedFiles(files); await processDroppedFiles(files);
}); }, { noAnimation: true });
$('#charListGridToggle').on('click', async () => { $('#charListGridToggle').on('click', async () => {
doCharListDisplaySwitch(); doCharListDisplaySwitch();

View File

@ -685,6 +685,23 @@ class PromptManager {
this.log('Initialized'); this.log('Initialized');
} }
/**
* Get the scroll position of the prompt manager
* @returns {number} - Scroll position of the prompt manager
*/
#getScrollPosition() {
return document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTop;
}
/**
* Set the scroll position of the prompt manager
* @param {number} scrollPosition - The scroll position to set
*/
#setScrollPosition(scrollPosition) {
if (scrollPosition === undefined || scrollPosition === null) return;
document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTo(0, scrollPosition);
}
/** /**
* Main rendering function * Main rendering function
* *
@ -703,17 +720,21 @@ class PromptManager {
this.tryGenerate().finally(async () => { this.tryGenerate().finally(async () => {
this.profileEnd('filling context'); this.profileEnd('filling context');
this.profileStart('render'); this.profileStart('render');
const scrollPosition = this.#getScrollPosition();
await this.renderPromptManager(); await this.renderPromptManager();
await this.renderPromptManagerListItems(); await this.renderPromptManagerListItems();
this.makeDraggable(); this.makeDraggable();
this.#setScrollPosition(scrollPosition);
this.profileEnd('render'); this.profileEnd('render');
}); });
} else { } else {
// Executed during live communication // Executed during live communication
this.profileStart('render'); this.profileStart('render');
const scrollPosition = this.#getScrollPosition();
await this.renderPromptManager(); await this.renderPromptManager();
await this.renderPromptManagerListItems(); await this.renderPromptManagerListItems();
this.makeDraggable(); this.makeDraggable();
this.#setScrollPosition(scrollPosition);
this.profileEnd('render'); this.profileEnd('render');
} }
}).catch(() => { }).catch(() => {

View File

@ -39,6 +39,7 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex
import { debounce_timeout } from './constants.js'; import { debounce_timeout } from './constants.js';
import Bowser from '../lib/bowser.min.js'; import Bowser from '../lib/bowser.min.js';
import { Popup } from './popup.js';
var RPanelPin = document.getElementById('rm_button_panel_pin'); var RPanelPin = document.getElementById('rm_button_panel_pin');
var LPanelPin = document.getElementById('lm_button_panel_pin'); var LPanelPin = document.getElementById('lm_button_panel_pin');
@ -303,7 +304,7 @@ export async function favsToHotswap() {
return; return;
} }
buildAvatarList(container, favs, { selectable: true, highlightFavs: false }); buildAvatarList(container, favs, { interactable: true, highlightFavs: false });
} }
//changes input bar and send button display depending on connection status //changes input bar and send button display depending on connection status
@ -1096,6 +1097,9 @@ export function initRossMods() {
} }
if (event.key == 'Escape') { //closes various panels if (event.key == 'Escape') { //closes various panels
// Do not close panels if we are currently inside a popup
if (Popup.util.isPopupOpen())
return;
//dont override Escape hotkey functions from script.js //dont override Escape hotkey functions from script.js
//"close edit box" and "cancel stream generation". //"close edit box" and "cancel stream generation".

View File

@ -38,6 +38,7 @@ const chara_note_position = {
function setNoteTextCommand(_, text) { function setNoteTextCommand(_, text) {
$('#extension_floating_prompt').val(text).trigger('input'); $('#extension_floating_prompt').val(text).trigger('input');
toastr.success('Author\'s Note text updated'); toastr.success('Author\'s Note text updated');
return '';
} }
function setNoteDepthCommand(_, text) { function setNoteDepthCommand(_, text) {
@ -50,6 +51,7 @@ function setNoteDepthCommand(_, text) {
$('#extension_floating_depth').val(Math.abs(value)).trigger('input'); $('#extension_floating_depth').val(Math.abs(value)).trigger('input');
toastr.success('Author\'s Note depth updated'); toastr.success('Author\'s Note depth updated');
return '';
} }
function setNoteIntervalCommand(_, text) { function setNoteIntervalCommand(_, text) {
@ -62,6 +64,7 @@ function setNoteIntervalCommand(_, text) {
$('#extension_floating_interval').val(Math.abs(value)).trigger('input'); $('#extension_floating_interval').val(Math.abs(value)).trigger('input');
toastr.success('Author\'s Note frequency updated'); toastr.success('Author\'s Note frequency updated');
return '';
} }
function setNotePositionCommand(_, text) { function setNotePositionCommand(_, text) {
@ -79,6 +82,7 @@ function setNotePositionCommand(_, text) {
$(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input'); $(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input');
toastr.info('Author\'s Note position updated'); toastr.info('Author\'s Note position updated');
return '';
} }
function updateSettings() { function updateSettings() {

View File

@ -6,6 +6,7 @@ import { BlankAutoCompleteOption } from './BlankAutoCompleteOption.js';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js'; import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js'; import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
import { Popup, getTopmostModalLayer } from '../popup.js';
/**@readonly*/ /**@readonly*/
/**@enum {Number}*/ /**@enum {Number}*/
@ -386,11 +387,15 @@ export class AutoComplete {
// no result and no input? hide autocomplete // no result and no input? hide autocomplete
return this.hide(); return this.hide();
} }
if (this.effectiveParserResult instanceof AutoCompleteSecondaryNameResult && !this.effectiveParserResult.forceMatch) {
// no result and matching is no forced? hide autocomplete
return this.hide();
}
// otherwise add "no match" notice // otherwise add "no match" notice
const option = new BlankAutoCompleteOption( const option = new BlankAutoCompleteOption(
this.name.length ? this.name.length ?
this.effectiveParserResult.makeNoMatchText() this.effectiveParserResult.makeNoMatchText()
: this.effectiveParserResult.makeNoOptionstext() : this.effectiveParserResult.makeNoOptionsText()
, ,
); );
this.result.push(option); this.result.push(option);
@ -438,7 +443,7 @@ export class AutoComplete {
} }
this.dom.append(frag); this.dom.append(frag);
this.updatePosition(); this.updatePosition();
document.body.append(this.domWrap); getTopmostModalLayer().append(this.domWrap);
} else { } else {
this.domWrap.remove(); this.domWrap.remove();
} }
@ -453,7 +458,7 @@ export class AutoComplete {
if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove(); if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
this.detailsDom.innerHTML = ''; this.detailsDom.innerHTML = '';
this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM'); this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
document.body.append(this.detailsWrap); getTopmostModalLayer().append(this.detailsWrap);
this.updateDetailsPositionDebounced(); this.updateDetailsPositionDebounced();
} }
@ -469,7 +474,7 @@ export class AutoComplete {
const rect = {}; const rect = {};
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();
this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`); this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`); this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`; this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
@ -481,8 +486,8 @@ export class AutoComplete {
this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`); this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`);
this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(99vw, ${rect[power_user.stscript.autocomplete.width.right].right}px)`); this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(99vw, ${rect[power_user.stscript.autocomplete.width.right].right}px)`);
} }
this.updateDetailsPosition();
} }
this.updateDetailsPosition();
} }
/** /**
@ -496,7 +501,7 @@ export class AutoComplete {
const rect = {}; const rect = {};
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();
if (this.isReplaceable) { if (this.isReplaceable) {
this.detailsWrap.classList.remove('full'); this.detailsWrap.classList.remove('full');
const selRect = this.selectedItem.dom.children[0].getBoundingClientRect(); const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
@ -592,7 +597,7 @@ export class AutoComplete {
} }
this.clone.style.position = 'fixed'; this.clone.style.position = 'fixed';
this.clone.style.visibility = 'hidden'; this.clone.style.visibility = 'hidden';
document.body.append(this.clone); getTopmostModalLayer().append(this.clone);
const mo = new MutationObserver(muts=>{ const mo = new MutationObserver(muts=>{
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) { if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
this.clone.remove(); this.clone.remove();
@ -745,8 +750,10 @@ export class AutoComplete {
} }
// autocomplete shown or not, cursor anywhere // autocomplete shown or not, cursor anywhere
switch (evt.key) { switch (evt.key) {
// The first is a non-breaking space, the second is a regular space.
case ' ':
case ' ': { case ' ': {
if (evt.ctrlKey) { if (evt.ctrlKey || evt.altKey) {
if (this.isActive && this.isReplaceable) { if (this.isActive && this.isReplaceable) {
// ctrl-space to toggle details for selected item // ctrl-space to toggle details for selected item
this.toggleDetails(); this.toggleDetails();
@ -754,6 +761,8 @@ export class AutoComplete {
// ctrl-space to force show autocomplete // ctrl-space to force show autocomplete
this.show(false, true); this.show(false, true);
} }
evt.preventDefault();
evt.stopPropagation();
return; return;
} }
break; break;

View File

@ -10,7 +10,7 @@ export class AutoCompleteNameResult {
/**@type {AutoCompleteOption[]} */ optionList = []; /**@type {AutoCompleteOption[]} */ optionList = [];
/**@type {boolean} */ canBeQuoted = false; /**@type {boolean} */ canBeQuoted = false;
/**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`; /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
/**@type {()=>string} */ makeNoOptionstext = ()=>'No options'; /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
/** /**
@ -27,7 +27,7 @@ export class AutoCompleteNameResult {
this.optionList = optionList; this.optionList = optionList;
this.canBeQuoted = canBeQuoted; this.canBeQuoted = canBeQuoted;
this.noMatchText = makeNoMatchText ?? this.makeNoMatchText; this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionstext; this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
} }

View File

@ -6,6 +6,7 @@ import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
export class AutoCompleteOption { export class AutoCompleteOption {
/**@type {string}*/ name; /**@type {string}*/ name;
/**@type {string}*/ typeIcon; /**@type {string}*/ typeIcon;
/**@type {string}*/ type;
/**@type {number}*/ nameOffset = 0; /**@type {number}*/ nameOffset = 0;
/**@type {AutoCompleteFuzzyScore}*/ score; /**@type {AutoCompleteFuzzyScore}*/ score;
/**@type {string}*/ replacer; /**@type {string}*/ replacer;
@ -24,9 +25,10 @@ export class AutoCompleteOption {
/** /**
* @param {string} name * @param {string} name
*/ */
constructor(name, typeIcon = ' ') { constructor(name, typeIcon = ' ', type = '') {
this.name = name; this.name = name;
this.typeIcon = typeIcon; this.typeIcon = typeIcon;
this.type = type;
} }
@ -141,6 +143,11 @@ export class AutoCompleteOption {
} }
li.append(specs); li.append(specs);
} }
const stopgap = document.createElement('span'); {
stopgap.classList.add('stopgap');
stopgap.textContent = '';
li.append(stopgap);
}
const help = document.createElement('span'); { const help = document.createElement('span'); {
help.classList.add('help'); help.classList.add('help');
const content = document.createElement('span'); { const content = document.createElement('span'); {
@ -181,6 +188,7 @@ export class AutoCompleteOption {
let li; let li;
li = this.makeItem(this.name, this.typeIcon, true); li = this.makeItem(this.name, this.typeIcon, true);
li.setAttribute('data-name', this.name); li.setAttribute('data-name', this.name);
li.setAttribute('data-option-type', this.type);
return li; return li;
} }

View File

@ -2,4 +2,5 @@ import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult { export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {
/**@type {boolean}*/ isRequired = false; /**@type {boolean}*/ isRequired = false;
/**@type {boolean}*/ forceMatch = true;
} }

View File

@ -95,7 +95,7 @@ function onLockBackgroundClick(e) {
if (!chatName) { if (!chatName) {
toastr.warning('Select a chat to lock the background for it'); toastr.warning('Select a chat to lock the background for it');
return; return '';
} }
const relativeBgImage = getUrlParameter(this); const relativeBgImage = getUrlParameter(this);
@ -103,6 +103,7 @@ function onLockBackgroundClick(e) {
saveBackgroundMetadata(relativeBgImage); saveBackgroundMetadata(relativeBgImage);
setCustomBackground(); setCustomBackground();
highlightLockedBackground(); highlightLockedBackground();
return '';
} }
function onUnlockBackgroundClick(e) { function onUnlockBackgroundClick(e) {
@ -110,6 +111,7 @@ function onUnlockBackgroundClick(e) {
removeBackgroundMetadata(); removeBackgroundMetadata();
unsetCustomBackground(); unsetCustomBackground();
highlightLockedBackground(); highlightLockedBackground();
return '';
} }
function hasCustomBackground() { function hasCustomBackground() {
@ -319,7 +321,7 @@ async function autoBackgroundCommand() {
const options = bgTitles.map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0); const options = bgTitles.map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) { if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.'); toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return; return '';
} }
const list = options.map(option => `- ${option.text}`).join('\n'); const list = options.map(option => `- ${option.text}`).join('\n');
@ -330,11 +332,12 @@ async function autoBackgroundCommand() {
if (bestMatch.length == 0) { if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.'); toastr.warning('No match found. Please try again.');
return; return '';
} }
console.debug('Automatically choosing background:', bestMatch); console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click(); bestMatch[0].item.element.click();
return '';
} }
export async function getBackgrounds() { export async function getBackgrounds() {

View File

@ -94,6 +94,9 @@ function enableBulkSelect() {
}); });
$(el).prepend(checkbox); $(el).prepend(checkbox);
}); });
$('#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select, #rm_print_characters_block.group_overlay_mode_select .group_select')
.addClass('disabled');
$('#rm_print_characters_block').addClass('bulk_select'); $('#rm_print_characters_block').addClass('bulk_select');
// We also need to disable the default click event for the character_select divs // We also need to disable the default click event for the character_select divs
$(document).on('click', '.bulk_select_checkbox', function (event) { $(document).on('click', '.bulk_select_checkbox', function (event) {
@ -106,6 +109,8 @@ function enableBulkSelect() {
*/ */
function disableBulkSelect() { function disableBulkSelect() {
$('.bulk_select_checkbox').remove(); $('.bulk_select_checkbox').remove();
$('#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select, #rm_print_characters_block.group_overlay_mode_select .group_select')
.removeClass('disabled');
$('#rm_print_characters_block').removeClass('bulk_select'); $('#rm_print_characters_block').removeClass('bulk_select');
} }

View File

@ -35,8 +35,9 @@ import {
extractTextFromOffice, extractTextFromOffice,
} from './utils.js'; } from './utils.js';
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { ScraperManager } from './scrapers.js'; import { ScraperManager } from './scrapers.js';
import { DragAndDropHandler } from './dragdrop.js';
/** /**
* @typedef {Object} FileAttachment * @typedef {Object} FileAttachment
@ -565,7 +566,7 @@ export function isExternalMediaAllowed() {
return !power_user.forbid_external_media; return !power_user.forbid_external_media;
} }
function enlargeMessageImage() { async function enlargeMessageImage() {
const mesBlock = $(this).closest('.mes'); const mesBlock = $(this).closest('.mes');
const mesId = mesBlock.attr('mesid'); const mesId = mesBlock.attr('mesid');
const message = chat[mesId]; const message = chat[mesId];
@ -579,14 +580,28 @@ function enlargeMessageImage() {
const img = document.createElement('img'); const img = document.createElement('img');
img.classList.add('img_enlarged'); img.classList.add('img_enlarged');
img.src = imgSrc; img.src = imgSrc;
const imgHolder = document.createElement('div');
imgHolder.classList.add('img_enlarged_holder');
imgHolder.append(img);
const imgContainer = $('<div><pre><code></code></pre></div>'); const imgContainer = $('<div><pre><code></code></pre></div>');
imgContainer.prepend(img); imgContainer.prepend(imgHolder);
imgContainer.addClass('img_enlarged_container'); imgContainer.addClass('img_enlarged_container');
imgContainer.find('code').addClass('txt').text(title); imgContainer.find('code').addClass('txt').text(title);
const titleEmpty = !title || title.trim().length === 0; const titleEmpty = !title || title.trim().length === 0;
imgContainer.find('pre').toggle(!titleEmpty); imgContainer.find('pre').toggle(!titleEmpty);
addCopyToCodeBlocks(imgContainer); addCopyToCodeBlocks(imgContainer);
callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true });
const popup = new Popup(imgContainer, POPUP_TYPE.DISPLAY, '', { large: true, transparent: true });
popup.dlg.style.width = 'unset';
popup.dlg.style.height = 'unset';
img.addEventListener('click', () => {
const shouldZoom = !img.classList.contains('zoomed');
img.classList.toggle('zoomed', shouldZoom);
});
await popup.show();
} }
async function deleteMessageImage() { async function deleteMessageImage() {
@ -991,49 +1006,24 @@ async function openAttachmentManager() {
template.find('.chatAttachmentsName').text(chatName); template.find('.chatAttachmentsName').text(chatName);
} }
function addDragAndDrop() { const dragDropHandler = new DragAndDropHandler('.popup', async (files, event) => {
$(document.body).on('dragover', '.dialogue_popup', (event) => { let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
event.preventDefault(); const targets = getAvailableTargets();
event.stopPropagation();
$(event.target).closest('.dialogue_popup').addClass('dragover'); const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets }));
targetSelectTemplate.find('.droppedFilesTarget').on('input', function () {
selectedTarget = String($(this).val());
}); });
const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' });
$(document.body).on('dragleave', '.dialogue_popup', (event) => { if (result !== POPUP_RESULT.AFFIRMATIVE) {
event.preventDefault(); console.log('File upload cancelled');
event.stopPropagation(); return;
$(event.target).closest('.dialogue_popup').removeClass('dragover'); }
}); for (const file of files) {
await uploadFileAttachmentToServer(file, selectedTarget);
$(document.body).on('drop', '.dialogue_popup', async (event) => { }
event.preventDefault(); renderAttachments();
event.stopPropagation(); });
$(event.target).closest('.dialogue_popup').removeClass('dragover');
const files = Array.from(event.originalEvent.dataTransfer.files);
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
const targets = getAvailableTargets();
const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets }));
targetSelectTemplate.find('.droppedFilesTarget').on('input', function () {
selectedTarget = String($(this).val());
});
const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' });
if (result !== POPUP_RESULT.AFFIRMATIVE) {
console.log('File upload cancelled');
return;
}
for (const file of files) {
await uploadFileAttachmentToServer(file, selectedTarget);
}
renderAttachments();
});
}
function removeDragAndDrop() {
$(document.body).off('dragover', '.shadow_popup');
$(document.body).off('dragleave', '.shadow_popup');
$(document.body).off('drop', '.shadow_popup');
}
let sortField = localStorage.getItem('DataBank_sortField') || 'created'; let sortField = localStorage.getItem('DataBank_sortField') || 'created';
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc'; let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
@ -1056,43 +1046,61 @@ async function openAttachmentManager() {
localStorage.setItem('DataBank_sortOrder', sortOrder); localStorage.setItem('DataBank_sortOrder', sortOrder);
renderAttachments(); renderAttachments();
}); });
template.find('.bulkActionDelete').on('click', async () => { function handleBulkAction(action) {
const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked'); return async () => {
const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked');
if (selectedAttachments.length === 0) { if (selectedAttachments.length === 0) {
toastr.info('No attachments selected.', 'Data Bank'); toastr.info('No attachments selected.', 'Data Bank');
return;
}
const confirm = await callGenericPopup('Are you sure you want to delete the selected attachments?', POPUP_TYPE.CONFIRM);
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
return;
}
const attachments = getDataBankAttachments();
selectedAttachments.forEach(async (checkbox) => {
const listItem = checkbox.closest('.attachmentListItem');
if (!(listItem instanceof HTMLElement)) {
return; return;
} }
const url = listItem.dataset.attachmentUrl;
const source = listItem.dataset.attachmentSource;
const attachment = attachments.find(a => a.url === url);
if (!attachment) {
return;
}
await deleteAttachment(attachment, source, () => {}, false);
});
document.querySelectorAll('.attachmentListItemCheckbox, .attachmentsBulkEditCheckbox').forEach(checkbox => { if (action.confirmMessage) {
if (checkbox instanceof HTMLInputElement) { const confirm = await callGenericPopup(action.confirmMessage, POPUP_TYPE.CONFIRM);
checkbox.checked = false; if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
return;
}
} }
});
await renderAttachments(); const includeDisabled = true;
}); const attachments = getDataBankAttachments(includeDisabled);
selectedAttachments.forEach(async (checkbox) => {
const listItem = checkbox.closest('.attachmentListItem');
if (!(listItem instanceof HTMLElement)) {
return;
}
const url = listItem.dataset.attachmentUrl;
const source = listItem.dataset.attachmentSource;
const attachment = attachments.find(a => a.url === url);
if (!attachment) {
return;
}
await action.perform(attachment, source);
});
document.querySelectorAll('.attachmentListItemCheckbox, .attachmentsBulkEditCheckbox').forEach(checkbox => {
if (checkbox instanceof HTMLInputElement) {
checkbox.checked = false;
}
});
await renderAttachments();
};
}
template.find('.bulkActionDisable').on('click', handleBulkAction({
perform: (attachment) => disableAttachment(attachment, () => { }),
}));
template.find('.bulkActionEnable').on('click', handleBulkAction({
perform: (attachment) => enableAttachment(attachment, () => { }),
}));
template.find('.bulkActionDelete').on('click', handleBulkAction({
confirmMessage: 'Are you sure you want to delete the selected attachments?',
perform: async (attachment, source) => await deleteAttachment(attachment, source, () => { }, false),
}));
template.find('.bulkActionSelectAll').on('click', () => { template.find('.bulkActionSelectAll').on('click', () => {
$('.attachmentListItemCheckbox:visible').each((_, checkbox) => { $('.attachmentListItemCheckbox:visible').each((_, checkbox) => {
if (checkbox instanceof HTMLInputElement) { if (checkbox instanceof HTMLInputElement) {
@ -1111,11 +1119,10 @@ async function openAttachmentManager() {
const cleanupFn = await renderButtons(); const cleanupFn = await renderButtons();
await verifyAttachments(); await verifyAttachments();
await renderAttachments(); await renderAttachments();
addDragAndDrop();
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' }); await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
cleanupFn(); cleanupFn();
removeDragAndDrop(); dragDropHandler.destroy();
} }
/** /**

107
public/scripts/dragdrop.js vendored Normal file
View File

@ -0,0 +1,107 @@
import { debounce_timeout } from './constants.js';
/**
* Drag and drop handler
*
* Can be used on any element, enabling drag&drop styling and callback on drop.
*/
export class DragAndDropHandler {
/** @private @type {JQuery.Selector} */ selector;
/** @private @type {(files: File[], event:JQuery.DropEvent<HTMLElement, undefined, any, any>) => void} */ onDropCallback;
/** @private @type {NodeJS.Timeout} Remark: Not actually NodeJS timeout, but it's close */ dragLeaveTimeout;
/** @private @type {boolean} */ noAnimation;
/**
* Create a DragAndDropHandler
* @param {JQuery.Selector} selector - The CSS selector for the elements to enable drag and drop
* @param {(files: File[], event:JQuery.DropEvent<HTMLElement, undefined, any, any>) => void} onDropCallback - The callback function to handle the drop event
*/
constructor(selector, onDropCallback, { noAnimation = false } = {}) {
this.selector = selector;
this.onDropCallback = onDropCallback;
this.dragLeaveTimeout = null;
this.noAnimation = noAnimation;
this.init();
}
/**
* Destroy the drag and drop functionality
*/
destroy() {
if (this.selector === 'body') {
$(document.body).off('dragover', this.handleDragOver.bind(this));
$(document.body).off('dragleave', this.handleDragLeave.bind(this));
$(document.body).off('drop', this.handleDrop.bind(this));
} else {
$(document.body).off('dragover', this.selector, this.handleDragOver.bind(this));
$(document.body).off('dragleave', this.selector, this.handleDragLeave.bind(this));
$(document.body).off('drop', this.selector, this.handleDrop.bind(this));
}
$(this.selector).remove('drop_target no_animation');
}
/**
* Initialize the drag and drop functionality
* Automatically called on construction
* @private
*/
init() {
if (this.selector === 'body') {
$(document.body).on('dragover', this.handleDragOver.bind(this));
$(document.body).on('dragleave', this.handleDragLeave.bind(this));
$(document.body).on('drop', this.handleDrop.bind(this));
} else {
$(document.body).on('dragover', this.selector, this.handleDragOver.bind(this));
$(document.body).on('dragleave', this.selector, this.handleDragLeave.bind(this));
$(document.body).on('drop', this.selector, this.handleDrop.bind(this));
}
$(this.selector).addClass('drop_target');
if (this.noAnimation) $(this.selector).addClass('no_animation');
}
/**
* @param {JQuery.DragOverEvent<HTMLElement, undefined, any, any>} event - The dragover event
* @private
*/
handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
clearTimeout(this.dragLeaveTimeout);
$(this.selector).addClass('drop_target dragover');
if (this.noAnimation) $(this.selector).addClass('no_animation');
}
/**
* @param {JQuery.DragLeaveEvent<HTMLElement, undefined, any, any>} event - The dragleave event
* @private
*/
handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
// Debounce the removal of the class, so it doesn't "flicker" on dragging over
clearTimeout(this.dragLeaveTimeout);
this.dragLeaveTimeout = setTimeout(() => {
$(this.selector).removeClass('dragover');
}, debounce_timeout.quick);
}
/**
* @param {JQuery.DropEvent<HTMLElement, undefined, any, any>} event - The drop event
* @private
*/
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
clearTimeout(this.dragLeaveTimeout);
$(this.selector).removeClass('dragover');
const files = Array.from(event.originalEvent.dataTransfer.files);
this.onDropCallback(files, event);
}
}

View File

@ -0,0 +1,162 @@
/** @type {CSSStyleSheet} */
let dynamicStyleSheet = null;
/** @type {CSSStyleSheet} */
let dynamicExtensionStyleSheet = null;
/**
* An observer that will check if any new stylesheets are added to the head
* @type {MutationObserver}
*/
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type !== 'childList') return;
mutation.addedNodes.forEach(node => {
if (node instanceof HTMLLinkElement && node.tagName === 'LINK' && node.rel === 'stylesheet') {
node.addEventListener('load', () => {
try {
applyDynamicFocusStyles(node.sheet);
} catch (e) {
console.warn('Failed to process new stylesheet:', e);
}
});
}
});
});
});
/**
* Generates dynamic focus styles based on the given stylesheet, taking its hover styles as reference
*
* @param {CSSStyleSheet} styleSheet - The stylesheet to process
* @param {object} [options] - Optional configuration options
* @param {boolean} [options.fromExtension=false] - Indicates if the styles are from an extension
*/
function applyDynamicFocusStyles(styleSheet, { fromExtension = false } = {}) {
/** @type {{baseSelector: string, rule: CSSStyleRule}[]} */
const hoverRules = [];
/** @type {Set<string>} */
const focusRules = new Set();
const PLACEHOLDER = ':__PLACEHOLDER__';
/**
* Processes the CSS rules and separates selectors for hover and focus
* @param {CSSRuleList} rules - The CSS rules to process
*/
function processRules(rules) {
Array.from(rules).forEach(rule => {
if (rule instanceof CSSImportRule) {
// Make sure that @import rules are processed recursively
processImportedStylesheet(rule.styleSheet);
} else if (rule instanceof CSSStyleRule) {
// Separate multiple selectors on a rule
const selectors = rule.selectorText.split(',').map(s => s.trim());
// We collect all hover and focus rules to be able to later decide which hover rules don't have a matching focus rule
selectors.forEach(selector => {
const isHover = selector.includes(':hover'), isFocus = selector.includes(':focus');
if (isHover && isFocus) {
// We currently do nothing here. Rules containing both hover and focus are very specific and should never be automatically touched
}
else if (isHover) {
const baseSelector = selector.replace(':hover', PLACEHOLDER).trim();
hoverRules.push({ baseSelector, rule });
} else if (isFocus) {
// We need to make sure that we remember all existing :focus, :focus-within and :focus-visible rules
const baseSelector = selector.replace(':focus-within', PLACEHOLDER).replace(':focus-visible', PLACEHOLDER).replace(':focus', PLACEHOLDER).trim();
focusRules.add(baseSelector);
}
});
} else if (rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule) {
// Recursively process nested rules
processRules(rule.cssRules);
}
});
}
/**
* Processes the CSS rules of an imported stylesheet recursively
* @param {CSSStyleSheet} sheet - The imported stylesheet to process
*/
function processImportedStylesheet(sheet) {
if (sheet && sheet.cssRules) {
processRules(sheet.cssRules);
}
}
processRules(styleSheet.cssRules);
/** @type {CSSStyleSheet} */
let targetStyleSheet = null;
// Now finally create the dynamic focus rules
hoverRules.forEach(({ baseSelector, rule }) => {
if (!focusRules.has(baseSelector)) {
// Only initialize the dynamic stylesheet if needed
targetStyleSheet ??= getDynamicStyleSheet({ fromExtension });
// The closest keyboard-equivalent to :hover styling is utilizing the :focus-visible rule from modern browsers.
// It let's the browser decide whether a focus highlighting is expected and makes sense.
// So we take all :hover rules that don't have a manually defined focus rule yet, and create their
// :focus-visible counterpart, which will make the styling work the same for keyboard and mouse.
// If something like :focus-within or a more specific selector like `.blah:has(:focus-visible)` for elements inside,
// it should be manually defined in CSS.
const focusSelector = rule.selectorText.replace(/:hover/g, ':focus-visible');
const focusRule = `${focusSelector} { ${rule.style.cssText} }`;
try {
targetStyleSheet.insertRule(focusRule, targetStyleSheet.cssRules.length);
} catch (e) {
console.warn('Failed to insert focus rule:', e);
}
}
});
}
/**
* Retrieves the stylesheet that should be used for dynamic rules
*
* @param {object} options - The options object
* @param {boolean} [options.fromExtension=false] - Indicates whether the rules are coming from extensions
* @return {CSSStyleSheet} The dynamic stylesheet
*/
function getDynamicStyleSheet({ fromExtension = false } = {}) {
if (fromExtension) {
if (!dynamicExtensionStyleSheet) {
const styleSheetElement = document.createElement('style');
styleSheetElement.setAttribute('id', 'dynamic-extension-styles');
document.head.appendChild(styleSheetElement);
dynamicExtensionStyleSheet = styleSheetElement.sheet;
}
return dynamicExtensionStyleSheet;
} else {
if (!dynamicStyleSheet) {
const styleSheetElement = document.createElement('style');
styleSheetElement.setAttribute('id', 'dynamic-styles');
document.head.appendChild(styleSheetElement);
dynamicStyleSheet = styleSheetElement.sheet;
}
return dynamicStyleSheet;
}
}
/**
* Initializes dynamic styles for ST
*/
export function initDynamicStyles() {
// Start observing the head for any new added stylesheets
observer.observe(document.head, {
childList: true,
subtree: true
});
// Process all stylesheets on initial load
Array.from(document.styleSheets).forEach(sheet => {
try {
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href.toLowerCase().includes('scripts/extensions') });
} catch (e) {
console.warn('Failed to process stylesheet on initial load:', e);
}
});
}

View File

@ -349,12 +349,12 @@ function autoConnectInputHandler() {
function addExtensionsButtonAndMenu() { function addExtensionsButtonAndMenu() {
const buttonHTML = const buttonHTML =
'<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles" title="Extras Extensions" /></div>'; '<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles interactable" title="Extras Extensions" /></div>';
const extensionsMenuHTML = '<div id="extensionsMenu" class="options-content" style="display: none;"></div>'; const extensionsMenuHTML = '<div id="extensionsMenu" class="options-content" style="display: none;"></div>';
$(document.body).append(extensionsMenuHTML); $(document.body).append(extensionsMenuHTML);
$('#leftSendForm').prepend(buttonHTML); $('#leftSendForm').append(buttonHTML);
const button = $('#extensionsMenuButton'); const button = $('#extensionsMenuButton');
const dropdown = $('#extensionsMenu'); const dropdown = $('#extensionsMenu');

View File

@ -1,11 +1,11 @@
<div id="assets_ui"> <div id="assets_ui">
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<b>Download Extensions & Assets</b> <b data-i18n="Download Extensions & Assets">Download Extensions & Assets</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<label for="assets-json-url-field">Assets URL</label> <label for="assets-json-url-field" data-i18n="Assets URL">Assets URL</label>
<div class="assets-connect-div"> <div class="assets-connect-div">
<input id="assets-json-url-field" class="text_pole widthUnset flex1"> <input id="assets-json-url-field" class="text_pole widthUnset flex1">
<i id="assets-connect-button" class="menu_button fa-solid fa-plug-circle-exclamation fa-xl redOverlayGlow"></i> <i id="assets-connect-button" class="menu_button fa-solid fa-plug-circle-exclamation fa-xl redOverlayGlow"></i>
@ -16,7 +16,7 @@
<input id="assets_search" class="text_pole flex1" placeholder="Search" type="search"> <input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">
<div id="assets-characters-button" class="menu_button menu_button_icon"> <div id="assets-characters-button" class="menu_button menu_button_icon">
<i class="fa-solid fa-image-portrait"></i> <i class="fa-solid fa-image-portrait"></i>
Characters <span data-i18n="Characters">Characters</span>
</div> </div>
</div> </div>
<div class="inline-drawer-content" id="assets_menu"> <div class="inline-drawer-content" id="assets_menu">

View File

@ -2,6 +2,10 @@ import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSour
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { SlashCommandExecutor } from '../../slash-commands/SlashCommandExecutor.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
/** /**
@ -196,9 +200,31 @@ jQuery(async () => {
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {}); const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
$('#extensionsMenu').prepend(buttons); $('#extensionsMenu').prepend(buttons);
/** A collection of local enum providers for this context of data bank */
const localEnumProviders = {
/**
* All attachments in the data bank based on the source argument. If not provided, defaults to 'chat'.
* @param {'name' | 'url'} returnField - Whether the enum should return the 'name' field or the 'url'
* @param {'chat' | 'character' | 'global' | ''} fallbackSource - The source to use if the source argument is not provided. Empty string to use all sources.
* */
attachments: (returnField = 'name', fallbackSource = 'chat') => (/** @type {SlashCommandExecutor} */ executor) => {
const source = executor.namedArgumentList.find(it => it.name == 'source')?.value ?? fallbackSource;
if (source instanceof SlashCommandClosure) throw new Error('Argument \'source\' does not support closures');
const attachments = getAttachments(source);
return attachments.map(attachment => new SlashCommandEnumValue(
returnField === 'name' ? attachment.name : attachment.url,
`${enumIcons.getStateIcon(!extension_settings.disabled_attachments.includes(attachment.url))} [${source}] ${returnField === 'url' ? attachment.name : attachment.url}`,
enumTypes.enum, enumIcons.file));
},
};
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db', name: 'db',
callback: () => document.getElementById('manageAttachments')?.click(), callback: () => {
document.getElementById('manageAttachments')?.click();
return '';
},
aliases: ['databank', 'data-bank'], aliases: ['databank', 'data-bank'],
helpString: 'Open the data bank', helpString: 'Open the data bank',
})); }));
@ -224,7 +250,13 @@ jQuery(async () => {
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), SlashCommandArgument.fromProps({
description: 'The name or URL of the attachment.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
acceptsMultiple: false,
enumProvider: localEnumProviders.attachments('name', ''),
}),
], ],
returns: ARGUMENT_TYPE.STRING, returns: ARGUMENT_TYPE.STRING,
})); }));
@ -251,8 +283,18 @@ jQuery(async () => {
helpString: 'Update an attachment in the Data Bank, preserving its name. Returns a new URL of the attachment.', helpString: 'Update an attachment in the Data Bank, preserving its name. Returns a new URL of the attachment.',
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false), SlashCommandNamedArgument.fromProps({
new SlashCommandNamedArgument('url', 'The URL of the attachment to update.', ARGUMENT_TYPE.STRING, false, false), name: 'name',
description: 'The name of the attachment.',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.attachments('name'),
}),
SlashCommandNamedArgument.fromProps({
name: 'url',
description: 'The URL of the attachment.',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.attachments('url'),
}),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false), new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
@ -269,7 +311,12 @@ jQuery(async () => {
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), SlashCommandArgument.fromProps({
description: 'The name or URL of the attachment.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.attachments('name', ''),
}),
], ],
})); }));
@ -282,7 +329,12 @@ jQuery(async () => {
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), SlashCommandArgument.fromProps({
description: 'The name or URL of the attachment.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.attachments('name', ''),
}),
], ],
})); }));
@ -295,7 +347,12 @@ jQuery(async () => {
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), SlashCommandArgument.fromProps({
description: 'The name or URL of the attachment.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.attachments(),
}),
], ],
})); }));
}); });

View File

@ -53,6 +53,14 @@
<i class="fa-solid fa-square"></i> <i class="fa-solid fa-square"></i>
<span data-i18n="Select None">Select None</span> <span data-i18n="Select None">Select None</span>
</div> </div>
<div class="menu_button menu_button_icon bulkActionDisable" title="Disable selected attachments">
<i class="fa-solid fa-comment-slash"></i>
<span data-i18n="Disable">Disable</span>
</div>
<div class="menu_button menu_button_icon bulkActionEnable" title="Enable selected attachments">
<i class="fa-solid fa-comment"></i>
<span data-i18n="Enable">Enable</span>
</div>
<div class="menu_button menu_button_icon bulkActionDelete" title="Delete selected attachments"> <div class="menu_button menu_button_icon bulkActionDelete" title="Delete selected attachments">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
<span data-i18n="Delete">Delete</span> <span data-i18n="Delete">Delete</span>
@ -88,8 +96,8 @@
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<strong><small class="characterAttachmentsName"></small></strong> <strong><small class="characterAttachmentsName"></small></strong>
<small> <small>
<span data-i18n="These files are available the current character in all chats they are in."> <span data-i18n="These files are available for the current character in all chats they are in.">
These files are available the current character in all chats they are in. These files are available for the current character in all chats they are in.
</span> </span>
<span> <span>
<span data-i18n="Saved locally. Not exported."> <span data-i18n="Saved locally. Not exported.">
@ -113,8 +121,8 @@
</h3> </h3>
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<strong><small class="chatAttachmentsName"></small></strong> <strong><small class="chatAttachmentsName"></small></strong>
<small data-i18n="These files are available to all characters in the current chat."> <small data-i18n="These files are available for all characters in the current chat.">
These files are available to all characters in the current chat. These files are available for all characters in the current chat.
</small> </small>
</div> </div>
<div class="chatAttachmentsList attachmentsList"></div> <div class="chatAttachmentsList attachmentsList"></div>

View File

@ -1,6 +1,6 @@
import { getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js'; import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js'; import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js'; import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
import { getMessageTimeStamp } from '../../RossAscends-mods.js'; import { getMessageTimeStamp } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state } from '../../secrets.js'; import { SECRET_KEYS, secret_state } from '../../secrets.js';
import { getMultimodalCaption } from '../shared.js'; import { getMultimodalCaption } from '../shared.js';
@ -8,6 +8,8 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'caption'; const MODULE_NAME = 'caption';
@ -95,7 +97,7 @@ async function sendCaptionedMessage(caption, image) {
template += ' {{caption}}'; template += ' {{caption}}';
} }
let messageText = substituteParams(template).replace(/{{caption}}/i, caption); let messageText = substituteParamsExtended(template, { caption: caption });
if (extension_settings.caption.refine_mode) { if (extension_settings.caption.refine_mode) {
messageText = await callPopup( messageText = await callPopup(
@ -272,7 +274,7 @@ async function getCaptionForFile(file, prompt, quiet) {
try { try {
setSpinnerIcon(); setSpinnerIcon();
const context = getContext(); const context = getContext();
const fileData = await getBase64Async(file); const fileData = await getBase64Async(await ensureImageFormatSupported(file));
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1]; const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
const base64Data = fileData.split(',')[1]; const base64Data = fileData.split(',')[1];
const { caption } = await doCaptionRequest(base64Data, fileData, prompt); const { caption } = await doCaptionRequest(base64Data, fileData, prompt);
@ -377,6 +379,12 @@ jQuery(async function () {
} }
function switchMultimodalBlocks() { function switchMultimodalBlocks() {
const isMultimodal = extension_settings.caption.source === 'multimodal'; const isMultimodal = extension_settings.caption.source === 'multimodal';
$('#caption_ollama_pull').on('click', (e) => {
const presetModel = extension_settings.caption.multimodal_model !== 'ollama_current' ? extension_settings.caption.multimodal_model : '';
e.preventDefault();
$('#ollama_download_model').trigger('click');
$('#dialogue_popup_input').val(presetModel);
});
$('#caption_multimodal_block').toggle(isMultimodal); $('#caption_multimodal_block').toggle(isMultimodal);
$('#caption_prompt_block').toggle(isMultimodal); $('#caption_prompt_block').toggle(isMultimodal);
$('#caption_multimodal_api').val(extension_settings.caption.multimodal_api); $('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
@ -445,11 +453,14 @@ jQuery(async function () {
returns: 'caption', returns: 'caption',
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'], 'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
),
new SlashCommandNamedArgument(
'id', 'get image from a message with this ID', [ARGUMENT_TYPE.NUMBER], false, false,
), ),
SlashCommandNamedArgument.fromProps({
name: 'id',
description: 'get image from a message with this ID',
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages(),
}),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(

View File

@ -2,38 +2,39 @@
<div class="caption_settings"> <div class="caption_settings">
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<b>Image Captioning</b> <b data-i18n="Image Captioning">Image Captioning</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<label for="caption_source">Source</label> <label for="caption_source" data-i18n="Source">Source</label>
<select id="caption_source" class="text_pole"> <select id="caption_source" class="text_pole">
<option value="local">Local</option> <option value="local" data-i18n="Local">Local</option>
<option value="multimodal">Multimodal (OpenAI / Anthropic / llama / Google)</option> <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option>
<option value="extras">Extras</option> <option value="extras" data-i18n="Extras">Extras</option>
<option value="horde">Horde</option> <option value="horde" data-i18n="Horde">Horde</option>
</select> </select>
<div id="caption_multimodal_block" class="flex-container wide100p"> <div id="caption_multimodal_block" class="flex-container wide100p">
<div class="flex1 flex-container flexFlowColumn flexNoGap"> <div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_api">API</label> <label for="caption_multimodal_api" data-i18n="API">API</label>
<select id="caption_multimodal_api" class="flex1 text_pole"> <select id="caption_multimodal_api" class="flex1 text_pole">
<option value="anthropic">Anthropic</option> <option value="anthropic">Anthropic</option>
<option value="custom">Custom (OpenAI-compatible)</option> <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
<option value="google">Google MakerSuite</option> <option value="google">Google MakerSuite</option>
<option value="koboldcpp">KoboldCpp</option> <option value="koboldcpp">KoboldCpp</option>
<option value="llamacpp">llama.cpp</option> <option value="llamacpp">llama.cpp</option>
<option value="ollama">Ollama</option> <option value="ollama">Ollama</option>
<option value="openai">OpenAI</option> <option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option> <option value="openrouter">OpenRouter</option>
<option value="ooba">Text Generation WebUI (oobabooga)</option> <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
</select> </select>
</div> </div>
<div class="flex1 flex-container flexFlowColumn flexNoGap"> <div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_model">Model</label> <label for="caption_multimodal_model" data-i18n="Model">Model</label>
<select id="caption_multimodal_model" class="flex1 text_pole"> <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-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-4-turbo">gpt-4-turbo</option>
<option data-type="openai" value="gpt-4o">gpt-4o</option> <option data-type="openai" value="gpt-4o">gpt-4o</option>
<option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</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-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option> <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
@ -44,9 +45,11 @@
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</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="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="fireworks/firellava-13b">fireworks/firellava-13b</option>
<option data-type="openrouter" value="anthropic/claude-3.5-sonnet">anthropic/claude-3.5-sonnet</option>
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</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-sonnet">anthropic/claude-3-sonnet</option>
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option> <option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
<option data-type="openrouter" value="anthropic/claude-3.5-sonnet:beta">anthropic/claude-3.5-sonnet:beta</option>
<option data-type="openrouter" value="anthropic/claude-3-haiku:beta">anthropic/claude-3-haiku:beta</option> <option data-type="openrouter" value="anthropic/claude-3-haiku:beta">anthropic/claude-3-haiku:beta</option>
<option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option> <option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option>
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option> <option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
@ -54,36 +57,42 @@
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</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="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="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="ollama_current" data-i18n="currently_selected">[Currently selected]</option>
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option> <option data-type="ollama" value="bakllava">bakllava</option>
<option data-type="ollama" value="llava:latest">llava:latest</option> <option data-type="ollama" value="llava">llava</option>
<option data-type="llamacpp" value="llamacpp_current">[Currently loaded]</option> <option data-type="ollama" value="llava-llama3">llava-llama3</option>
<option data-type="ooba" value="ooba_current">[Currently loaded]</option> <option data-type="ollama" value="llava-phi3">llava-phi3</option>
<option data-type="koboldcpp" value="koboldcpp_current">[Currently loaded]</option> <option data-type="ollama" value="moondream">moondream</option>
<option data-type="custom" value="custom_current">[Currently selected]</option> <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
<option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>
<option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
<option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
</select> </select>
</div> </div>
<div data-type="ollama">
The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>.
</div>
<label data-type="openai,anthropic,google" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid."> <label data-type="openai,anthropic,google" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox"> <input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
Allow reverse proxy <span data-i18n="Allow reverse proxy">Allow reverse proxy</span>
</label> </label>
<div class="flexBasis100p m-b-1"> <div class="flexBasis100p m-b-1">
<small><b>Hint:</b> Set your API keys and endpoints in the 'API Connections' tab first.</small> <small><b data-i18n="Hint:">Hint:</b> <span data-i18n="Set your API keys and endpoints in the 'API Connections' tab first.">Set your API keys and endpoints in the 'API Connections' tab first.</span></small>
</div> </div>
</div> </div>
<div id="caption_prompt_block"> <div id="caption_prompt_block">
<label for="caption_prompt">Caption Prompt</label> <label for="caption_prompt" data-i18n="Caption Prompt">Caption Prompt</label>
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="&lt; Use default &gt;">${PROMPT_DEFAULT}</textarea> <textarea id="caption_prompt" class="text_pole" rows="1" placeholder="&lt; Use default &gt;">${PROMPT_DEFAULT}</textarea>
<label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned."> <label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
<input id="caption_prompt_ask" type="checkbox" class="checkbox"> <input id="caption_prompt_ask" type="checkbox" class="checkbox">
Ask every time <span data-i18n="Ask every time">Ask every time</span>
</label> </label>
</div> </div>
<label for="caption_template">Message Template <small>(use <code>&lcub;&lcub;caption&rcub;&rcub;</code> macro)</small></label> <label for="caption_template"><span data-i18n="Message Template">Message Template</span> <small><span data-i18n="(use _space">(use </span> <code>&lcub;&lcub;caption&rcub;&rcub;</code> <span data-i18n="macro)">macro)</span></small></label>
<textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">${TEMPLATE_DEFAULT}</textarea> <textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">${TEMPLATE_DEFAULT}</textarea>
<label class="checkbox_label margin-bot-10px" for="caption_refine_mode"> <label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
<input id="caption_refine_mode" type="checkbox" class="checkbox"> <input id="caption_refine_mode" type="checkbox" class="checkbox">
Edit captions before saving <span data-i18n="Edit captions before saving">Edit captions before saving</span>
</label> </label>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams } from '../../../script.js'; import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';
import { dragElement, isMobile } from '../../RossAscends-mods.js'; import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js'; import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, power_user } from '../../power-user.js'; import { loadMovingUIState, power_user } from '../../power-user.js';
@ -10,6 +10,8 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js';
import { isFunctionCallingSupported } from '../../openai.js'; import { isFunctionCallingSupported } from '../../openai.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'expressions'; const MODULE_NAME = 'expressions';
@ -87,6 +89,7 @@ function getFallbackExpression() {
*/ */
function toggleTalkingHeadCommand(_) { function toggleTalkingHeadCommand(_) {
setTalkingHeadState(!extension_settings.expressions.talkinghead); setTalkingHeadState(!extension_settings.expressions.talkinghead);
return String(extension_settings.expressions.talkinghead);
} }
function isVisualNovelMode() { function isVisualNovelMode() {
@ -914,6 +917,7 @@ async function setSpriteSetCommand(_, folder) {
// moduleWorker(); // moduleWorker();
const vnMode = isVisualNovelMode(); const vnMode = isVisualNovelMode();
await sendExpressionCall(folder, lastExpression, true, vnMode); await sendExpressionCall(folder, lastExpression, true, vnMode);
return '';
} }
async function classifyCommand(_, text) { async function classifyCommand(_, text) {
@ -935,7 +939,7 @@ async function classifyCommand(_, text) {
async function setSpriteSlashCommand(_, spriteId) { async function setSpriteSlashCommand(_, spriteId) {
if (!spriteId) { if (!spriteId) {
console.log('No sprite id provided'); console.log('No sprite id provided');
return; return '';
} }
spriteId = spriteId.trim().toLowerCase(); spriteId = spriteId.trim().toLowerCase();
@ -955,7 +959,7 @@ async function setSpriteSlashCommand(_, spriteId) {
if (!spriteItem) { if (!spriteItem) {
console.log('No sprite found for search term ' + spriteId); console.log('No sprite found for search term ' + spriteId);
return; return '';
} }
label = spriteItem.label; label = spriteItem.label;
@ -963,6 +967,7 @@ async function setSpriteSlashCommand(_, spriteId) {
const vnMode = isVisualNovelMode(); const vnMode = isVisualNovelMode();
await sendExpressionCall(spriteFolderName, label, true, vnMode); await sendExpressionCall(spriteFolderName, label, true, vnMode);
return label;
} }
/** /**
@ -1008,8 +1013,7 @@ async function getLlmPrompt(labels) {
} }
const labelsString = labels.map(x => `"${x}"`).join(', '); const labelsString = labels.map(x => `"${x}"`).join(', ');
const prompt = substituteParams(String(extension_settings.expressions.llmPrompt)) const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });
.replace(/{{labels}}/gi, labelsString);
return prompt; return prompt;
} }
@ -1187,7 +1191,7 @@ function getLastCharacterMessage() {
const reversedChat = context.chat.slice().reverse(); const reversedChat = context.chat.slice().reverse();
for (let mes of reversedChat) { for (let mes of reversedChat) {
if (mes.is_user || mes.is_system) { if (mes.is_user || mes.is_system || mes.extra?.type === system_message_types.NARRATOR) {
continue; continue;
} }
@ -1326,10 +1330,18 @@ async function renderFallbackExpressionPicker() {
} }
} }
function getCachedExpressions() {
if (!Array.isArray(expressionsList)) {
return [];
}
return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);
}
async function getExpressionsList() { async function getExpressionsList() {
// Return cached list if available // Return cached list if available
if (Array.isArray(expressionsList)) { if (Array.isArray(expressionsList)) {
return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique); return getCachedExpressions();
} }
/** /**
@ -2033,17 +2045,31 @@ function migrateSettings() {
}); });
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced); eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sprite',
const localEnumProviders = {
expressions: () => getCachedExpressions().map(expression => {
const isCustom = extension_settings.expressions.custom?.includes(expression);
return new SlashCommandEnumValue(expression, null, isCustom ? enumTypes.name : enumTypes.enum, isCustom ? 'C' : 'D');
}),
};
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'sprite',
aliases: ['emote'], aliases: ['emote'],
callback: setSpriteSlashCommand, callback: setSpriteSlashCommand,
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'spriteId', [ARGUMENT_TYPE.STRING], true, description: 'spriteId',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.expressions,
}),
], ],
helpString: 'Force sets the sprite for the current character.', helpString: 'Force sets the sprite for the current character.',
returns: 'label',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'spriteoverride', SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'spriteoverride',
aliases: ['costume'], aliases: ['costume'],
callback: setSpriteSetCommand, callback: setSpriteSetCommand,
unnamedArgumentList: [ unnamedArgumentList: [
@ -2053,22 +2079,29 @@ function migrateSettings() {
], ],
helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lastsprite', SlashCommandParser.addCommandObject(SlashCommand.fromProps({
callback: (_, value) => lastExpression[value.trim()] ?? '', name: 'lastsprite',
callback: (_, value) => lastExpression[String(value).trim()] ?? '',
returns: 'sprite', returns: 'sprite',
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'charName', [ARGUMENT_TYPE.STRING], true, description: 'character name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
}),
], ],
helpString: 'Returns the last set sprite / expression for the named character.', helpString: 'Returns the last set sprite / expression for the named character.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'th', SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'th',
callback: toggleTalkingHeadCommand, callback: toggleTalkingHeadCommand,
aliases: ['talkinghead'], aliases: ['talkinghead'],
helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
returns: ARGUMENT_TYPE.BOOLEAN,
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'classify', SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'classify',
callback: classifyCommand, callback: classifyCommand,
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(

View File

@ -1,65 +1,65 @@
<div class="expression_settings"> <div class="expression_settings">
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<b>Character Expressions</b> <b data-i18n="Character Expressions">Character Expressions</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings."> <label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings.">
<input id="expression_translate" type="checkbox"> <input id="expression_translate" type="checkbox">
<span>Translate text to English before classification</span> <span data-i18n="Translate text to English before classification">Translate text to English before classification</span>
</label> </label>
<label class="checkbox_label" for="expressions_show_default"> <label class="checkbox_label" for="expressions_show_default">
<input id="expressions_show_default" type="checkbox"> <input id="expressions_show_default" type="checkbox">
<span>Show default images (emojis) if sprite missing</span> <span data-i18n="Show default images (emojis) if sprite missing">Show default images (emojis) if sprite missing</span>
</label> </label>
<label id="image_type_block" class="checkbox_label" for="image_type_toggle"> <label id="image_type_block" class="checkbox_label" for="image_type_toggle">
<input id="image_type_toggle" type="checkbox"> <input id="image_type_toggle" type="checkbox">
<span>Image Type - talkinghead (extras)</span> <span data-i18n="Image Type - talkinghead (extras)">Image Type - talkinghead (extras)</span>
</label> </label>
<div class="expression_api_block m-b-1 m-t-1"> <div class="expression_api_block m-b-1 m-t-1">
<label for="expression_api">Classifier API</label> <label for="expression_api" data-i18n="Classifier API">Classifier API</label>
<small>Select the API for classifying expressions.</small> <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>
<select id="expression_api" class="flex1 margin0"> <select id="expression_api" class="flex1 margin0">
<option value="0">Local</option> <option value="0" data-i18n="Local">Local</option>
<option value="1">Extras</option> <option value="1" data-i18n="Extras">Extras</option>
<option value="2">LLM</option> <option value="2" data-i18n="LLM">LLM</option>
</select> </select>
</div> </div>
<div class="expression_llm_prompt_block m-b-1 m-t-1"> <div class="expression_llm_prompt_block m-b-1 m-t-1">
<label for="expression_llm_prompt" class="title_restorable"> <label for="expression_llm_prompt" class="title_restorable">
<span>LLM Prompt</span> <span data-i18n="LLM Prompt">LLM Prompt</span>
<div id="expression_llm_prompt_restore" title="Restore default value" class="right_menu_button"> <div id="expression_llm_prompt_restore" title="Restore default value" class="right_menu_button">
<i class="fa-solid fa-clock-rotate-left fa-sm"></i> <i class="fa-solid fa-clock-rotate-left fa-sm"></i>
</div> </div>
</label> </label>
<small>Will be used if the API doesn't support JSON schemas or function calling.</small> <small data-i18n="Will be used if the API doesn't support JSON schemas or function calling.">Will be used if the API doesn't support JSON schemas or function calling.</small>
<textarea id="expression_llm_prompt" type="text" class="text_pole textarea_compact" rows="2" placeholder="Use &lcub;&lcub;labels&rcub;&rcub; special macro."></textarea> <textarea id="expression_llm_prompt" type="text" class="text_pole textarea_compact" rows="2" placeholder="Use &lcub;&lcub;labels&rcub;&rcub; special macro."></textarea>
</div> </div>
<div class="expression_fallback_block m-b-1 m-t-1"> <div class="expression_fallback_block m-b-1 m-t-1">
<label for="expression_fallback">Default / Fallback Expression</label> <label for="expression_fallback" data-i18n="Default / Fallback Expression">Default / Fallback Expression</label>
<small>Set the default and fallback expression being used when no matching expression is found.</small> <small data-i18n="Set the default and fallback expression being used when no matching expression is found.">Set the default and fallback expression being used when no matching expression is found.</small>
<select id="expression_fallback" class="flex1 margin0" data-i18n="Fallback Expression" placeholder="Fallback Expression"></select> <select id="expression_fallback" class="flex1 margin0" data-i18n="Fallback Expression" placeholder="Fallback Expression"></select>
</div> </div>
<div class="expression_custom_block m-b-1 m-t-1"> <div class="expression_custom_block m-b-1 m-t-1">
<label for="expression_custom">Custom Expressions</label> <label for="expression_custom" data-i18n="Custom Expressions">Custom Expressions</label>
<small>Can be set manually or with an <tt>/emote</tt> slash command.</small> <small><span data-i18n="Can be set manually or with an _space">Can be set manually or with an </span><tt>/emote</tt><span data-i18n="space_ slash command."> slash command.</span></small>
<div class="flex-container"> <div class="flex-container">
<select id="expression_custom" class="flex1 margin0"><select> <select id="expression_custom" class="flex1 margin0"><select>
<i id="expression_custom_add" class="menu_button fa-solid fa-plus margin0" title="Add"></i> <i id="expression_custom_add" class="menu_button fa-solid fa-plus margin0" title="Add"></i>
<i id="expression_custom_remove" class="menu_button fa-solid fa-xmark margin0" title="Remove"></i> <i id="expression_custom_remove" class="menu_button fa-solid fa-xmark margin0" title="Remove"></i>
</div> </div>
</div> </div>
<div id="no_chat_expressions"> <div id="no_chat_expressions" data-i18n="Open a chat to see the character expressions.">
Open a chat to see the character expressions. Open a chat to see the character expressions.
</div> </div>
<div id="open_chat_expressions"> <div id="open_chat_expressions">
<div class="offline_mode"> <div class="offline_mode">
<small>You are in offline mode. Click on the image below to set the expression.</small> <small data-i18n="You are in offline mode. Click on the image below to set the expression.">You are in offline mode. Click on the image below to set the expression.</small>
</div> </div>
<label for="expression_override">Sprite Folder Override</label> <label for="expression_override" data-i18n="Sprite Folder Override">Sprite Folder Override</label>
<small>Use a forward slash to specify a subfolder. Example: <tt>Bob/formal</tt></small> <small><span data-i18n="Use a forward slash to specify a subfolder. Example: _space">Use a forward slash to specify a subfolder. Example: </span><tt>Bob/formal</tt></small>
<div class="flex-container flexnowrap"> <div class="flex-container flexnowrap">
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" /> <input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" /> <input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
@ -67,17 +67,17 @@
<div class="expression_buttons flex-container spaceEvenly"> <div class="expression_buttons flex-container spaceEvenly">
<div id="expression_upload_pack_button" class="menu_button"> <div id="expression_upload_pack_button" class="menu_button">
<i class="fa-solid fa-file-zipper"></i> <i class="fa-solid fa-file-zipper"></i>
<span>Upload sprite pack (ZIP)</span> <span data-i18n="Upload sprite pack (ZIP)">Upload sprite pack (ZIP)</span>
</div> </div>
<div id="expression_override_cleanup_button" class="menu_button"> <div id="expression_override_cleanup_button" class="menu_button">
<i class="fa-solid fa-trash-can"></i> <i class="fa-solid fa-trash-can"></i>
<span>Remove all image overrides</span> <span data-i18n="Remove all image overrides">Remove all image overrides</span>
</div> </div>
</div> </div>
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>/characters/</b> folder of your user data directory and name it as the name of the character. <p class="hint"><b data-i18n="Hint:">Hint:</b> <i><span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p> <span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt></i></p>
<h3 id="image_list_header"> <h3 id="image_list_header">
<strong>Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span> <strong data-i18n="Sprite set:">Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>
</h3> </h3>
<div id="image_list"></div> <div id="image_list"></div>

View File

@ -11,6 +11,8 @@ import { dragElement } from '../../RossAscends-mods.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { DragAndDropHandler } from '../../dragdrop.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
const extensionName = 'gallery'; const extensionName = 'gallery';
const extensionFolderPath = `scripts/extensions/${extensionName}/`; const extensionFolderPath = `scripts/extensions/${extensionName}/`;
@ -56,7 +58,8 @@ async function getGalleryItems(url) {
* @returns {Promise<void>} - Promise representing the completion of the gallery initialization. * @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
*/ */
async function initGallery(items, url) { async function initGallery(items, url) {
$('#dragGallery').nanogallery2({ const gallery = $('#dragGallery');
gallery.nanogallery2({
'items': items, 'items': items,
thumbnailWidth: 'auto', thumbnailWidth: 'auto',
thumbnailHeight: thumbnailHeight, thumbnailHeight: thumbnailHeight,
@ -80,44 +83,24 @@ async function initGallery(items, url) {
eventSource.on('resizeUI', function (elmntName) { eventSource.on('resizeUI', function (elmntName) {
jQuery('#dragGallery').nanogallery2('resize'); gallery.nanogallery2('resize');
}); });
const dropZone = $('#dragGallery'); const dragDropHandler = new DragAndDropHandler('#dragGallery', async (files, event) => {
//remove any existing handlers let file = files[0];
dropZone.off('dragover');
dropZone.off('dragleave');
dropZone.off('drop');
// Set dropzone height to be the same as the parent
dropZone.css('height', dropZone.parent().css('height'));
// Initialize dropzone handlers
dropZone.on('dragover', function (e) {
e.stopPropagation(); // Ensure this event doesn't propagate
e.preventDefault();
$(this).addClass('dragging'); // Add a CSS class to change appearance during drag-over
});
dropZone.on('dragleave', function (e) {
e.stopPropagation(); // Ensure this event doesn't propagate
$(this).removeClass('dragging');
});
dropZone.on('drop', function (e) {
e.stopPropagation(); // Ensure this event doesn't propagate
e.preventDefault();
$(this).removeClass('dragging');
let file = e.originalEvent.dataTransfer.files[0];
uploadFile(file, url); // Added url parameter to know where to upload uploadFile(file, url); // Added url parameter to know where to upload
}); });
// Set dropzone height to be the same as the parent
gallery.css('height', gallery.parent().css('height'));
//let images populate first //let images populate first
await delay(100); await delay(100);
//unset the height (which must be getting set by the gallery library at some point) //unset the height (which must be getting set by the gallery library at some point)
$('#dragGallery').css('height', 'unset'); gallery.css('height', 'unset');
//force a resize to make images display correctly //force a resize to make images display correctly
jQuery('#dragGallery').nanogallery2('resize'); gallery.nanogallery2('resize');
} }
/** /**
@ -419,7 +402,10 @@ function viewWithDragbox(items) {
// Registers a simple command for opening the char gallery. // Registers a simple command for opening the char gallery.
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery',
aliases: ['sg'], aliases: ['sg'],
callback: showGalleryCommand, callback: () => {
showCharGallery();
return '';
},
helpString: 'Shows the gallery.', helpString: 'Shows the gallery.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery',
@ -427,21 +413,22 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery
callback: listGalleryCommand, callback: listGalleryCommand,
returns: 'list of images', returns: 'list of images',
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( SlashCommandNamedArgument.fromProps({
'char', 'character name', [ARGUMENT_TYPE.STRING], false, name: 'char',
), description: 'character name',
new SlashCommandNamedArgument( typeList: [ARGUMENT_TYPE.STRING],
'group', 'group name', [ARGUMENT_TYPE.STRING], false, enumProvider: commonEnumProviders.characters('character'),
), }),
SlashCommandNamedArgument.fromProps({
name: 'group',
description: 'group name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('group'),
}),
], ],
helpString: 'List images in the gallery of the current char / group or a specified char / group.', helpString: 'List images in the gallery of the current char / group or a specified char / group.',
})); }));
function showGalleryCommand(args) {
showCharGallery();
}
async function listGalleryCommand(args) { async function listGalleryCommand(args) {
try { try {
let url = args.char ?? (args.group ? groups.find(it=>it.name == args.group)?.id : null) ?? (selected_group || this_chid); let url = args.char ?? (args.group ? groups.find(it=>it.name == args.group)?.id : null) ?? (selected_group || this_chid);

View File

@ -11,7 +11,7 @@ import {
generateQuietPrompt, generateQuietPrompt,
is_send_press, is_send_press,
saveSettingsDebounced, saveSettingsDebounced,
substituteParams, substituteParamsExtended,
generateRaw, generateRaw,
getMaxContextSize, getMaxContextSize,
} from '../../../script.js'; } from '../../../script.js';
@ -24,6 +24,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { resolveVariable } from '../../variables.js'; import { resolveVariable } from '../../variables.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = '1_memory'; const MODULE_NAME = '1_memory';
@ -43,8 +44,7 @@ const formatMemoryValue = function (value) {
value = value.trim(); value = value.trim();
if (extension_settings.memory.template) { if (extension_settings.memory.template) {
let result = extension_settings.memory.template.replace(/{{summary}}/i, value); return substituteParamsExtended(extension_settings.memory.template, { summary: value });
return substituteParams(result);
} else { } else {
return `Summary: ${value}`; return `Summary: ${value}`;
} }
@ -447,7 +447,7 @@ async function summarizeCallback(args, text) {
} }
const source = args.source || extension_settings.memory.source; const source = args.source || extension_settings.memory.source;
const prompt = substituteParams((resolveVariable(args.prompt) || extension_settings.memory.prompt)?.replace(/{{words}}/gi, extension_settings.memory.promptWords)); const prompt = substituteParamsExtended((resolveVariable(args.prompt) || extension_settings.memory.prompt), { words: extension_settings.memory.promptWords });
try { try {
switch (source) { switch (source) {
@ -534,7 +534,7 @@ async function summarizeChatMain(context, force, skipWIAN) {
} }
console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary); console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);
const prompt = extension_settings.memory.prompt?.replace(/{{words}}/gi, extension_settings.memory.promptWords); const prompt = substituteParamsExtended(extension_settings.memory.prompt, { words: extension_settings.memory.promptWords });
if (!prompt) { if (!prompt) {
console.debug('Summarization prompt is empty. Skipping summarization.'); console.debug('Summarization prompt is empty. Skipping summarization.');
@ -920,7 +920,14 @@ jQuery(async function () {
callback: summarizeCallback, callback: summarizeCallback,
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']), new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']),
new SlashCommandNamedArgument('prompt', 'prompt to use for summarization', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false, false, ''), SlashCommandNamedArgument.fromProps({
name: 'prompt',
description: 'prompt to use for summarization',
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME],
defaultValue: '',
enumProvider: commonEnumProviders.variables('all'),
forceEnum: false,
}),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''), new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),

View File

@ -2,7 +2,7 @@
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<div class="flex-container alignitemscenter margin0"> <div class="flex-container alignitemscenter margin0">
<b>Summarize</b> <b data-i18n="ext_sum_title">Summarize</b>
<i id="summaryExtensionPopoutButton" class="fa-solid fa-window-restore menu_button margin0"></i> <i id="summaryExtensionPopoutButton" class="fa-solid fa-window-restore menu_button margin0"></i>
</div> </div>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
@ -24,7 +24,7 @@
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea> <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls"> <div class="memory_contents_controls">
<div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" data-i18n="[title]ext_sum_force_tip" title="Trigger a summary update right now." data-i18n="Trigger a summary update right now."> <div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="[title]ext_sum_force_tip">
<i class="fa-solid fa-database"></i> <i class="fa-solid fa-database"></i>
<span data-i18n="ext_sum_force_text">Summarize now</span> <span data-i18n="ext_sum_force_text">Summarize now</span>
</div> </div>

View File

@ -1,38 +1,42 @@
<div id="qr--modalEditor"> <div id="qr--modalEditor">
<div id="qr--main"> <div id="qr--main">
<h3>Labels and Message</h3> <h3 data-i18n="Labels and Message">Labels and Message</h3>
<div class="qr--labels"> <div class="qr--labels">
<label> <label>
<span class="qr--labelText">Label</span> <span class="qr--labelText" data-i18n="Label">Label</span>
<input type="text" class="text_pole" id="qr--modal-label"> <input type="text" class="text_pole" id="qr--modal-label">
</label> </label>
<label> <label>
<span class="qr--labelText">Title</span> <span class="qr--labelText" data-i18n="Title">Title</span>
<small class="qr--labelHint">(tooltip, leave empty to show message or /command)</small> <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>
<input type="text" class="text_pole" id="qr--modal-title"> <input type="text" class="text_pole" id="qr--modal-title">
</label> </label>
</div> </div>
<div class="qr--modal-messageContainer"> <div class="qr--modal-messageContainer">
<label for="qr--modal-message"> <label for="qr--modal-message" data-i18n="Message / Command:">
Message / Command: Message / Command:
</label> </label>
<div class="qr--modal-editorSettings"> <div class="qr--modal-editorSettings">
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--modal-wrap"> <input type="checkbox" id="qr--modal-wrap">
<span>Word wrap</span> <span data-i18n="Word wrap">Word wrap</span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<span>Tab size:</span> <span data-i18n="Tab size:">Tab size:</span>
<input type="number" min="1" max="9" id="qr--modal-tabSize" class="text_pole"> <input type="number" min="1" max="9" id="qr--modal-tabSize" class="text_pole">
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--modal-executeShortcut"> <input type="checkbox" id="qr--modal-executeShortcut">
<span>Ctrl+Enter to execute</span> <span data-i18n="Ctrl+Enter to execute">Ctrl+Enter to execute</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="qr--modal-syntax">
<span>Syntax highlight</span>
</label> </label>
</div> </div>
<div id="qr--modal-messageHolder"> <div id="qr--modal-messageHolder">
<pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre> <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
<textarea class="monospace" id="qr--modal-message" spellcheck="false"></textarea> <textarea id="qr--modal-message" spellcheck="false"></textarea>
</div> </div>
</div> </div>
</div> </div>
@ -40,14 +44,14 @@
<div id="qr--qrOptions"> <div id="qr--qrOptions">
<h3>Context Menu</h3> <h3 data-i18n="Context Menu">Context Menu</h3>
<div id="qr--ctxEditor"> <div id="qr--ctxEditor">
<template id="qr--ctxItem"> <template id="qr--ctxItem">
<div class="qr--ctxItem" data-order="0"> <div class="qr--ctxItem" data-order="0">
<div class="drag-handle ui-sortable-handle"></div> <div class="drag-handle ui-sortable-handle"></div>
<select class="qr--set"></select> <select class="qr--set"></select>
<label class="qr--isChainedLabel checkbox_label" title="When enabled, the current Quick Reply will be sent together with (before) the clicked QR from the context menu."> <label class="qr--isChainedLabel checkbox_label" title="When enabled, the current Quick Reply will be sent together with (before) the clicked QR from the context menu.">
Chaining: <span data-i18n="Chaining:">Chaining:</span>
<input type="checkbox" class="qr--isChained"> <input type="checkbox" class="qr--isChained">
</label> </label>
<div class="qr--delete menu_button menu_button_icon fa-solid fa-trash-can" title="Remove entry"></div> <div class="qr--delete menu_button menu_button_icon fa-solid fa-trash-can" title="Remove entry"></div>
@ -59,48 +63,48 @@
</div> </div>
<h3>Auto-Execute</h3> <h3 data-i18n="Auto-Execute">Auto-Execute</h3>
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)"> <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
<input type="checkbox" id="qr--preventAutoExecute" > <input type="checkbox" id="qr--preventAutoExecute" >
<span><i class="fa-solid fa-fw fa-plane-slash"></i> Don't trigger auto-execute</span> <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--isHidden" > <input type="checkbox" id="qr--isHidden" >
<span><i class="fa-solid fa-fw fa-eye-slash"></i> Invisible (auto-execute only)</span> <span><i class="fa-solid fa-fw fa-eye-slash"></i><span data-i18n="Invisible (auto-execute only)">Invisible (auto-execute only)</span></span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--executeOnStartup" > <input type="checkbox" id="qr--executeOnStartup" >
<span><i class="fa-solid fa-fw fa-rocket"></i> Execute on app startup</span> <span><i class="fa-solid fa-fw fa-rocket"></i><span data-i18n="Execute on startup">Execute on startup</span></span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--executeOnUser" > <input type="checkbox" id="qr--executeOnUser" >
<span><i class="fa-solid fa-fw fa-user"></i> Execute on user message</span> <span><i class="fa-solid fa-fw fa-user"></i><span data-i18n="Execute on user message">Execute on user message</span></span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--executeOnAi" > <input type="checkbox" id="qr--executeOnAi" >
<span><i class="fa-solid fa-fw fa-robot"></i> Execute on AI message</span> <span><i class="fa-solid fa-fw fa-robot"></i><span data-i18n="Execute on AI message">Execute on AI message</span></span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--executeOnChatChange" > <input type="checkbox" id="qr--executeOnChatChange" >
<span><i class="fa-solid fa-fw fa-message"></i> Execute on opening chat</span> <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>
</label> </label>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--executeOnGroupMemberDraft"> <input type="checkbox" id="qr--executeOnGroupMemberDraft">
<span><i class="fa-solid fa-fw fa-people-group"></i> Execute before group member message</span> <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>
</label> </label>
<div class="flex-container alignItemsBaseline flexFlowColumn flexNoGap" title="Activate this quick reply when a World Info entry with the same Automation ID is triggered."> <div class="flex-container alignItemsBaseline flexFlowColumn flexNoGap" title="Activate this quick reply when a World Info entry with the same Automation ID is triggered.">
<small>Automation ID</small> <small data-i18n="Automation ID:">Automation ID</small>
<input type="text" id="qr--automationId" class="text_pole flex1" placeholder="( None )"> <input type="text" id="qr--automationId" class="text_pole flex1" placeholder="( None )">
</div> </div>
</div> </div>
<h3>Testing</h3> <h3 data-i18n="Testing">Testing</h3>
<div id="qr--modal-executeButtons"> <div id="qr--modal-executeButtons">
<div id="qr--modal-execute" class="qr--modal-executeButton menu_button" title="Execute the quick reply now"> <div id="qr--modal-execute" class="qr--modal-executeButton menu_button" title="Execute the quick reply now">
<i class="fa-solid fa-play"></i> <i class="fa-solid fa-play"></i>
Execute <span data-i18n="Execute">Execute</span>
</div> </div>
<div id="qr--modal-pause" class="qr--modal-executeButton menu_button" title="Pause / continue execution"> <div id="qr--modal-pause" class="qr--modal-executeButton menu_button" title="Pause / continue execution">
<span class="qr--modal-executeComboIcon"> <span class="qr--modal-executeComboIcon">
@ -115,7 +119,7 @@
<div id="qr--modal-executeProgress"></div> <div id="qr--modal-executeProgress"></div>
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" id="qr--modal-executeHide"> <input type="checkbox" id="qr--modal-executeHide">
<span> Hide editor while executing</span> <span title="Hide editor while executing"> Hide editor while executing</span>
</label> </label>
<div id="qr--modal-executeErrors"></div> <div id="qr--modal-executeErrors"></div>
<div id="qr--modal-executeResult"></div> <div id="qr--modal-executeResult"></div>

View File

@ -1,22 +1,22 @@
<div id="qr--settings"> <div id="qr--settings">
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<strong>Quick Reply</strong> <strong data-i18n="Quick Reply">Quick Reply</strong>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<label class="flex-container"> <label class="flex-container">
<input type="checkbox" id="qr--isEnabled"> Enable Quick Replies <input type="checkbox" id="qr--isEnabled"><span data-i18n="Enable Quick Replies">Enable Quick Replies</span>
</label> </label>
<label class="flex-container"> <label class="flex-container">
<input type="checkbox" id="qr--isCombined"> Combine buttons from all active sets <input type="checkbox" id="qr--isCombined"><span data-i18n="Combine Quick Replies">Combine Quick Replies</span>
</label> </label>
<hr> <hr>
<div id="qr--global"> <div id="qr--global">
<div class="qr--head"> <div class="qr--head">
<div class="qr--title">Global Quick Reply Sets</div> <div class="qr--title" data-i18n="Global Quick Reply Sets">Global Quick Reply Sets</div>
<div class="qr--actions"> <div class="qr--actions">
<div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--global-setListAdd" title="Add quick reply set"></div> <div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--global-setListAdd" title="Add quick reply set"></div>
</div> </div>
@ -28,7 +28,7 @@
<div id="qr--chat"> <div id="qr--chat">
<div class="qr--head"> <div class="qr--head">
<div class="qr--title">Chat Quick Reply Sets</div> <div class="qr--title" data-i18n="Chat Quick Reply Sets">Chat Quick Reply Sets</div>
<div class="qr--actions"> <div class="qr--actions">
<div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--chat-setListAdd" title="Add quick reply set"></div> <div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--chat-setListAdd" title="Add quick reply set"></div>
</div> </div>
@ -40,7 +40,7 @@
<div id="qr--editor"> <div id="qr--editor">
<div class="qr--head"> <div class="qr--head">
<div class="qr--title">Edit Quick Replies</div> <div class="qr--title" data-i18n="Edit Quick Replies">Edit Quick Replies</div>
<div class="qr--actions"> <div class="qr--actions">
<select id="qr--set" class="text_pole"></select> <select id="qr--set" class="text_pole"></select>
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-new" title="Create new quick reply set"></div> <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-new" title="Create new quick reply set"></div>
@ -52,13 +52,13 @@
</div> </div>
<div id="qr--set-settings"> <div id="qr--set-settings">
<label class="flex-container"> <label class="flex-container">
<input type="checkbox" id="qr--disableSend"> <span>Disable send (insert into input field)</span> <input type="checkbox" id="qr--disableSend"> <span data-i18n="Disable Send (Insert Into Input Field)">Disable send (insert into input field)</span>
</label> </label>
<label class="flex-container"> <label class="flex-container">
<input type="checkbox" id="qr--placeBeforeInput"> <span>Place quick reply before input</span> <input type="checkbox" id="qr--placeBeforeInput"> <span data-i18n="Place Quick Reply Before Input">Place quick reply before input</span>
</label> </label>
<label class="flex-container" id="qr--injectInputContainer"> <label class="flex-container" id="qr--injectInputContainer">
<input type="checkbox" id="qr--injectInput"> <span>Inject user input automatically <small>(if disabled, use <code>{{input}}</code> macro for manual injection)</small></span> <input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>
</label> </label>
</div> </div>
<div id="qr--set-qrList" class="qr--qrList"></div> <div id="qr--set-qrList" class="qr--qrList"></div>

View File

@ -264,6 +264,13 @@ export class QuickReply {
const updateSyntax = ()=>{ const updateSyntax = ()=>{
messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value; messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
}; };
const updateSyntaxEnabled = ()=>{
if (JSON.parse(localStorage.getItem('qr--syntax'))) {
dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
} else {
dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
}
};
/**@type {HTMLInputElement}*/ /**@type {HTMLInputElement}*/
const tabSize = dom.querySelector('#qr--modal-tabSize'); const tabSize = dom.querySelector('#qr--modal-tabSize');
tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4'); tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');
@ -282,6 +289,13 @@ export class QuickReply {
executeShortcut.addEventListener('click', () => { executeShortcut.addEventListener('click', () => {
localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked)); localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
}); });
/**@type {HTMLInputElement}*/
const syntax = dom.querySelector('#qr--modal-syntax');
syntax.checked = JSON.parse(localStorage.getItem('qr--syntax') ?? 'true');
syntax.addEventListener('click', () => {
localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
updateSyntaxEnabled();
});
/**@type {HTMLTextAreaElement}*/ /**@type {HTMLTextAreaElement}*/
const message = dom.querySelector('#qr--modal-message'); const message = dom.querySelector('#qr--modal-message');
message.value = this.message; message.value = this.message;
@ -352,8 +366,7 @@ export class QuickReply {
} }
}); });
window.addEventListener('resize', resizeListener); window.addEventListener('resize', resizeListener);
message.style.color = 'transparent'; updateSyntaxEnabled();
message.style.background = 'transparent';
message.style.setProperty('text-shadow', 'none', 'important'); message.style.setProperty('text-shadow', 'none', 'important');
/**@type {HTMLElement}*/ /**@type {HTMLElement}*/
const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner'); const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
@ -544,7 +557,7 @@ export class QuickReply {
this.editorExecuteErrors.innerHTML = ''; this.editorExecuteErrors.innerHTML = '';
this.editorExecuteResult.innerHTML = ''; this.editorExecuteResult.innerHTML = '';
if (this.editorExecuteHide.checked) { if (this.editorExecuteHide.checked) {
this.editorPopup.dom.classList.add('qr--hide'); this.editorPopup.dlg.classList.add('qr--hide');
} }
try { try {
this.editorExecutePromise = this.execute({}, true); this.editorExecutePromise = this.execute({}, true);
@ -575,7 +588,7 @@ export class QuickReply {
} }
this.editorExecutePromise = null; this.editorExecutePromise = null;
this.editorExecuteBtn.classList.remove('qr--busy'); this.editorExecuteBtn.classList.remove('qr--busy');
this.editorPopup.dom.classList.remove('qr--hide'); this.editorPopup.dlg.classList.remove('qr--hide');
} }
updateEditorProgress(done, total) { updateEditorProgress(done, total) {

View File

@ -1,9 +1,13 @@
import { SlashCommand } from '../../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { isTrueBoolean } from '../../../utils.js'; import { isTrueBoolean } from '../../../utils.js';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { QuickReplyApi } from '../api/QuickReplyApi.js'; import { QuickReplyApi } from '../api/QuickReplyApi.js';
import { QuickReply } from './QuickReply.js';
import { QuickReplySet } from './QuickReplySet.js';
export class SlashCommandHandler { export class SlashCommandHandler {
/**@type {QuickReplyApi}*/ api; /**@type {QuickReplyApi}*/ api;
@ -19,6 +23,50 @@ export class SlashCommandHandler {
init() { init() {
function getExecutionIcons(/**@type {QuickReply} */ qr) {
let icons = '';
if (qr.preventAutoExecute) icons += '🚫';
if (qr.isHidden) icons += '👁️';
if (qr.executeOnStartup) icons += '🚀';
if (qr.executeOnUser) icons += enumIcons.user;
if (qr.executeOnAi) icons += enumIcons.assistant;
if (qr.executeOnChatChange) icons += '💬';
if (qr.executeOnGroupMemberDraft) icons += enumIcons.group;
return icons;
}
const localEnumProviders = {
/** All quick reply sets, optionally filtering out sets that wer already used in the "set" named argument */
qrSets: (executor) => QuickReplySet.list.filter(qrSet => qrSet.name != String(executor.namedArgumentList.find(x => x.name == 'set')?.value))
.map(qrSet => new SlashCommandEnumValue(qrSet.name, null, enumTypes.enum, 'S')),
/** All QRs inside a set, utilizing the "set" named argument */
qrEntries: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => {
const icons = getExecutionIcons(qr);
const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim();
return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);
}) ?? [],
/** All QRs as a set.name string, to be able to execute, for example via the /run command */
qrExecutables: () => {
const globalSetList = this.api.settings.config.setList;
const chatSetList = this.api.settings.chatConfig?.setList;
const globalQrs = globalSetList.map(link => link.set.qrList.map(qr => ({ set: link.set, qr }))).flat();
const chatQrs = chatSetList?.map(link => link.set.qrList.map(qr => ({ set: link.set, qr }))).flat() ?? [];
const otherQrs = QuickReplySet.list.filter(set => !globalSetList.some(link => link.set.name === set.name && !chatSetList?.some(link => link.set.name === set.name)))
.map(set => set.qrList.map(qr => ({ set, qr }))).flat();
return [
...globalQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `[global] ${x.qr.title || x.qr.message}`, enumTypes.name, enumIcons.qr)),
...chatQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `[chat] ${x.qr.title || x.qr.message}`, enumTypes.enum, enumIcons.qr)),
...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),
];
},
}
window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr',
callback: (_, value) => this.executeQuickReplyByIndex(Number(value)), callback: (_, value) => this.executeQuickReplyByIndex(Number(value)),
unnamedArgumentList: [ unnamedArgumentList: [
@ -29,110 +77,166 @@ export class SlashCommandHandler {
helpString: 'Activates the specified Quick Reply', helpString: 'Activates the specified Quick Reply',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qrset', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qrset',
callback: () => toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'), callback: () => {
toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.');
return '';
},
helpString: '<strong>DEPRECATED</strong> The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.', helpString: '<strong>DEPRECATED</strong> The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set',
callback: (args, value) => this.toggleGlobalSet(value, args), callback: (args, value) => {
this.toggleGlobalSet(value, args);
return '';
},
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
), ),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'QR set name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: 'Toggle global QR set', helpString: 'Toggle global QR set',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-on', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-on',
callback: (args, value) => this.addGlobalSet(value, args), callback: (args, value) => {
this.addGlobalSet(value, args);
return '';
},
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
), ),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'QR set name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: 'Activate global QR set', helpString: 'Activate global QR set',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-off', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-off',
callback: (_, value) => this.removeGlobalSet(value), callback: (_, value) => {
this.removeGlobalSet(value);
return '';
},
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'QR set name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: 'Deactivate global QR set', helpString: 'Deactivate global QR set',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set',
callback: (args, value) => this.toggleChatSet(value, args), callback: (args, value) => {
this.toggleChatSet(value, args);
return '';
},
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
), ),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'QR set name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: 'Toggle chat QR set', helpString: 'Toggle chat QR set',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-on', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-on',
callback: (args, value) => this.addChatSet(value, args), callback: (args, value) => {
this.addChatSet(value, args);
return '';
},
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ['true', 'false'], 'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
), ),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'QR set name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: 'Activate chat QR set', helpString: 'Activate chat QR set',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-off', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-off',
callback: (_, value) => this.removeChatSet(value), callback: (_, value) => {
this.removeChatSet(value);
return '';
},
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'QR set name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: 'Deactivate chat QR set', helpString: 'Deactivate chat QR set',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-list', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-list',
callback: (_, value) => this.listSets(value ?? 'all'), callback: (_, value) => JSON.stringify(this.listSets(value ?? 'all')),
returns: 'list of QR sets', returns: 'list of QR sets',
namedArgumentList: [], namedArgumentList: [],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(
'set type', [ARGUMENT_TYPE.STRING], false, false, null, ['all', 'global', 'chat'], 'set type', [ARGUMENT_TYPE.STRING], false, false, 'all', ['all', 'global', 'chat'],
), ),
], ],
helpString: 'Gets a list of the names of all quick reply sets.', helpString: 'Gets a list of the names of all quick reply sets.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-list', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-list',
callback: (_, value) => this.listQuickReplies(value), callback: (_, value) => {
return JSON.stringify(this.listQuickReplies(value));
},
returns: 'list of QRs', returns: 'list of QRs',
namedArgumentList: [], namedArgumentList: [],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'set name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: 'Gets a list of the names of all quick replies in this quick reply set.', helpString: 'Gets a list of the names of all quick replies in this quick reply set.',
})); }));
const qrArgs = [ const qrArgs = [
new SlashCommandNamedArgument('label', 'text on the button, e.g., label=MyButton', [ARGUMENT_TYPE.STRING]), SlashCommandNamedArgument.fromProps({
new SlashCommandNamedArgument('set', 'name of the QR set, e.g., set=PresetName1', [ARGUMENT_TYPE.STRING]), name: 'set',
description: 'name of the QR set, e.g., set=PresetName1',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
SlashCommandNamedArgument.fromProps({
name: 'label',
description: 'text on the button, e.g., label=MyButton',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrLabels,
}),
new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
@ -144,8 +248,12 @@ export class SlashCommandHandler {
const qrUpdateArgs = [ const qrUpdateArgs = [
new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false), new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
]; ];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
callback: (args, message) => this.createQuickReply(args, message), callback: (args, message) => {
this.createQuickReply(args, message);
return '';
},
namedArgumentList: qrArgs, namedArgumentList: qrArgs,
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(
@ -165,9 +273,15 @@ export class SlashCommandHandler {
`, `,
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
callback: (args, message) => this.updateQuickReply(args, message), callback: (args, message) => {
this.updateQuickReply(args, message);
return '';
},
returns: 'updated quick reply', returns: 'updated quick reply',
namedArgumentList: [...qrUpdateArgs, ...qrArgs], namedArgumentList: [...qrUpdateArgs, ...qrArgs],
unnamedArgumentList: [
new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),
],
helpString: ` helpString: `
<div> <div>
Updates Quick Reply. Updates Quick Reply.
@ -183,34 +297,57 @@ export class SlashCommandHandler {
`, `,
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-delete', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-delete',
callback: (args, name) => this.deleteQuickReply(args, name), callback: (args, name) => {
this.deleteQuickReply(args, name);
return '';
},
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( SlashCommandNamedArgument.fromProps({
'set', 'Quick Reply set', [ARGUMENT_TYPE.STRING], true, name: 'set',
), description: 'QR set name',
new SlashCommandNamedArgument( typeList: [ARGUMENT_TYPE.STRING],
'label', 'Quick Reply label', [ARGUMENT_TYPE.STRING], false, isRequired: true,
), enumProvider: localEnumProviders.qrSets,
}),
SlashCommandNamedArgument.fromProps({
name: 'label',
description: 'Quick Reply label',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries,
}),
], ],
helpString: 'Deletes a Quick Reply from the specified set. If no label is provided, the entire set is deleted.', helpString: 'Deletes a Quick Reply from the specified set. If no label is provided, the entire set is deleted.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextadd', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextadd',
callback: (args, name) => this.createContextItem(args, name), callback: (args, name) => {
this.createContextItem(args, name);
return '';
},
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( SlashCommandNamedArgument.fromProps({
'set', 'string', [ARGUMENT_TYPE.STRING], true, name: 'set',
), description: 'QR set name',
new SlashCommandNamedArgument( typeList: [ARGUMENT_TYPE.STRING],
'label', 'string', [ARGUMENT_TYPE.STRING], true, isRequired: true,
), enumProvider: localEnumProviders.qrSets,
}),
SlashCommandNamedArgument.fromProps({
name: 'label',
description: 'Quick Reply label',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries,
}),
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
), ),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'preset name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -227,19 +364,32 @@ export class SlashCommandHandler {
`, `,
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextdel', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextdel',
callback: (args, name) => this.deleteContextItem(args, name), callback: (args, name) => {
this.deleteContextItem(args, name);
return '';
},
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( SlashCommandNamedArgument.fromProps({
'set', 'string', [ARGUMENT_TYPE.STRING], true, name: 'set',
), description: 'QR set name',
new SlashCommandNamedArgument( typeList: [ARGUMENT_TYPE.STRING],
'label', 'string', [ARGUMENT_TYPE.STRING], true, isRequired: true,
), enumProvider: localEnumProviders.qrSets,
}),
SlashCommandNamedArgument.fromProps({
name: 'label',
description: 'Quick Reply label',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries,
}),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'preset name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -256,16 +406,25 @@ export class SlashCommandHandler {
`, `,
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextclear', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextclear',
callback: (args, label) => this.clearContextMenu(args, label), callback: (args, label) => {
this.clearContextMenu(args, label);
return '';
},
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( SlashCommandNamedArgument.fromProps({
'set', 'context menu preset name', [ARGUMENT_TYPE.STRING], true, name: 'set',
), description: 'QR set name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'label', [ARGUMENT_TYPE.STRING], true, description: 'Quick Reply label',
), typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries,
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -288,13 +447,20 @@ export class SlashCommandHandler {
new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false), new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
]; ];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
callback: (args, name) => this.createSet(name, args), callback: (args, name) => {
this.createSet(name, args);
return '';
},
aliases: ['qr-presetadd'], aliases: ['qr-presetadd'],
namedArgumentList: presetArgs, namedArgumentList: presetArgs,
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'name', [ARGUMENT_TYPE.STRING], true, description: 'QR set name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
forceEnum: false,
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -312,11 +478,19 @@ export class SlashCommandHandler {
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
callback: (args, name) => this.updateSet(name, args), callback: (args, name) => {
this.updateSet(name, args);
return '';
},
aliases: ['qr-presetupdate'], aliases: ['qr-presetupdate'],
namedArgumentList: presetArgs, namedArgumentList: presetArgs,
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true), SlashCommandArgument.fromProps({
description: 'QR set name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -329,10 +503,18 @@ export class SlashCommandHandler {
`, `,
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
callback: (args, name) => this.deleteSet(name), callback: (_, name) => {
this.deleteSet(name);
return '';
},
aliases: ['qr-presetdelete'], aliases: ['qr-presetdelete'],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true), SlashCommandArgument.fromProps({
description: 'QR set name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -468,7 +650,7 @@ export class SlashCommandHandler {
} }
deleteQuickReply(args, label) { deleteQuickReply(args, label) {
try { try {
this.api.deleteQuickReply(args.set, label); this.api.deleteQuickReply(args.set, args.label ?? label);
} catch (ex) { } catch (ex) {
toastr.error(ex.message); toastr.error(ex.message);
} }

View File

@ -148,7 +148,7 @@ export class SettingsUi {
this.onQrSetChange(); this.onQrSetChange();
} }
onQrSetChange() { onQrSetChange() {
this.currentQrSet = QuickReplySet.get(this.currentSet.value); this.currentQrSet = QuickReplySet.get(this.currentSet.value) ?? new QuickReplySet();
this.disableSend.checked = this.currentQrSet.disableSend; this.disableSend.checked = this.currentQrSet.disableSend;
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput; this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
this.injectInput.checked = this.currentQrSet.injectInput; this.injectInput.checked = this.currentQrSet.injectInput;

View File

@ -220,68 +220,68 @@
align-items: baseline; align-items: baseline;
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
flex-direction: column; flex-direction: column;
overflow: auto; overflow: auto;
} }
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main { body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
flex: 0 0 auto; flex: 0 0 auto;
} }
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels {
flex-direction: column; flex-direction: column;
} }
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder { body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
min-height: 50svh; min-height: 50svh;
height: 50svh; height: 50svh;
} }
} }
.dialogue_popup:has(#qr--modalEditor) { .popup:has(#qr--modalEditor) {
aspect-ratio: unset; aspect-ratio: unset;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text { .popup:has(#qr--modalEditor) .popup-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1em; gap: 1em;
overflow: hidden; overflow: hidden;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 0.5em; gap: 0.5em;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {
flex: 1 1 1px; flex: 1 1 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {
flex: 1 1 auto; flex: 1 1 auto;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {
flex: 1 1 auto; flex: 1 1 auto;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {
flex: 0 0 auto; flex: 0 0 auto;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1em; gap: 1em;
@ -289,19 +289,35 @@
font-size: smaller; font-size: smaller;
align-items: baseline; align-items: baseline;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label {
white-space: nowrap; white-space: nowrap;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input {
font-size: inherit; font-size: inherit;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
flex: 1 1 auto; flex: 1 1 auto;
display: grid; display: grid;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax { .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-messageSyntax {
display: none;
}
.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message {
background-color: var(--ac-style-color-background);
color: var(--ac-style-color-text);
}
.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
color: unset;
background-color: rgba(108 171 251 / 0.25);
}
@supports (color: rgb(from white r g b / 0.25)) {
.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
}
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax {
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
padding: 0; padding: 0;
@ -311,22 +327,34 @@
min-width: 100%; min-width: 100%;
width: 0; width: 0;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax > #qr--modal-messageSyntaxInner { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax > #qr--modal-messageSyntaxInner {
height: 100%; height: 100%;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
background-color: transparent;
color: transparent;
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
caret-color: white; caret-color: var(--ac-style-color-text);
mix-blend-mode: difference; overflow: auto;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar-thumb { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar-thumb {
visibility: hidden; visibility: hidden;
cursor: default; cursor: default;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-message, .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection {
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner { color: transparent;
background-color: rgba(108 171 251 / 0.25);
}
@supports (color: rgb(from white r g b / 0.25)) {
.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
}
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-message,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner {
font-family: var(--monoFontFamily);
padding: 0.75em; padding: 0.75em;
margin: 0; margin: 0;
border: none; border: none;
@ -335,11 +363,11 @@
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px; border-radius: 5px;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {
display: flex; display: flex;
gap: 1em; gap: 1em;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton {
border-width: 2px; border-width: 2px;
border-style: solid; border-style: solid;
display: flex; display: flex;
@ -347,42 +375,42 @@
gap: 0.5em; gap: 0.5em;
padding: 0.5em 0.75em; padding: 0.5em 0.75em;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton .qr--modal-executeComboIcon { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton .qr--modal-executeComboIcon {
display: flex; display: flex;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
transition: 200ms; transition: 200ms;
filter: grayscale(0); filter: grayscale(0);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute.qr--busy { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute.qr--busy {
cursor: wait; cursor: wait;
opacity: 0.5; opacity: 0.5;
filter: grayscale(1); filter: grayscale(1);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
border-color: #51a351; border-color: #51a351;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
cursor: default; cursor: default;
opacity: 0.5; opacity: 0.5;
filter: grayscale(1); filter: grayscale(1);
pointer-events: none; pointer-events: none;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-pause, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-pause,
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop {
cursor: pointer; cursor: pointer;
opacity: 1; opacity: 1;
filter: grayscale(0); filter: grayscale(0);
pointer-events: all; pointer-events: all;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause {
border-color: #92befc; border-color: #92befc;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
border-color: #d78872; border-color: #d78872;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {
--prog: 0; --prog: 0;
--progColor: #92befc; --progColor: #92befc;
--progFlashColor: #d78872; --progFlashColor: #d78872;
@ -393,7 +421,7 @@
background-color: var(--black50a); background-color: var(--black50a);
position: relative; position: relative;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress:after {
content: ''; content: '';
background-color: var(--progColor); background-color: var(--progColor);
position: absolute; position: absolute;
@ -401,23 +429,23 @@
right: calc(100% - var(--prog) * 1%); right: calc(100% - var(--prog) * 1%);
transition: 200ms; transition: 200ms;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--paused:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress.qr--paused:after {
animation-name: qr--progressPulse; animation-name: qr--progressPulse;
animation-duration: 1500ms; animation-duration: 1500ms;
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
animation-delay: 0s; animation-delay: 0s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--aborted:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress.qr--aborted:after {
background-color: var(--progAbortedColor); background-color: var(--progAbortedColor);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--success:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress.qr--success:after {
background-color: var(--progSuccessColor); background-color: var(--progSuccessColor);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--error:after { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress.qr--error:after {
background-color: var(--progErrorColor); background-color: var(--progErrorColor);
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeErrors {
display: none; display: none;
text-align: left; text-align: left;
font-size: smaller; font-size: smaller;
@ -428,10 +456,10 @@
min-width: 100%; min-width: 100%;
width: 0; width: 0;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors {
display: block; display: block;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult {
display: none; display: none;
text-align: left; text-align: left;
font-size: smaller; font-size: smaller;
@ -442,10 +470,10 @@
min-width: 100%; min-width: 100%;
width: 0; width: 0;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult.qr--hasResult { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
display: block; display: block;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult:before { .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {
content: 'Result: '; content: 'Result: ';
} }
@keyframes qr--progressPulse { @keyframes qr--progressPulse {
@ -457,6 +485,10 @@
background-color: var(--progFlashColor); background-color: var(--progFlashColor);
} }
} }
.shadow_popup.qr--hide {
.popup.qr--hide {
opacity: 0 !important;
}
.popup.qr--hide::backdrop {
opacity: 0 !important; opacity: 0 !important;
} }

View File

@ -244,7 +244,7 @@
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { body .popup:has(#qr--modalEditor) .popup-content>#qr--modalEditor {
flex-direction: column; flex-direction: column;
overflow: auto; overflow: auto;
> #qr--main { > #qr--main {
@ -259,10 +259,10 @@
} }
} }
} }
.dialogue_popup:has(#qr--modalEditor) { .popup:has(#qr--modalEditor) {
aspect-ratio: unset; aspect-ratio: unset;
.dialogue_popup_text { .popup-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -322,6 +322,22 @@
display: grid; display: grid;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
&.qr--noSyntax {
> #qr--modal-messageSyntax {
display: none;
}
> #qr--modal-message {
background-color: var(--ac-style-color-background);
color: var(--ac-style-color-text);
&::selection {
color: unset;
background-color: rgba(108 171 251 / 0.25);
@supports (color: rgb(from white r g b / 0.25)) {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
}
}
}
}
> #qr--modal-messageSyntax { > #qr--modal-messageSyntax {
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
@ -336,16 +352,26 @@
} }
} }
> #qr--modal-message { > #qr--modal-message {
background-color: transparent;
color: transparent;
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
caret-color: white; caret-color: var(--ac-style-color-text);
mix-blend-mode: difference; overflow: auto;
&::-webkit-scrollbar, &::-webkit-scrollbar-thumb { &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
visibility: hidden; visibility: hidden;
cursor: default; cursor: default;
} }
&::selection {
color: transparent;
background-color: rgba(108 171 251 / 0.25);
@supports (color: rgb(from white r g b / 0.25)) {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
}
}
} }
#qr--modal-message, #qr--modal-messageSyntaxInner { #qr--modal-message, #qr--modal-messageSyntaxInner {
font-family: var(--monoFontFamily);
padding: 0.75em; padding: 0.75em;
margin: 0; margin: 0;
border: none; border: none;
@ -481,6 +507,6 @@
} }
} }
.shadow_popup.qr--hide { .popup.qr--hide {
opacity: 0 !important; opacity: 0 !important;
} }

View File

@ -8,11 +8,11 @@
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<div class="flex-container"> <div class="flex-container">
<div id="open_regex_editor" class="menu_button menu_button_icon" title="New global regex script"> <div id="open_regex_editor" class="menu_button menu_button_icon" data-i18n="[title]ext_regex_new_global_script_desc" title="New global regex script">
<i class="fa-solid fa-pen-to-square"></i> <i class="fa-solid fa-pen-to-square"></i>
<small data-i18n="ext_regex_new_global_script">+ Global</small> <small data-i18n="ext_regex_new_global_script">+ Global</small>
</div> </div>
<div id="open_scoped_editor" class="menu_button menu_button_icon" title="New scoped regex script"> <div id="open_scoped_editor" class="menu_button menu_button_icon" data-i18n="[title]ext_regex_new_scoped_script_desc" title="New scoped regex script">
<i class="fa-solid fa-address-card"></i> <i class="fa-solid fa-address-card"></i>
<small data-i18n="ext_regex_new_scoped_script">+ Scoped</small> <small data-i18n="ext_regex_new_scoped_script">+ Scoped</small>
</div> </div>
@ -39,7 +39,7 @@
<label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle"> <label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle">
<input type="checkbox" id="regex_scoped_toggle" class="enable_scoped" /> <input type="checkbox" id="regex_scoped_toggle" class="enable_scoped" />
<span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" title="Disallow using scoped regex"></span> <span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" title="Disallow using scoped regex"></span>
<span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" title="Allow using scoped regex"></span> <span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" data-i18n="[title]ext_regex_allow_scoped" title="Allow using scoped regex"></span>
</label> </label>
</div> </div>
<small data-i18n="ext_regex_scoped_scripts_desc"> <small data-i18n="ext_regex_scoped_scripts_desc">

View File

@ -92,7 +92,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
/** /**
* Runs the provided regex script on the given string * Runs the provided regex script on the given string
* @param {object} regexScript The regex script to run * @param {import('./index.js').RegexScript} regexScript The regex script to run
* @param {string} rawString The string to run the regex script on * @param {string} rawString The string to run the regex script on
* @param {RegexScriptParams} params The parameters to use for the regex script * @param {RegexScriptParams} params The parameters to use for the regex script
* @returns {string} The new string * @returns {string} The new string

View File

@ -3,11 +3,32 @@ import { extension_settings, renderExtensionTemplateAsync, writeExtensionField }
import { selected_group } from '../../group-chats.js'; import { selected_group } from '../../group-chats.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js'; import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
import { resolveVariable } from '../../variables.js'; import { resolveVariable } from '../../variables.js';
import { regex_placement, runRegexScript } from './engine.js'; import { regex_placement, runRegexScript } from './engine.js';
/**
* @typedef {object} RegexScript
* @property {string} scriptName - The name of the script
* @property {boolean} disabled - Whether the script is disabled
* @property {string} replaceString - The replace string
* @property {string[]} trimStrings - The trim strings
* @property {string?} findRegex - The find regex
* @property {string?} substituteRegex - The substitute regex
*/
/**
* Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data
*
* @return {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information.
*/
export function getRegexScripts() {
return [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])];
}
/** /**
* Saves a regex script to the extension settings or character data. * Saves a regex script to the extension settings or character data.
* @param {import('../../char-data.js').RegexScriptData} regexScript * @param {import('../../char-data.js').RegexScriptData} regexScript
@ -339,7 +360,7 @@ function runRegexCallback(args, value) {
} }
const scriptName = String(resolveVariable(args.name)); const scriptName = String(resolveVariable(args.name));
const scripts = [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])]; const scripts = getRegexScripts();
for (const script of scripts) { for (const script of scripts) {
if (String(script.scriptName).toLowerCase() === String(scriptName).toLowerCase()) { if (String(script.scriptName).toLowerCase() === String(scriptName).toLowerCase()) {
@ -551,14 +572,26 @@ jQuery(async () => {
await loadRegexScripts(); await loadRegexScripts();
$('#saved_regex_scripts').sortable('enable'); $('#saved_regex_scripts').sortable('enable');
const localEnumProviders = {
regexScripts: () => getRegexScripts().map(script => {
const isGlobal = extension_settings.regex?.some(x => x.scriptName === script.scriptName);
return new SlashCommandEnumValue(script.scriptName, `${enumIcons.getStateIcon(!script.disabled)} [${isGlobal ? 'global' : 'scoped'}] ${script.findRegex}`,
isGlobal ? enumTypes.enum : enumTypes.name, isGlobal ? 'G' : 'S');
}),
};
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'regex', name: 'regex',
callback: runRegexCallback, callback: runRegexCallback,
returns: 'replaced text', returns: 'replaced text',
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( SlashCommandNamedArgument.fromProps({
'name', 'script name', [ARGUMENT_TYPE.STRING], true, name: 'name',
), description: 'script name',
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: localEnumProviders.regexScripts,
}),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(

View File

@ -17,6 +17,7 @@ import {
getCharacterAvatar, getCharacterAvatar,
formatCharacterAvatar, formatCharacterAvatar,
substituteParams, substituteParams,
substituteParamsExtended,
} from '../../../script.js'; } from '../../../script.js';
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js'; import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
import { selected_group } from '../../group-chats.js'; import { selected_group } from '../../group-chats.js';
@ -30,6 +31,8 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { resolveVariable } from '../../variables.js'; import { resolveVariable } from '../../variables.js';
import { debounce_timeout } from '../../constants.js'; import { debounce_timeout } from '../../constants.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'sd'; const MODULE_NAME = 'sd';
@ -50,7 +53,15 @@ const sources = {
pollinations: 'pollinations', pollinations: 'pollinations',
}; };
const initiators = {
command: 'command',
action: 'action',
interactive: 'interactive',
wand: 'wand',
};
const generationMode = { const generationMode = {
MESSAGE: -1,
CHARACTER: 0, CHARACTER: 0,
USER: 1, USER: 1,
SCENARIO: 2, SCENARIO: 2,
@ -62,6 +73,7 @@ const generationMode = {
CHARACTER_MULTIMODAL: 8, CHARACTER_MULTIMODAL: 8,
USER_MULTIMODAL: 9, USER_MULTIMODAL: 9,
FACE_MULTIMODAL: 10, FACE_MULTIMODAL: 10,
FREE_EXTENDED: 11,
}; };
const multimodalMap = { const multimodalMap = {
@ -71,6 +83,7 @@ const multimodalMap = {
}; };
const modeLabels = { const modeLabels = {
[generationMode.MESSAGE]: 'Chat Message Template',
[generationMode.CHARACTER]: 'Character ("Yourself")', [generationMode.CHARACTER]: 'Character ("Yourself")',
[generationMode.FACE]: 'Portrait ("Your Face")', [generationMode.FACE]: 'Portrait ("Your Face")',
[generationMode.USER]: 'User ("Me")', [generationMode.USER]: 'User ("Me")',
@ -81,6 +94,7 @@ const modeLabels = {
[generationMode.CHARACTER_MULTIMODAL]: 'Character (Multimodal Mode)', [generationMode.CHARACTER_MULTIMODAL]: 'Character (Multimodal Mode)',
[generationMode.FACE_MULTIMODAL]: 'Portrait (Multimodal Mode)', [generationMode.FACE_MULTIMODAL]: 'Portrait (Multimodal Mode)',
[generationMode.USER_MULTIMODAL]: 'User (Multimodal Mode)', [generationMode.USER_MULTIMODAL]: 'User (Multimodal Mode)',
[generationMode.FREE_EXTENDED]: 'Free Mode (LLM-Extended)',
}; };
const triggerWords = { const triggerWords = {
@ -94,7 +108,7 @@ const triggerWords = {
}; };
const messageTrigger = { const messageTrigger = {
activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render)\b.{0,10}\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the|this|that|those)?)?(.+)/i, activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render|show)\b.{0,10}\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the|this|that|those|your)?)?(.+)/i,
specialCases: { specialCases: {
[generationMode.CHARACTER]: ['you', 'yourself'], [generationMode.CHARACTER]: ['you', 'yourself'],
[generationMode.USER]: ['me', 'myself'], [generationMode.USER]: ['me', 'myself'],
@ -106,6 +120,8 @@ const messageTrigger = {
}; };
const promptTemplates = { const promptTemplates = {
// Not really a prompt template, rather an outcome message template
[generationMode.MESSAGE]: '[{{char}} sends a picture that contains: {{prompt}}].',
/*OLD: [generationMode.CHARACTER]: "Pause your roleplay and provide comma-delimited list of phrases and keywords which describe {{char}}'s physical appearance and clothing. Ignore {{char}}'s personality traits, and chat history when crafting this description. End your response once the comma-delimited list is complete. Do not roleplay when writing this description, and do not attempt to continue the story.", */ /*OLD: [generationMode.CHARACTER]: "Pause your roleplay and provide comma-delimited list of phrases and keywords which describe {{char}}'s physical appearance and clothing. Ignore {{char}}'s personality traits, and chat history when crafting this description. End your response once the comma-delimited list is complete. Do not roleplay when writing this description, and do not attempt to continue the story.", */
[generationMode.CHARACTER]: '[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\']', [generationMode.CHARACTER]: '[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\']',
//face-specific prompt //face-specific prompt
@ -143,6 +159,7 @@ const promptTemplates = {
[generationMode.FACE_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "close-up portrait".', [generationMode.FACE_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "close-up portrait".',
[generationMode.CHARACTER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".', [generationMode.CHARACTER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".',
[generationMode.USER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".', [generationMode.USER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".',
[generationMode.FREE_EXTENDED]: 'Pause your roleplay and provide an exhaustive comma-separated list of tags describing the appearance of "{0}" in great detail. Start with {{charPrefix}} (sic) if the subject is associated with {{char}}.',
}; };
const defaultPrefix = 'best quality, absurdres, aesthetic,'; const defaultPrefix = 'best quality, absurdres, aesthetic,';
@ -204,6 +221,7 @@ const defaultSettings = {
interactive_mode: false, interactive_mode: false,
multimodal_captioning: false, multimodal_captioning: false,
snap: false, snap: false,
free_extend: false,
prompts: promptTemplates, prompts: promptTemplates,
@ -261,6 +279,11 @@ const defaultSettings = {
// Pollinations settings // Pollinations settings
pollinations_enhance: false, pollinations_enhance: false,
pollinations_refine: false, pollinations_refine: false,
// Visibility toggles
wand_visible: false,
command_visible: false,
interactive_visible: false,
}; };
const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
@ -312,7 +335,7 @@ function processTriggers(chat, _, abort) {
} }
abort(true); abort(true);
setTimeout(() => generatePicture('sd', subject, message), 1); setTimeout(() => generatePicture(initiators.interactive, {}, subject, message), 1);
} catch { } catch {
console.log('SD: Failed to process triggers.'); console.log('SD: Failed to process triggers.');
return; return;
@ -417,8 +440,12 @@ async function loadSettings() {
$('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt); $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
$('#sd_snap').prop('checked', extension_settings.sd.snap); $('#sd_snap').prop('checked', extension_settings.sd.snap);
$('#sd_clip_skip').val(extension_settings.sd.clip_skip); $('#sd_clip_skip').val(extension_settings.sd.clip_skip);
$('#sd_clip_skip_value').text(extension_settings.sd.clip_skip); $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);
$('#sd_seed').val(extension_settings.sd.seed); $('#sd_seed').val(extension_settings.sd.seed);
$('#sd_free_extend').prop('checked', extension_settings.sd.free_extend);
$('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible);
$('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
$('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
for (const style of extension_settings.sd.styles) { for (const style of extension_settings.sd.styles) {
const option = document.createElement('option'); const option = document.createElement('option');
@ -476,7 +503,7 @@ async function loadSettingOptions() {
function addPromptTemplates() { function addPromptTemplates() {
$('#sd_prompt_templates').empty(); $('#sd_prompt_templates').empty();
for (const [name, prompt] of Object.entries(extension_settings.sd.prompts)) { for (const [name, prompt] of Object.entries(extension_settings.sd.prompts).sort((a, b) => Number(a[0]) - Number(b[0]))) {
const label = $('<label></label>') const label = $('<label></label>')
.text(modeLabels[name]) .text(modeLabels[name])
.attr('for', `sd_prompt_${name}`) .attr('for', `sd_prompt_${name}`)
@ -683,6 +710,18 @@ function onChatChanged() {
$('#sd_character_prompt').val(characterPrompt); $('#sd_character_prompt').val(characterPrompt);
$('#sd_character_negative_prompt').val(negativePrompt); $('#sd_character_negative_prompt').val(negativePrompt);
$('#sd_character_prompt_share').prop('checked', hasSharedData); $('#sd_character_prompt_share').prop('checked', hasSharedData);
adjustElementScrollHeight();
}
function adjustElementScrollHeight() {
if (!$('.sd_settings').is(':visible')) {
return;
}
resetScrollHeight($('#sd_prompt_prefix'));
resetScrollHeight($('#sd_negative_prompt'));
resetScrollHeight($('#sd_character_prompt'));
resetScrollHeight($('#sd_character_negative_prompt'));
} }
function onCharacterPromptInput() { function onCharacterPromptInput() {
@ -762,9 +801,29 @@ function onRefineModeInput() {
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onFreeExtendInput() {
extension_settings.sd.free_extend = !!$('#sd_free_extend').prop('checked');
saveSettingsDebounced();
}
function onWandVisibleInput() {
extension_settings.sd.wand_visible = !!$('#sd_wand_visible').prop('checked');
saveSettingsDebounced();
}
function onCommandVisibleInput() {
extension_settings.sd.command_visible = !!$('#sd_command_visible').prop('checked');
saveSettingsDebounced();
}
function onInteractiveVisibleInput() {
extension_settings.sd.interactive_visible = !!$('#sd_interactive_visible').prop('checked');
saveSettingsDebounced();
}
function onClipSkipInput() { function onClipSkipInput() {
extension_settings.sd.clip_skip = Number($('#sd_clip_skip').val()); extension_settings.sd.clip_skip = Number($('#sd_clip_skip').val());
$('#sd_clip_skip_value').text(extension_settings.sd.clip_skip); $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -775,13 +834,13 @@ function onSeedInput() {
function onScaleInput() { function onScaleInput() {
extension_settings.sd.scale = Number($('#sd_scale').val()); extension_settings.sd.scale = Number($('#sd_scale').val());
$('#sd_scale_value').text(extension_settings.sd.scale.toFixed(1)); $('#sd_scale_value').val(extension_settings.sd.scale.toFixed(1));
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onStepsInput() { function onStepsInput() {
extension_settings.sd.steps = Number($('#sd_steps').val()); extension_settings.sd.steps = Number($('#sd_steps').val());
$('#sd_steps_value').text(extension_settings.sd.steps); $('#sd_steps_value').val(extension_settings.sd.steps);
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -844,13 +903,23 @@ function onSchedulerChange() {
function onWidthInput() { function onWidthInput() {
extension_settings.sd.width = Number($('#sd_width').val()); extension_settings.sd.width = Number($('#sd_width').val());
$('#sd_width_value').text(extension_settings.sd.width); $('#sd_width_value').val(extension_settings.sd.width);
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onHeightInput() { function onHeightInput() {
extension_settings.sd.height = Number($('#sd_height').val()); extension_settings.sd.height = Number($('#sd_height').val());
$('#sd_height_value').text(extension_settings.sd.height); $('#sd_height_value').val(extension_settings.sd.height);
saveSettingsDebounced();
}
function onSwapDimensionsClick() {
const w = extension_settings.sd.height;
const h = extension_settings.sd.width;
extension_settings.sd.width = w;
extension_settings.sd.height = h;
$('#sd_width').val(w).trigger('input');
$('#sd_height').val(h).trigger('input');
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -890,7 +959,7 @@ async function onViewAnlasClick() {
function onNovelUpscaleRatioInput() { function onNovelUpscaleRatioInput() {
extension_settings.sd.novel_upscale_ratio = Number($('#sd_novel_upscale_ratio').val()); extension_settings.sd.novel_upscale_ratio = Number($('#sd_novel_upscale_ratio').val());
$('#sd_novel_upscale_ratio_value').text(extension_settings.sd.novel_upscale_ratio.toFixed(1)); $('#sd_novel_upscale_ratio_value').val(extension_settings.sd.novel_upscale_ratio.toFixed(1));
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -992,19 +1061,19 @@ function onHrUpscalerChange() {
function onHrScaleInput() { function onHrScaleInput() {
extension_settings.sd.hr_scale = Number($('#sd_hr_scale').val()); extension_settings.sd.hr_scale = Number($('#sd_hr_scale').val());
$('#sd_hr_scale_value').text(extension_settings.sd.hr_scale.toFixed(1)); $('#sd_hr_scale_value').val(extension_settings.sd.hr_scale.toFixed(1));
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onDenoisingStrengthInput() { function onDenoisingStrengthInput() {
extension_settings.sd.denoising_strength = Number($('#sd_denoising_strength').val()); extension_settings.sd.denoising_strength = Number($('#sd_denoising_strength').val());
$('#sd_denoising_strength_value').text(extension_settings.sd.denoising_strength.toFixed(2)); $('#sd_denoising_strength_value').val(extension_settings.sd.denoising_strength.toFixed(2));
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onHrSecondPassStepsInput() { function onHrSecondPassStepsInput() {
extension_settings.sd.hr_second_pass_steps = Number($('#sd_hr_second_pass_steps').val()); extension_settings.sd.hr_second_pass_steps = Number($('#sd_hr_second_pass_steps').val());
$('#sd_hr_second_pass_steps_value').text(extension_settings.sd.hr_second_pass_steps); $('#sd_hr_second_pass_steps_value').val(extension_settings.sd.hr_second_pass_steps);
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -1026,6 +1095,7 @@ async function changeComfyWorkflow(_, name) {
} else { } else {
toastr.error(`ComfyUI Workflow "${name}" does not exist.`); toastr.error(`ComfyUI Workflow "${name}" does not exist.`);
} }
return '';
} }
async function validateAutoUrl() { async function validateAutoUrl() {
@ -2016,6 +2086,10 @@ function getGenerationType(prompt) {
mode = multimodalMap[mode]; mode = multimodalMap[mode];
} }
if (mode === generationMode.FREE && extension_settings.sd.free_extend) {
mode = generationMode.FREE_EXTENDED;
}
return mode; return mode;
} }
@ -2080,7 +2154,16 @@ function getRawLastMessage() {
return `((${processReply(lastMessage.mes)})), (${processReply(character.scenario)}:0.7), (${processReply(character.description)}:0.5)`; return `((${processReply(lastMessage.mes)})), (${processReply(character.scenario)}:0.7), (${processReply(character.description)}:0.5)`;
} }
async function generatePicture(args, trigger, message, callback) { /**
* Generates an image based on the given trigger word.
* @param {string} initiator The initiator of the image generation
* @param {Record<string, object>} args Command arguments
* @param {string} trigger Subject trigger word
* @param {string} [message] Chat message
* @param {function} [callback] Callback function
* @returns {Promise<string>} Image path
*/
async function generatePicture(initiator, args, trigger, message, callback) {
if (!trigger || trigger.trim().length === 0) { if (!trigger || trigger.trim().length === 0) {
console.log('Trigger word empty, aborting'); console.log('Trigger word empty, aborting');
return; return;
@ -2111,9 +2194,9 @@ async function generatePicture(args, trigger, message, callback) {
eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath }); eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
if (typeof callbackOriginal === 'function') { if (typeof callbackOriginal === 'function') {
callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix); callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator);
} else { } else {
sendMessage(prompt, imagePath, generationType, negativePromptPrefix); sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator);
} }
}; };
} }
@ -2122,18 +2205,19 @@ async function generatePicture(args, trigger, message, callback) {
callback = () => { }; callback = () => { };
} }
const negativePromptPrefix = resolveVariable(args?.negative) || '';
const dimensions = setTypeSpecificDimensions(generationType); const dimensions = setTypeSpecificDimensions(generationType);
let negativePromptPrefix = resolveVariable(args?.negative) || '';
let imagePath = ''; let imagePath = '';
try { try {
const prompt = await getPrompt(generationType, message, trigger, quietPrompt); const combineNegatives = (prefix) => { negativePromptPrefix = combinePrefixes(negativePromptPrefix, prefix); };
const prompt = await getPrompt(generationType, message, trigger, quietPrompt, combineNegatives);
console.log('Processed image prompt:', prompt); console.log('Processed image prompt:', prompt);
context.deactivateSendButtons(); context.deactivateSendButtons();
hideSwipeButtons(); hideSwipeButtons();
imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback); imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator);
} catch (err) { } catch (err) {
console.trace(err); console.trace(err);
throw new Error('SD prompt text generation failed.'); throw new Error('SD prompt text generation failed.');
@ -2196,7 +2280,16 @@ function restoreOriginalDimensions(savedParams) {
extension_settings.sd.width = savedParams.width; extension_settings.sd.width = savedParams.width;
} }
async function getPrompt(generationType, message, trigger, quietPrompt) { /**
* Generates a prompt for image generation.
* @param {number} generationType The type of image generation to perform.
* @param {string} message A message text to use for the image generation.
* @param {string} trigger A trigger string to use for the image generation.
* @param {string} quietPrompt A quiet prompt to use for the image generation.
* @param {function} combineNegatives A function that combines the negative prompt with other prompts.
* @returns {Promise<string>} - A promise that resolves when the prompt generation completes.
*/
async function getPrompt(generationType, message, trigger, quietPrompt, combineNegatives) {
let prompt; let prompt;
switch (generationType) { switch (generationType) {
@ -2204,7 +2297,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
prompt = message || getRawLastMessage(); prompt = message || getRawLastMessage();
break; break;
case generationMode.FREE: case generationMode.FREE:
prompt = generateFreeModePrompt(trigger.trim()); prompt = generateFreeModePrompt(trigger.trim(), combineNegatives);
break; break;
case generationMode.FACE_MULTIMODAL: case generationMode.FACE_MULTIMODAL:
case generationMode.CHARACTER_MULTIMODAL: case generationMode.CHARACTER_MULTIMODAL:
@ -2216,6 +2309,10 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
break; break;
} }
if (generationType === generationMode.FREE_EXTENDED) {
prompt = generateFreeModePrompt(prompt.trim(), combineNegatives);
}
if (generationType !== generationMode.FREE) { if (generationType !== generationMode.FREE) {
prompt = await refinePrompt(prompt, true); prompt = await refinePrompt(prompt, true);
} }
@ -2226,9 +2323,10 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
/** /**
* Generates a free prompt with a character-specific prompt prefix support. * Generates a free prompt with a character-specific prompt prefix support.
* @param {string} trigger - The prompt to use for the image generation. * @param {string} trigger - The prompt to use for the image generation.
* @param {function} combineNegatives - A function that combines the negative prompt with other prompts.
* @returns {string} * @returns {string}
*/ */
function generateFreeModePrompt(trigger) { function generateFreeModePrompt(trigger, combineNegatives) {
return trigger return trigger
.replace(/(?:^char(\s|,)|\{\{charPrefix\}\})/gi, (_, suffix) => { .replace(/(?:^char(\s|,)|\{\{charPrefix\}\})/gi, (_, suffix) => {
const getLastCharacterKey = () => { const getLastCharacterKey = () => {
@ -2249,7 +2347,9 @@ function generateFreeModePrompt(trigger) {
const key = getLastCharacterKey(); const key = getLastCharacterKey();
const value = (extension_settings.sd.character_prompts[key] || '').trim(); const value = (extension_settings.sd.character_prompts[key] || '').trim();
return value ? value + (suffix || '') : ''; const negativeValue = (extension_settings.sd.character_negative_prompts[key] || '').trim();
typeof combineNegatives === 'function' && negativeValue ? combineNegatives(negativeValue) : void 0;
return value ? combinePrefixes(value, (suffix || '')) : '';
}); });
} }
@ -2335,12 +2435,13 @@ async function generatePrompt(quietPrompt) {
* @param {number} generationType Type of image generation * @param {number} generationType Type of image generation
* @param {string} prompt Prompt to be used for image generation * @param {string} prompt Prompt to be used for image generation
* @param {string} additionalNegativePrefix Additional negative prompt to be used for image generation * @param {string} additionalNegativePrefix Additional negative prompt to be used for image generation
* @param {string} [characterName] Name of the character * @param {string} characterName Name of the character
* @param {function} [callback] Callback function to be called after image generation * @param {function} callback Callback function to be called after image generation
* @param {string} initiator The initiator of the image generation
* @returns * @returns
*/ */
async function sendGenerationRequest(generationType, prompt, additionalNegativePrefix, characterName = null, callback) { async function sendGenerationRequest(generationType, prompt, additionalNegativePrefix, characterName, callback, initiator) {
const noCharPrefix = [generationMode.FREE, generationMode.BACKGROUND, generationMode.USER, generationMode.USER_MULTIMODAL]; const noCharPrefix = [generationMode.FREE, generationMode.BACKGROUND, generationMode.USER, generationMode.USER_MULTIMODAL, generationMode.FREE_EXTENDED];
const prefix = noCharPrefix.includes(generationType) const prefix = noCharPrefix.includes(generationType)
? extension_settings.sd.prompt_prefix ? extension_settings.sd.prompt_prefix
: combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix()); : combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix());
@ -2405,7 +2506,7 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
const filename = `${characterName}_${humanizedDateTime()}`; const filename = `${characterName}_${humanizedDateTime()}`;
const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format); const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
callback ? callback(prompt, base64Image, generationType, additionalNegativePrefix) : sendMessage(prompt, base64Image, generationType, additionalNegativePrefix); callback ? callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator) : sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator);
return base64Image; return base64Image;
} }
@ -3009,15 +3110,17 @@ async function onComfyDeleteWorkflowClick() {
* @param {string} image Base64 encoded image * @param {string} image Base64 encoded image
* @param {number} generationType Generation type of the image * @param {number} generationType Generation type of the image
* @param {string} additionalNegativePrefix Additional negative prompt used for the image generation * @param {string} additionalNegativePrefix Additional negative prompt used for the image generation
* @param {string} initiator The initiator of the image generation
*/ */
async function sendMessage(prompt, image, generationType, additionalNegativePrefix) { async function sendMessage(prompt, image, generationType, additionalNegativePrefix, initiator) {
const context = getContext(); const context = getContext();
const name = context.groupId ? systemUserName : context.name2; const name = context.groupId ? systemUserName : context.name2;
const messageText = `[${name} sends a picture that contains: ${prompt}]`; const template = extension_settings.sd.prompts[generationMode.MESSAGE] || '{{prompt}}';
const messageText = substituteParamsExtended(template, { char: name, prompt: prompt });
const message = { const message = {
name: name, name: name,
is_user: false, is_user: false,
is_system: true, is_system: !getVisibilityByInitiator(initiator),
send_date: getMessageTimeStamp(), send_date: getMessageTimeStamp(),
mes: messageText, mes: messageText,
extra: { extra: {
@ -3033,6 +3136,24 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
context.saveChat(); context.saveChat();
} }
/**
* Gets the visibility of the resulting message based on the initiator.
* @param {string} initiator Generation initiator
* @returns {boolean} Is resulting message visible
*/
function getVisibilityByInitiator(initiator) {
switch (initiator) {
case initiators.interactive:
return !!extension_settings.sd.interactive_visible;
case initiators.wand:
return !!extension_settings.sd.wand_visible;
case initiators.command:
return !!extension_settings.sd.command_visible;
default:
return false;
}
}
async function addSDGenButtons() { async function addSDGenButtons() {
const buttonHtml = await renderExtensionTemplateAsync('stable-diffusion', 'button'); const buttonHtml = await renderExtensionTemplateAsync('stable-diffusion', 'button');
const dropdownHtml = await renderExtensionTemplateAsync('stable-diffusion', 'dropdown'); const dropdownHtml = await renderExtensionTemplateAsync('stable-diffusion', 'dropdown');
@ -3082,7 +3203,7 @@ async function addSDGenButtons() {
if (param) { if (param) {
console.log('doing /sd ' + param); console.log('doing /sd ' + param);
generatePicture('sd', param); generatePicture(initiators.wand, {}, param);
} }
}); });
} }
@ -3159,11 +3280,11 @@ async function sdMessageButton(e) {
const generationType = message?.extra?.generationType ?? generationMode.FREE; const generationType = message?.extra?.generationType ?? generationMode.FREE;
console.log('Regenerating an image, using existing prompt:', prompt); console.log('Regenerating an image, using existing prompt:', prompt);
dimensions = setTypeSpecificDimensions(generationType); dimensions = setTypeSpecificDimensions(generationType);
await sendGenerationRequest(generationType, prompt, negative, characterFileName, saveGeneratedImage); await sendGenerationRequest(generationType, prompt, negative, characterFileName, saveGeneratedImage, initiators.action);
} }
else { else {
console.log('doing /sd raw last'); console.log('doing /sd raw last');
await generatePicture('sd', 'raw_last', messageText, saveGeneratedImage); await generatePicture(initiators.action, {}, 'raw_last', messageText, saveGeneratedImage);
} }
} }
catch (error) { catch (error) {
@ -3226,15 +3347,18 @@ jQuery(async () => {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine', name: 'imagine',
callback: generatePicture, callback: (args, trigger) => generatePicture(initiators.command, args, String(trigger)),
aliases: ['sd', 'img', 'image'], aliases: ['sd', 'img', 'image'],
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'quiet', 'whether to post the generated image to chat', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['false', 'true'], 'quiet', 'whether to post the generated image to chat', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
),
new SlashCommandNamedArgument(
'negative', 'negative prompt prefix', [ARGUMENT_TYPE.STRING], false, false, '',
), ),
SlashCommandNamedArgument.fromProps({
name: 'negative',
description: 'negative prompt prefix',
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME],
enumProvider: commonEnumProviders.variables('all'),
}),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(
@ -3243,7 +3367,7 @@ jQuery(async () => {
], ],
helpString: ` helpString: `
<div> <div>
Requests to generate an image and posts it to chat (unless quiet=true argument is specified). Supported arguments: <code>${Object.values(triggerWords).flat().join(', ')}</code>. Requests to generate an image and posts it to chat (unless <code>quiet=true</code> argument is specified).</code>.
</div> </div>
<div> <div>
Anything else would trigger a "free mode" to make generate whatever you prompted. Example: <code>/imagine apple tree</code> would generate a picture of an apple tree. Returns a link to the generated image. Anything else would trigger a "free mode" to make generate whatever you prompted. Example: <code>/imagine apple tree</code> would generate a picture of an apple tree. Returns a link to the generated image.
@ -3256,9 +3380,12 @@ jQuery(async () => {
callback: changeComfyWorkflow, callback: changeComfyWorkflow,
aliases: ['icw'], aliases: ['icw'],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'workflowName', [ARGUMENT_TYPE.STRING], true, description: 'workflow name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => Array.from(document.querySelectorAll('#sd_comfy_workflow > [value]')).map(x => x.getAttribute('value')).map(workflow => new SlashCommandEnumValue(workflow)),
}),
], ],
helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>', helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>',
})); }));
@ -3326,6 +3453,11 @@ jQuery(async () => {
$('#sd_clip_skip').on('input', onClipSkipInput); $('#sd_clip_skip').on('input', onClipSkipInput);
$('#sd_seed').on('input', onSeedInput); $('#sd_seed').on('input', onSeedInput);
$('#sd_character_prompt_share').on('input', onCharacterPromptShareInput); $('#sd_character_prompt_share').on('input', onCharacterPromptShareInput);
$('#sd_free_extend').on('input', onFreeExtendInput);
$('#sd_wand_visible').on('input', onWandVisibleInput);
$('#sd_command_visible').on('input', onCommandVisibleInput);
$('#sd_interactive_visible').on('input', onInteractiveVisibleInput);
$('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
$('.sd_settings .inline-drawer-toggle').on('click', function () { $('.sd_settings .inline-drawer-toggle').on('click', function () {
initScrollHeight($('#sd_prompt_prefix')); initScrollHeight($('#sd_prompt_prefix'));

View File

@ -22,6 +22,11 @@
<input id="sd_multimodal_captioning" type="checkbox" /> <input id="sd_multimodal_captioning" type="checkbox" />
<span data-i18n="sd_multimodal_captioning_txt">Use multimodal captioning for portraits</span> <span data-i18n="sd_multimodal_captioning_txt">Use multimodal captioning for portraits</span>
</label> </label>
<label for="sd_free_extend" class="checkbox_label" data-i18n="[title]sd_free_extend" title="Automatically extend free mode subject prompts (not portraits or backgrounds) using a currently selected LLM.">
<input id="sd_free_extend" type="checkbox" />
<span data-i18n="sd_free_extend_txt">Extend free mode prompts</span>
<small data-i18n="sd_free_extend_small">(interactive/commands)</small>
</label>
<label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model"> <label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">
<input id="sd_expand" type="checkbox" /> <input id="sd_expand" type="checkbox" />
<span data-i18n="sd_expand_txt">Auto-enhance prompts</span> <span data-i18n="sd_expand_txt">Auto-enhance prompts</span>
@ -123,16 +128,20 @@
<div data-sd-source="openai"> <div data-sd-source="openai">
<small data-i18n="These settings only apply to DALL-E 3">These settings only apply to DALL-E 3</small> <small data-i18n="These settings only apply to DALL-E 3">These settings only apply to DALL-E 3</small>
<div class="flex-container"> <div class="flex-container">
<label for="sd_openai_style" data-i18n="Image Style">Image Style</label> <div class="flex1">
<select id="sd_openai_style"> <label for="sd_openai_style" data-i18n="Image Style">Image Style</label>
<option value="vivid">Vivid</option> <select id="sd_openai_style">
<option value="natural">Natural</option> <option value="vivid">Vivid</option>
</select> <option value="natural">Natural</option>
<label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> </select>
<select id="sd_openai_quality"> </div>
<option value="standard" data-i18n="Standard">Standard</option> <div class="flex1">
<option value="hd" data-i18n="HD">HD</option> <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
</select> <select id="sd_openai_quality">
<option value="standard" data-i18n="Standard">Standard</option>
<option value="hd" data-i18n="HD">HD</option>
</select>
</div>
</div> </div>
</div> </div>
<div data-sd-source="comfy"> <div data-sd-source="comfy">
@ -180,91 +189,164 @@
</label> </label>
</div> </div>
</div> </div>
<label for="sd_scale" data-i18n="CFG Scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
<input id="sd_scale" type="range" min="{{scale_min}}" max="{{scale_max}}" step="{{scale_step}}" value="{{scale}}" /> <div class="flex-container">
<div data-sd-source="novel" class="marginTopBot5"> <div class="flex1">
<label class="checkbox_label" for="sd_novel_decrisper" title="Reduce artifacts caused by high guidance values."> <label for="sd_model" data-i18n="Model">Model</label>
<input id="sd_novel_decrisper" type="checkbox" /> <select id="sd_model"></select>
<span data-i18n="Decrisper"> </div>
Decrisper
</span> <div class="flex1" data-sd-source="comfy">
</label> <label for="sd_vae">VAE</label>
<select id="sd_vae"></select>
</div>
</div> </div>
<label for="sd_steps" data-i18n="Sampling steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
<input id="sd_steps" type="range" min="{{steps_min}}" max="{{steps_max}}" step="{{steps_step}}" value="{{steps}}" /> <div class="flex-container">
<label for="sd_width" data-i18n="Width">Width (<span id="sd_width_value"></span>)</label> <div class="flex1">
<input id="sd_width" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{width}}" /> <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
<label for="sd_height" data-i18n="Height">Height (<span id="sd_height_value"></span>)</label> <select id="sd_sampler"></select>
<input id="sd_height" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{height}}" /> </div>
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
<select id="sd_resolution"><!-- Populated in JS --></select> <div class="flex1" data-sd-source="comfy,auto">
<label for="sd_model" data-i18n="Model">Model</label> <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
<select id="sd_model"></select> <select id="sd_scheduler"></select>
<label for="sd_sampler" data-i18n="Sampling method">Sampling method</label> </div>
<select id="sd_sampler"></select>
<label data-sd-source="horde" for="sd_horde_karras" class="checkbox_label marginTopBot5">
<input id="sd_horde_karras" type="checkbox" />
<span data-i18n="Karras (not all samplers supported)">
Karras (not all samplers supported)
</span>
</label>
<div data-sd-source="novel" class="flex-container marginTopBot5">
<label class="flex1 checkbox_label" data-i18n="[title]SMEA versions of samplers are modified to perform better at high resolution." title="SMEA versions of samplers are modified to perform better at high resolution.">
<input id="sd_novel_sm" type="checkbox" />
<span data-i18n="SMEA">
SMEA
</span>
</label>
<label class="flex1 checkbox_label" data-i18n="[title]DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions." title="DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.">
<input id="sd_novel_sm_dyn" type="checkbox" />
<span data-i18n="DYN">
DYN
</span>
</label>
</div> </div>
<div data-sd-source="comfy,auto">
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label> <div class="flex-container">
<select id="sd_scheduler"></select> <div class="flex1">
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
<select id="sd_resolution"><!-- Populated in JS --></select>
</div>
<div class="flex1" data-sd-source="auto,vlad,drawthings">
<label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label>
<select id="sd_hr_upscaler"></select>
</div>
</div> </div>
<div data-sd-source="comfy">
<label for="sd_vae">VAE</label> <div class="flex-container">
<select id="sd_vae"></select> <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p">
<small>
<span data-i18n="Sampling steps">Sampling steps</span>
</small>
<input class="neo-range-slider" type="range" id="sd_steps" name="sd_steps" min="{{steps_min}}" max="{{steps_max}}" step="{{steps_step}}" value="{{steps}}" >
<input class="neo-range-input" type="number" id="sd_steps_value" data-for="sd_steps" min="{{steps_min}}" max="{{steps_max}}" step="{{steps_step}}" value="{{steps}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p">
<small>
<span data-i18n="CFG Scale">CFG Scale</span>
</small>
<input class="neo-range-slider" type="range" id="sd_scale" name="sd_scale" min="{{scale_min}}" max="{{scale_max}}" step="{{scale_step}}" value="{{scale}}" >
<input class="neo-range-input" type="number" id="sd_scale_value" data-for="sd_scale" min="{{scale_min}}" max="{{scale_max}}" step="{{scale_step}}" value="{{scale}}" >
</div>
</div> </div>
<div class="flex-container marginTopBot5">
<div id="sd_dimensions_block" class="flex-container">
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p">
<small>
<span data-i18n="Width">Width</span>
</small>
<input class="neo-range-slider" type="range" id="sd_width" name="sd_width" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{width}}" >
<input class="neo-range-input" type="number" id="sd_width_value" data-for="sd_width" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{width}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p">
<small>
<span data-i18n="Height">Height</span>
</small>
<input class="neo-range-slider" type="range" id="sd_height" name="sd_height" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{height}}" >
<input class="neo-range-input" type="number" id="sd_height_value" data-for="sd_height" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{height}}" >
</div>
<div id="sd_swap_dimensions" class="right_menu_button" title="Swap width and height" data-i18n="[title]Swap width and height">
<i class="fa-solid fa-arrow-right-arrow-left"></i>
</div>
</div>
<div class="flex-container">
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,drawthings">
<small>
<span data-i18n="Upscale by">Upscale by</span>
</small>
<input class="neo-range-slider" type="range" id="sd_hr_scale" name="sd_hr_scale" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" >
<input class="neo-range-input" type="number" id="sd_hr_scale_value" data-for="sd_hr_scale" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad">
<small>
<span data-i18n="Denoising strength">Denoising strength</span>
</small>
<input class="neo-range-slider" type="range" id="sd_denoising_strength" name="sd_denoising_strength" min="{{denoising_strength_min}}" max="{{denoising_strength_max}}" step="{{denoising_strength_step}}" value="{{denoising_strength}}" >
<input class="neo-range-input" type="number" id="sd_denoising_strength_value" data-for="sd_denoising_strength" min="{{denoising_strength_min}}" max="{{denoising_strength_max}}" step="{{denoising_strength_step}}" value="{{denoising_strength}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad">
<small>
<span data-i18n="Hires steps (2nd pass)">Hires steps (2nd pass)</span>
</small>
<input class="neo-range-slider" type="range" id="sd_hr_second_pass_steps" name="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" >
<input class="neo-range-input" type="number" id="sd_hr_second_pass_steps_value" data-for="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="novel">
<small>
<span data-i18n="Upscale by">Upscale by</span>
</small>
<input class="neo-range-slider" type="range" id="sd_novel_upscale_ratio" name="sd_novel_upscale_ratio" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" >
<input class="neo-range-input" type="number" id="sd_novel_upscale_ratio_value" data-for="sd_novel_upscale_ratio" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras">
<small>
<span data-i18n="CLIP Skip">CLIP Skip</span>
</small>
<input class="neo-range-slider" type="range" id="sd_clip_skip" name="sd_clip_skip" min="{{clip_skip_min}}" max="{{clip_skip_max}}" step="{{clip_skip_step}}" value="{{clip_skip}}" >
<input class="neo-range-input" type="number" id="sd_clip_skip_value" data-for="sd_clip_skip" min="{{clip_skip_min}}" max="{{clip_skip_max}}" step="{{clip_skip_step}}" value="{{clip_skip}}" >
</div>
</div>
<div class="flex-container marginTopBot5" data-sd-source="auto,vlad,extras,horde,drawthings,comfy">
<label class="flex1 checkbox_label"> <label class="flex1 checkbox_label">
<input id="sd_restore_faces" type="checkbox" /> <input id="sd_restore_faces" type="checkbox" />
<span data-i18n="Restore Faces">Restore Faces</span> <small data-i18n="Restore Faces">Restore Faces</small>
</label> </label>
<label class="flex1 checkbox_label"> <label class="flex1 checkbox_label">
<input id="sd_enable_hr" type="checkbox" /> <input id="sd_enable_hr" type="checkbox" />
<span data-i18n="Hires. Fix">Hires. Fix</span> <small data-i18n="Hires. Fix">Hires. Fix</small>
</label>
<label data-sd-source="horde" for="sd_horde_karras" class="flex1 checkbox_label">
<input id="sd_horde_karras" type="checkbox" />
<small data-i18n="Karras">Karras</small>
<i class="fa-solid fa-info-circle fa-sm opacity50p" data-i18n="[title]Not all samplers supported." title="Not all samplers supported."></i>
</label> </label>
</div> </div>
<div data-sd-source="auto,vlad,comfy,horde,drawthings,extras" class="marginTopBot5">
<label for="sd_clip_skip">CLIP Skip (<span id="sd_clip_skip_value"></span>)</label> <div class="flex-container marginTopBot5" data-sd-source="novel">
<input type="range" id="sd_clip_skip" min="{{clip_skip_min}}" max="{{clip_skip_max}}" step="{{clip_skip_step}}" value="{{clip_skip}}" /> <label class="flex1 checkbox_label" data-i18n="[title]SMEA versions of samplers are modified to perform better at high resolution." title="SMEA versions of samplers are modified to perform better at high resolution.">
</div> <input id="sd_novel_sm" type="checkbox" />
<div data-sd-source="auto,vlad,drawthings"> <small data-i18n="SMEA">SMEA</small>
<label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label> </label>
<select id="sd_hr_upscaler"></select> <label class="flex1 checkbox_label" data-i18n="[title]DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions." title="DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.">
<label for="sd_hr_scale"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_hr_scale_value"></span>)</label> <input id="sd_novel_sm_dyn" type="checkbox" />
<input id="sd_hr_scale" type="range" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" /> <small data-i18n="DYN">DYN</small>
</div> </label>
<div data-sd-source="auto,vlad"> <label class="flex1 checkbox_label" for="sd_novel_decrisper" title="Reduce artifacts caused by high guidance values.">
<label for="sd_denoising_strength"><span data-i18n="Denoising strength">Denoising strength</span> (<span id="sd_denoising_strength_value"></span>)</label> <input id="sd_novel_decrisper" type="checkbox" />
<input id="sd_denoising_strength" type="range" min="{{denoising_strength_min}}" max="{{denoising_strength_max}}" step="{{denoising_strength_step}}" value="{{denoising_strength}}" /> <small data-i18n="Decrisper">Decrisper</small>
<label for="sd_hr_second_pass_steps"><span data-i18n="Hires steps (2nd pass)">Hires steps (2nd pass)</span> (<span id="sd_hr_second_pass_steps_value"></span>)</label> </label>
<input id="sd_hr_second_pass_steps" type="range" min="{{hr_second_pass_steps_min}}" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" />
</div>
<div data-sd-source="novel">
<label for="sd_novel_upscale_ratio"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_novel_upscale_ratio_value"></span>)</label>
<input id="sd_novel_upscale_ratio" type="range" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" />
</div> </div>
<div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras" class="marginTop5"> <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras" class="marginTop5">
<label for="sd_seed">Seed</label> <label for="sd_seed">
<small>(-1 for random)</small> <span data-i18n="Seed">Seed</span>
<small data-i18n="(-1 for random)">(-1 for random)</small>
</label>
<input id="sd_seed" type="number" class="text_pole" min="-1" max="9999999999" step="1" /> <input id="sd_seed" type="number" class="text_pole" min="-1" max="9999999999" step="1" />
</div> </div>
<hr> <hr>
<h4 data-i18n="[title]Preset for prompt prefix and negative prompt" title="Preset for prompt prefix and negative prompt"> <h4 data-i18n="[title]Preset for prompt prefix and negative prompt" title="Preset for prompt prefix and negative prompt">
<span data-i18n="Style">Style</span> <span data-i18n="Style">Style</span>
@ -279,16 +361,16 @@
</div> </div>
</div> </div>
<label for="sd_prompt_prefix" data-i18n="Common prompt prefix">Common prompt prefix</label> <label for="sd_prompt_prefix" data-i18n="Common prompt prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_prompt_prefix_placeholder" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea> <textarea id="sd_prompt_prefix" class="text_pole textarea_compact" data-i18n="[placeholder]sd_prompt_prefix_placeholder" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
<label for="sd_negative_prompt" data-i18n="Negative common prompt prefix">Negative common prompt prefix</label> <label for="sd_negative_prompt" data-i18n="Negative common prompt prefix">Negative common prompt prefix</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea> <textarea id="sd_negative_prompt" class="text_pole textarea_compact"></textarea>
<div id="sd_character_prompt_block"> <div id="sd_character_prompt_block">
<label for="sd_character_prompt" data-i18n="Character-specific prompt prefix">Character-specific prompt prefix</label> <label for="sd_character_prompt" data-i18n="Character-specific prompt prefix">Character-specific prompt prefix</label>
<small data-i18n="Won't be used in groups.">Won't be used in groups.</small> <small data-i18n="Won't be used in groups.">Won't be used in groups.</small>
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_character_prompt_placeholder" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prompt prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea> <textarea id="sd_character_prompt" class="text_pole textarea_compact" data-i18n="[placeholder]sd_character_prompt_placeholder" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prompt prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea>
<label for="sd_character_negative_prompt" data-i18n="Character-specific negative prompt prefix">Character-specific negative prompt prefix</label> <label for="sd_character_negative_prompt" data-i18n="Character-specific negative prompt prefix">Character-specific negative prompt prefix</label>
<small data-i18n="Won't be used in groups.">Won't be used in groups.</small> <small data-i18n="Won't be used in groups.">Won't be used in groups.</small>
<textarea id="sd_character_negative_prompt" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_character_negative_prompt_placeholder" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prompt prefix.&#10;Example: jewellery, shoes, glasses"></textarea> <textarea id="sd_character_negative_prompt" class="text_pole textarea_compact" data-i18n="[placeholder]sd_character_negative_prompt_placeholder" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prompt prefix.&#10;Example: jewellery, shoes, glasses"></textarea>
<label for="sd_character_prompt_share" class="checkbox_label flexWrap marginTop5"> <label for="sd_character_prompt_share" class="checkbox_label flexWrap marginTop5">
<input id="sd_character_prompt_share" type="checkbox" /> <input id="sd_character_prompt_share" type="checkbox" />
<span data-i18n="Shareable"> <span data-i18n="Shareable">
@ -299,6 +381,36 @@
</small> </small>
</label> </label>
</div> </div>
<hr>
<h4 data-i18n="Chat Message Visibility (by source)">
Chat Message Visibility (by source)
</h4>
<small data-i18n="Uncheck to hide the extension's messages in chat prompts.">
Uncheck to hide the extension's messages in chat prompts.
</small>
<div class="flex-container flexFlowColumn marginTopBot5 flexGap10">
<label for="sd_wand_visible" class="checkbox_label">
<span class="flex1 flex-container alignItemsCenter">
<i class="fa-solid fa-wand-magic-sparkles"></i>
<span data-i18n="Extensions Menu">Extensions Menu</span>
</span>
<input id="sd_wand_visible" type="checkbox" />
</label>
<label for="sd_command_visible" class="checkbox_label">
<span class="flex1 flex-container alignItemsCenter">
<i class="fa-solid fa-terminal"></i>
<span data-i18n="Slash Command">Slash Command</span>
</span>
<input id="sd_command_visible" type="checkbox" />
</label>
<label for="sd_interactive_visible" class="checkbox_label">
<span class="flex1 flex-container alignItemsCenter">
<i class="fa-solid fa-message"></i>
<span data-i18n="Interactive Mode">Interactive Mode</span>
</span>
<input id="sd_interactive_visible" type="checkbox" />
</label>
</div>
</div> </div>
</div> </div>
<div class="inline-drawer"> <div class="inline-drawer">

View File

@ -96,3 +96,19 @@
opacity: 1; opacity: 1;
} }
} }
.sd_settings .flex1.checkbox_label input[type="checkbox"] {
margin-right: 5px;
margin-left: 5px;
}
#sd_dimensions_block {
position: relative;
}
#sd_swap_dimensions {
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
}

View File

@ -123,6 +123,7 @@ async function doCount() {
//toastr success with the token count of the chat //toastr success with the token count of the chat
const count = await getTokenCountAsync(allMessages); const count = await getTokenCountAsync(allMessages);
toastr.success(`Token count: ${count}`); toastr.success(`Token count: ${count}`);
return count;
} }
jQuery(() => { jQuery(() => {
@ -134,7 +135,7 @@ jQuery(() => {
$('#extensionsMenu').prepend(buttonHtml); $('#extensionsMenu').prepend(buttonHtml);
$('#token_counter').on('click', doTokenCounter); $('#token_counter').on('click', doTokenCounter);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count',
callback: doCount, callback: async () => String(await doCount()),
returns: 'number of tokens', returns: 'number of tokens',
helpString: 'Counts the number of tokens in the current chat.', helpString: 'Counts the number of tokens in the current chat.',
})); }));

View File

@ -0,0 +1,8 @@
<div id="translate_chat" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-language extensionsMenuExtensionButton" /></div>
<span data-i18n="ext_translate_btn_chat">Translate Chat</span>
</div>
<div id="translate_input_message" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-keyboard extensionsMenuExtensionButton" /></div>
<span data-i18n="ext_translate_btn_input">Translate Input</span>
</div>

View File

@ -0,0 +1 @@
<h3 data-i18n="ext_translate_delete_confirm_1">Are you sure?</h3><span data-i18n="ext_translate_delete_confirm_2">This will remove translated text from all messages in the current chat. This action cannot be undone.</span>

View File

@ -0,0 +1,38 @@
<div class="translation_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="ext_translate_title">Chat Translation</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="translation_auto_mode" class="checkbox_label" data-i18n="ext_translate_auto_mode">Auto-mode</label>
<select id="translation_auto_mode">
<option data-i18n="ext_translate_mode_none" value="none">None</option>
<option data-i18n="ext_translate_mode_responses" value="responses">Translate responses</option>
<option data-i18n="ext_translate_mode_inputs" value="inputs">Translate inputs</option>
<option data-i18n="ext_translate_mode_both" value="both">Translate both</option>
</select>
<label data-i18n="ext_translate_mode_provider" for="translation_provider">Provider</label>
<div class="flex-container gap5px flexnowrap marginBot5">
<select id="translation_provider" name="provider" class="margin0">
<option value="libre">Libre</option>
<option value="google">Google</option>
<option value="lingva">Lingva</option>
<option value="deepl">DeepL</option>
<option value="deeplx">DeepLX</option>
<option value="bing">Bing</option>
<option value="oneringtranslator">OneRingTranslator</option>
<option value="yandex">Yandex</option>
<select>
<div id="translate_key_button" class="menu_button fa-solid fa-key margin0"></div>
<div id="translate_url_button" class="menu_button fa-solid fa-link margin0"></div>
</div>
<label data-i18n="ext_translate_target_lang" for="translation_target_language">Target Language</label>
<select id="translation_target_language" name="target_language"></select>
<div id="translation_clear" class="menu_button">
<i class="fa-solid fa-trash-can"></i>
<span data-i18n="ext_translate_clear">Clear Translations</span>
</div>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@ import {
substituteParams, substituteParams,
updateMessageBlock, updateMessageBlock,
} from '../../../script.js'; } from '../../../script.js';
import { extension_settings, getContext } from '../../extensions.js'; import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
import { findSecret, secret_state, writeSecret } from '../../secrets.js'; import { findSecret, secret_state, writeSecret } from '../../secrets.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@ -340,6 +340,34 @@ async function translateProviderBing(text, lang) {
throw new Error(response.statusText); throw new Error(response.statusText);
} }
/**
* Translates text using the Yandex Translate API
* @param {string} text Text to translate
* @param {string} lang Target language code
* @returns {Promise<string>} Translated text
*/
async function translateProviderYandex(text, lang) {
let chunks = [];
const chunkSize = 5000;
if (text.length <= chunkSize) {
chunks.push(text);
} else {
chunks = splitRecursive(text, chunkSize);
}
const response = await fetch('/api/translate/yandex', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ chunks: chunks, lang: lang }),
});
if (response.ok) {
const result = await response.text();
return result;
}
throw new Error(response.statusText);
}
/** /**
* Splits text into chunks and translates each chunk separately * Splits text into chunks and translates each chunk separately
* @param {string} text Text to translate * @param {string} text Text to translate
@ -374,6 +402,10 @@ async function translate(text, lang) {
return ''; return '';
} }
if (!lang) {
lang = extension_settings.translate.target_language;
}
switch (extension_settings.translate.provider) { switch (extension_settings.translate.provider) {
case 'libre': case 'libre':
return await translateProviderLibre(text, lang); return await translateProviderLibre(text, lang);
@ -389,6 +421,8 @@ async function translate(text, lang) {
return await translateProviderOneRing(text, lang); return await translateProviderOneRing(text, lang);
case 'bing': case 'bing':
return await chunkedTranslate(text, lang, translateProviderBing, 1000); return await chunkedTranslate(text, lang, translateProviderBing, 1000);
case 'yandex':
return await translateProviderYandex(text, lang);
default: default:
console.error('Unknown translation provider', extension_settings.translate.provider); console.error('Unknown translation provider', extension_settings.translate.provider);
return text; return text;
@ -474,7 +508,8 @@ async function onTranslateChatClick() {
} }
async function onTranslationsClearClick() { async function onTranslationsClearClick() {
const confirm = await callPopup('<h3>Are you sure?</h3>This will remove translated text from all messages in the current chat. This action cannot be undone.', 'confirm'); const popupHtml = await renderExtensionTemplateAsync('translate', 'deleteConfirmation');
const confirm = await callPopup(popupHtml, 'confirm');
if (!confirm) { if (!confirm) {
return; return;
@ -532,56 +567,10 @@ const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
window['translate'] = translate; window['translate'] = translate;
jQuery(() => { jQuery(async () => {
const html = ` const html = await renderExtensionTemplateAsync('translate', 'index');
<div class="translation_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Chat Translation</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="translation_auto_mode" class="checkbox_label">Auto-mode</label>
<select id="translation_auto_mode">
<option value="none">None</option>
<option value="responses">Translate responses</option>
<option value="inputs">Translate inputs</option>
<option value="both">Translate both</option>
</select>
<label for="translation_provider">Provider</label>
<div class="flex-container gap5px flexnowrap marginBot5">
<select id="translation_provider" name="provider" class="margin0">
<option value="libre">Libre</option>
<option value="google">Google</option>
<option value="lingva">Lingva</option>
<option value="deepl">DeepL</option>
<option value="deeplx">DeepLX</option>
<option value="bing">Bing</option>
<option value="oneringtranslator">OneRingTranslator</option>
<select>
<div id="translate_key_button" class="menu_button fa-solid fa-key margin0"></div>
<div id="translate_url_button" class="menu_button fa-solid fa-link margin0"></div>
</div>
<label for="translation_target_language">Target Language</label>
<select id="translation_target_language" name="target_language"></select>
<div id="translation_clear" class="menu_button">
<i class="fa-solid fa-trash-can"></i>
<span>Clear Translations</span>
</div>
</div>
</div>
</div>`;
const buttonHtml = ` const buttonHtml = await renderExtensionTemplateAsync('translate', 'buttons');
<div id="translate_chat" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-language extensionsMenuExtensionButton" /></div>
Translate Chat
</div>
<div id="translate_input_message" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-keyboard extensionsMenuExtensionButton" /></div>
Translate Input
</div>
`;
$('#extensionsMenu').append(buttonHtml); $('#extensionsMenu').append(buttonHtml);
$('#extensions_settings2').append(html); $('#extensions_settings2').append(html);
$('#translate_chat').on('click', onTranslateChatClick); $('#translate_chat').on('click', onTranslateChatClick);
@ -668,5 +657,6 @@ jQuery(() => {
: extension_settings.translate.target_language; : extension_settings.translate.target_language;
return await translate(String(value), target); return await translate(String(value), target);
}, },
returns: ARGUMENT_TYPE.STRING,
})); }));
}); });

View File

@ -175,7 +175,7 @@ class AzureTtsProvider {
const url = URL.createObjectURL(audio); const url = URL.createObjectURL(audio);
this.audioElement.src = url; this.audioElement.src = url;
this.audioElement.play(); this.audioElement.play();
URL.revokeObjectURL(url); this.audioElement.onended = () => URL.revokeObjectURL(url);
} }
async fetchTtsGeneration(text, voiceId) { async fetchTtsGeneration(text, voiceId) {

View File

@ -155,7 +155,7 @@ class EdgeTtsProvider {
const url = URL.createObjectURL(audio); const url = URL.createObjectURL(audio);
this.audioElement.src = url; this.audioElement.src = url;
this.audioElement.play(); this.audioElement.play();
URL.revokeObjectURL(url); this.audioElement.onended = () => URL.revokeObjectURL(url);
} }
/** /**

View File

@ -75,9 +75,9 @@ class ElevenLabsTtsProvider {
this.settings.style_exaggeration = $('#elevenlabs_tts_style_exaggeration').val(); this.settings.style_exaggeration = $('#elevenlabs_tts_style_exaggeration').val();
this.settings.speaker_boost = $('#elevenlabs_tts_speaker_boost').is(':checked'); this.settings.speaker_boost = $('#elevenlabs_tts_speaker_boost').is(':checked');
this.settings.model = $('#elevenlabs_tts_model').find(':selected').val(); this.settings.model = $('#elevenlabs_tts_model').find(':selected').val();
$('#elevenlabs_tts_stability_output').text(this.settings.stability * 100 + '%'); $('#elevenlabs_tts_stability_output').text(Math.round(this.settings.stability * 100) + '%');
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost * 100 + '%'); $('#elevenlabs_tts_similarity_boost_output').text(Math.round(this.settings.similarity_boost * 100) + '%');
$('#elevenlabs_tts_style_exaggeration_output').text(this.settings.style_exaggeration * 100 + '%'); $('#elevenlabs_tts_style_exaggeration_output').text(Math.round(this.settings.style_exaggeration * 100) + '%');
$('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings()); $('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());
saveTtsProviderSettings(); saveTtsProviderSettings();
} }
@ -117,9 +117,9 @@ class ElevenLabsTtsProvider {
$('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this)); $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));
$('#elevenlabs_tts_speaker_boost').on('change', this.onSettingsChange.bind(this)); $('#elevenlabs_tts_speaker_boost').on('change', this.onSettingsChange.bind(this));
$('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this)); $('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this));
$('#elevenlabs_tts_stability_output').text(this.settings.stability); $('#elevenlabs_tts_stability_output').text(Math.round(this.settings.stability * 100) + '%');
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost); $('#elevenlabs_tts_similarity_boost_output').text(Math.round(this.settings.similarity_boost * 100) + '%');
$('#elevenlabs_tts_style_exaggeration_output').text(this.settings.style_exaggeration); $('#elevenlabs_tts_style_exaggeration_output').text(Math.round(this.settings.style_exaggeration * 100) + '%');
$('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings()); $('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());
try { try {
await this.checkReady(); await this.checkReady();
@ -311,8 +311,8 @@ class ElevenLabsTtsProvider {
}, },
}; };
if (this.shouldInvolveExtendedSettings()) { if (this.shouldInvolveExtendedSettings()) {
request.voice_settings.style_exaggeration = Number(this.settings.style_exaggeration); request.voice_settings.style = Number(this.settings.style_exaggeration);
request.voice_settings.speaker_boost = Boolean(this.settings.speaker_boost); request.voice_settings.use_speaker_boost = Boolean(this.settings.speaker_boost);
} }
const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, { const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
method: 'POST', method: 'POST',

View File

@ -1,4 +1,4 @@
import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced, substituteParams } from '../../../script.js'; import { callPopup, cancelTtsPlay, eventSource, event_types, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js'; import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js';
import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js'; import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
import { EdgeTtsProvider } from './edge.js'; import { EdgeTtsProvider } from './edge.js';
@ -11,12 +11,16 @@ import { power_user } from '../../power-user.js';
import { OpenAITtsProvider } from './openai.js'; import { OpenAITtsProvider } from './openai.js';
import { XTTSTtsProvider } from './xtts.js'; import { XTTSTtsProvider } from './xtts.js';
import { GSVITtsProvider } from './gsvi.js'; import { GSVITtsProvider } from './gsvi.js';
import { SBVits2TtsProvider } from './sbvits2.js';
import { AllTalkTtsProvider } from './alltalk.js'; import { AllTalkTtsProvider } from './alltalk.js';
import { SpeechT5TtsProvider } from './speecht5.js'; import { SpeechT5TtsProvider } from './speecht5.js';
import { AzureTtsProvider } from './azure.js'; import { AzureTtsProvider } from './azure.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { debounce_timeout } from '../../constants.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
export { talkingAnimation }; export { talkingAnimation };
const UPDATE_INTERVAL = 1000; const UPDATE_INTERVAL = 1000;
@ -27,6 +31,8 @@ let talkingHeadState = false;
let lastChatId = null; let lastChatId = null;
let lastMessage = null; let lastMessage = null;
let lastMessageHash = null; let lastMessageHash = null;
let periodicMessageGenerationTimer = null;
let lastPositionOfParagraphEnd = -1;
const DEFAULT_VOICE_MARKER = '[Default Voice]'; const DEFAULT_VOICE_MARKER = '[Default Voice]';
const DISABLED_VOICE_MARKER = 'disabled'; const DISABLED_VOICE_MARKER = 'disabled';
@ -77,6 +83,7 @@ const ttsProviders = {
Silero: SileroTtsProvider, Silero: SileroTtsProvider,
XTTSv2: XTTSTtsProvider, XTTSv2: XTTSTtsProvider,
GSVI: GSVITtsProvider, GSVI: GSVITtsProvider,
SBVits2: SBVits2TtsProvider,
System: SystemTtsProvider, System: SystemTtsProvider,
Coqui: CoquiTtsProvider, Coqui: CoquiTtsProvider,
Edge: EdgeTtsProvider, Edge: EdgeTtsProvider,
@ -107,7 +114,7 @@ async function onNarrateOneMessage() {
async function onNarrateText(args, text) { async function onNarrateText(args, text) {
if (!text) { if (!text) {
return; return '';
} }
audioElement.src = '/sounds/silence.mp3'; audioElement.src = '/sounds/silence.mp3';
@ -133,6 +140,7 @@ async function onNarrateText(args, text) {
// Return back to the chat voices // Return back to the chat voices
await initVoiceMap(false); await initVoiceMap(false);
return '';
} }
async function moduleWorker() { async function moduleWorker() {
@ -529,6 +537,7 @@ function loadSettings() {
$('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only); $('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only);
$('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only); $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
$('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation); $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
$('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
$('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only); $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
$('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user); $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks); $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@ -592,6 +601,12 @@ function onAutoGenerationClick() {
} }
function onPeriodicAutoGenerationClick() {
extension_settings.tts.periodic_auto_generation = !!$('#tts_periodic_auto_generation').prop('checked');
saveSettingsDebounced();
}
function onNarrateDialoguesClick() { function onNarrateDialoguesClick() {
extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked'); extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
@ -677,13 +692,14 @@ export function saveTtsProviderSettings() {
//###################// //###################//
async function onChatChanged() { async function onChatChanged() {
await resetTtsPlayback(); await onGenerationEnded();
resetTtsPlayback();
const voiceMapInit = initVoiceMap(); const voiceMapInit = initVoiceMap();
await Promise.race([voiceMapInit, delay(1000)]); await Promise.race([voiceMapInit, delay(debounce_timeout.relaxed)]);
lastMessage = null; lastMessage = null;
} }
async function onMessageEvent(messageId) { async function onMessageEvent(messageId, lastCharIndex) {
// If TTS is disabled, do nothing // If TTS is disabled, do nothing
if (!extension_settings.tts.enabled) { if (!extension_settings.tts.enabled) {
return; return;
@ -721,12 +737,17 @@ async function onMessageEvent(messageId) {
return; return;
} }
// if we only want to process part of the message
if (lastCharIndex) {
message.mes = message.mes.substring(0, lastCharIndex);
}
const isLastMessageInCurrent = () => const isLastMessageInCurrent = () =>
lastMessage && lastMessage &&
typeof lastMessage === 'object' && typeof lastMessage === 'object' &&
message.swipe_id === lastMessage.swipe_id && message.swipe_id === lastMessage.swipe_id &&
message.name === lastMessage.name && message.name === lastMessage.name &&
message.is_user === lastMessage.is_user && message.is_user === lastMessage.is_user &&
message.mes.indexOf(lastMessage.mes) !== -1; message.mes.indexOf(lastMessage.mes) !== -1;
// if last message within current message, message got extended. only send diff to TTS. // if last message within current message, message got extended. only send diff to TTS.
@ -779,6 +800,83 @@ async function onMessageDeleted() {
resetTtsPlayback(); resetTtsPlayback();
} }
async function onGenerationStarted(generationType, _args, isDryRun) {
// If dry running or quiet mode, do nothing
if (isDryRun || ['quiet', 'impersonate'].includes(generationType)) {
return;
}
// If TTS is disabled, do nothing
if (!extension_settings.tts.enabled) {
return;
}
// Auto generation is disabled
if (!extension_settings.tts.auto_generation) {
return;
}
// Periodic auto generation is disabled
if (!extension_settings.tts.periodic_auto_generation) {
return;
}
// If the reply is not being streamed
if (!isStreamingEnabled()) {
return;
}
// start the timer
if (!periodicMessageGenerationTimer) {
periodicMessageGenerationTimer = setInterval(onPeriodicMessageGenerationTick, UPDATE_INTERVAL);
}
}
async function onGenerationEnded() {
if (periodicMessageGenerationTimer) {
clearInterval(periodicMessageGenerationTimer);
periodicMessageGenerationTimer = null;
}
lastPositionOfParagraphEnd = -1;
}
async function onPeriodicMessageGenerationTick() {
const context = getContext();
// no characters or group selected
if (!context.groupId && context.characterId === undefined) {
return;
}
const lastMessageId = context.chat.length - 1;
// the last message was from the user
if (context.chat[lastMessageId].is_user) {
return;
}
const lastMessage = structuredClone(context.chat[lastMessageId]);
const lastMessageText = lastMessage?.mes ?? '';
// look for double ending lines which should indicate the end of a paragraph
let newLastPositionOfParagraphEnd = lastMessageText
.indexOf('\n\n', lastPositionOfParagraphEnd + 1);
// if not found, look for a single ending line which should indicate the end of a paragraph
if (newLastPositionOfParagraphEnd === -1) {
newLastPositionOfParagraphEnd = lastMessageText
.indexOf('\n', lastPositionOfParagraphEnd + 1);
}
// send the message to the tts module if we found the new end of a paragraph
if (newLastPositionOfParagraphEnd > -1) {
onMessageEvent(lastMessageId, newLastPositionOfParagraphEnd);
if (periodicMessageGenerationTimer) {
lastPositionOfParagraphEnd = newLastPositionOfParagraphEnd;
}
}
}
/** /**
* Get characters in current chat * Get characters in current chat
* @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
@ -1008,6 +1106,10 @@ $(document).ready(function () {
<input type="checkbox" id="tts_auto_generation"> <input type="checkbox" id="tts_auto_generation">
<small>Auto Generation</small> <small>Auto Generation</small>
</label> </label>
<label class="checkbox_label" for="tts_periodic_auto_generation" title="Requires auto generation to be enabled.">
<input type="checkbox" id="tts_periodic_auto_generation">
<small>Narrate by paragraphs (when streaming)</small>
</label>
<label class="checkbox_label" for="tts_narrate_quoted"> <label class="checkbox_label" for="tts_narrate_quoted">
<input type="checkbox" id="tts_narrate_quoted"> <input type="checkbox" id="tts_narrate_quoted">
<small>Only narrate "quotes"</small> <small>Only narrate "quotes"</small>
@ -1070,6 +1172,7 @@ $(document).ready(function () {
$('#tts_skip_tags').on('click', onSkipTagsClick); $('#tts_skip_tags').on('click', onSkipTagsClick);
$('#tts_pass_asterisks').on('click', onPassAsterisksClick); $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
$('#tts_auto_generation').on('click', onAutoGenerationClick); $('#tts_auto_generation').on('click', onAutoGenerationClick);
$('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
$('#tts_narrate_user').on('click', onNarrateUserClick); $('#tts_narrate_user').on('click', onNarrateUserClick);
$('#playback_rate').on('input', function () { $('#playback_rate').on('input', function () {
@ -1097,15 +1200,25 @@ $(document).ready(function () {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged); eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted); eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
eventSource.on(event_types.GROUP_UPDATED, onChatChanged); eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);
eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded);
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent); eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent);
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent); eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'speak', SlashCommandParser.addCommandObject(SlashCommand.fromProps({
callback: onNarrateText, name: 'speak',
callback: async (args, value) => {
await onNarrateText(args, value);
return '';
},
aliases: ['narrate', 'tts'], aliases: ['narrate', 'tts'],
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( SlashCommandNamedArgument.fromProps({
'voice', 'character voice name', [ARGUMENT_TYPE.STRING], false, name: 'voice',
), description: 'character voice name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
enumProvider: () => Object.keys(voiceMap).map(voiceName => new SlashCommandEnumValue(voiceName, null, enumTypes.enum, enumIcons.voice)),
}),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(

View File

@ -180,7 +180,7 @@ class NovelTtsProvider {
const url = URL.createObjectURL(audio); const url = URL.createObjectURL(audio);
this.audioElement.src = url; this.audioElement.src = url;
this.audioElement.play(); this.audioElement.play();
URL.revokeObjectURL(url); this.audioElement.onended = () => URL.revokeObjectURL(url);
} }
async* fetchTtsGeneration(inputText, voiceId) { async* fetchTtsGeneration(inputText, voiceId) {

View File

@ -0,0 +1,343 @@
import { getPreviewString, saveTtsProviderSettings } from './index.js';
export { SBVits2TtsProvider };
class SBVits2TtsProvider {
//########//
// Config //
//########//
settings;
ready = false;
voices = [];
separator = '. ';
audioElement = document.createElement('audio');
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
// backup for auto_split
text = text.replace(/\n+/g, '<br>');
return text;
}
languageLabels = {
'Chinese': 'ZH',
'English': 'EN',
'Japanese': 'JP',
};
langKey2LangCode = {
'ZH': 'zh-CN',
'EN': 'en-US',
'JP': 'ja-JP',
};
defaultSettings = {
provider_endpoint: 'http://localhost:5000',
sdp_ratio: 0.2,
noise: 0.6,
noisew: 0.8,
length: 1,
language: 'JP',
auto_split: true,
split_interval: 0.5,
assist_text: '',
assist_text_weight: 1,
style: 'Neutral',
style_weight: 1,
reference_audio_path: '',
};
get settingsHtml() {
let html = `
<label for="sbvits_api_language">Language</label>
<select id="sbvits_api_language">`;
for (let language in this.languageLabels) {
if (this.languageLabels[language] == this.settings?.language) {
html += `<option value="${this.languageLabels[language]}" selected="selected">${language}</option>`;
continue;
}
html += `<option value="${this.languageLabels[language]}">${language}</option>`;
}
html += `
</select>
<label">SBVits2 Settings:</label><br/>
<label for="sbvits_tts_endpoint">Provider Endpoint:</label>
<input id="sbvits_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
<span>Use <a target="_blank" href="https://github.com/litagin02/Style-Bert-VITS2">Style-Bert-VITS2 API Server</a>.</span><br/>
<label for="sbvits_sdp_ratio">sdp_ratio: <span id="sbvits_sdp_ratio_output">${this.defaultSettings.sdp_ratio}</span></label>
<input id="sbvits_sdp_ratio" type="range" value="${this.defaultSettings.sdp_ratio}" min="0.0" max="1" step="0.01" />
<label for="sbvits_noise">noise: <span id="sbvits_noise_output">${this.defaultSettings.noise}</span></label>
<input id="sbvits_noise" type="range" value="${this.defaultSettings.noise}" min="0.1" max="2" step="0.01" />
<label for="sbvits_noisew">noisew: <span id="sbvits_noisew_output">${this.defaultSettings.noisew}</span></label>
<input id="sbvits_noisew" type="range" value="${this.defaultSettings.noisew}" min="0.1" max="2" step="0.01" />
<label for="sbvits_length">length: <span id="sbvits_length_output">${this.defaultSettings.length}</span></label>
<input id="sbvits_length" type="range" value="${this.defaultSettings.length}" min="0.0" max="5" step="0.01" />
<label for="sbvits_auto_split" class="checkbox_label">
<input id="sbvits_auto_split" type="checkbox" ${this.defaultSettings.auto_split ? 'checked' : ''} />
Enable Text Splitting
</label>
<label for="sbvits_split_interval">split_interval: <span id="sbvits_split_interval_output">${this.defaultSettings.split_interval}</span></label>
<input id="sbvits_split_interval" type="range" value="${this.defaultSettings.split_interval}" min="0.0" max="5" step="0.01" />
<label for="sbvits_assist_text">assist_text:</label>
<input id="sbvits_assist_text" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.assist_text}"/>
<label for="sbvits_assist_text_weight">assist_text_weight: <span id="sbvits_assist_text_weight_output">${this.defaultSettings.assist_text_weight}</span></label>
<input id="sbvits_assist_text_weight" type="range" value="${this.defaultSettings.assist_text_weight}" min="0.0" max="1" step="0.01" />
<label for="sbvits_style_weight">style_weight: <span id="sbvits_style_weight_output">${this.defaultSettings.style_weight}</span></label>
<input id="sbvits_style_weight" type="range" value="${this.defaultSettings.style_weight}" min="0.0" max="20" step="0.01" />
<label for="sbvits_reference_audio_path">reference_audio_path:</label>
<input id="sbvits_reference_audio_path" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.reference_audio_path}"/>
`;
return html;
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.provider_endpoint = $('#sbvits_tts_endpoint').val();
this.settings.language = $('#sbvits_api_language').val();
this.settings.assist_text = $('#sbvits_assist_text').val();
this.settings.reference_audio_path = $('#sbvits_reference_audio_path').val();
// Update the default TTS settings based on input fields
this.settings.sdp_ratio = $('#sbvits_sdp_ratio').val();
this.settings.noise = $('#sbvits_noise').val();
this.settings.noisew = $('#sbvits_noisew').val();
this.settings.length = $('#sbvits_length').val();
this.settings.auto_split = $('#sbvits_auto_split').is(':checked');
this.settings.split_interval = $('#sbvits_split_interval').val();
this.settings.assist_text_weight = $('#sbvits_assist_text_weight').val();
this.settings.style_weight = $('#sbvits_style_weight').val();
// Update the UI to reflect changes
$('#sbvits_sdp_ratio_output').text(this.settings.sdp_ratio);
$('#sbvits_noise_output').text(this.settings.noise);
$('#sbvits_noisew_output').text(this.settings.noisew);
$('#sbvits_length_output').text(this.settings.length);
$('#sbvits_split_interval_output').text(this.settings.split_interval);
$('#sbvits_assist_text_weight_output').text(this.settings.assist_text_weight);
$('#sbvits_style_weight_output').text(this.settings.style_weight);
saveTtsProviderSettings();
this.changeTTSSettings();
}
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info('Using default TTS Provider settings');
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings;
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key];
} else {
console.debug(`Ignoring non-user-configurable setting: ${key}`);
}
}
// Set initial values from the settings
$('#sbvits_tts_endpoint').val(this.settings.provider_endpoint);
$('#sbvits_api_language').val(this.settings.language);
$('#sbvits_assist_text').val(this.settings.assist_text);
$('#sbvits_reference_audio_path').val(this.settings.reference_audio_path);
$('#sbvits_sdp_ratio').val(this.settings.sdp_ratio);
$('#sbvits_noise').val(this.settings.noise);
$('#sbvits_noisew').val(this.settings.noisew);
$('#sbvits_length').val(this.settings.length);
$('#sbvits_auto_split').prop('checked', this.settings.auto_split);
$('#sbvits_split_interval').val(this.settings.split_interval);
$('#sbvits_assist_text_weight').val(this.settings.assist_text_weight);
$('#sbvits_style_weight').val(this.settings.style_weight);
// Update the UI to reflect changes
$('#sbvits_sdp_ratio_output').text(this.settings.sdp_ratio);
$('#sbvits_noise_output').text(this.settings.noise);
$('#sbvits_noisew_output').text(this.settings.noisew);
$('#sbvits_length_output').text(this.settings.length);
$('#sbvits_split_interval_output').text(this.settings.split_interval);
$('#sbvits_assist_text_weight_output').text(this.settings.assist_text_weight);
$('#sbvits_style_weight_output').text(this.settings.style_weight);
// Register input/change event listeners to update settings on user interaction
$('#sbvits_tts_endpoint').on('input', () => { this.onSettingsChange(); });
$('#sbvits_api_language').on('change', () => { this.onSettingsChange(); });
$('#sbvits_assist_text').on('input', () => { this.onSettingsChange(); });
$('#sbvits_reference_audio_path').on('input', () => { this.onSettingsChange(); });
$('#sbvits_sdp_ratio').on('change', () => { this.onSettingsChange(); });
$('#sbvits_noise').on('change', () => { this.onSettingsChange(); });
$('#sbvits_noisew').on('change', () => { this.onSettingsChange(); });
$('#sbvits_length').on('change', () => { this.onSettingsChange(); });
$('#sbvits_auto_split').on('change', () => { this.onSettingsChange(); });
$('#sbvits_split_interval').on('change', () => { this.onSettingsChange(); });
$('#sbvits_assist_text_weight').on('change', () => { this.onSettingsChange(); });
$('#sbvits_style_weight').on('change', () => { this.onSettingsChange(); });
await this.checkReady();
console.info('SBVits2: Settings loaded');
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
}
async onRefreshClick() {
return;
}
//#################//
// TTS Interfaces //
//#################//
/**
* Get a voice from the TTS provider.
* @param {string} voiceName Voice name to get
* @returns {Promise<Object>} Voice object
*/
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceObjects();
}
const match = this.voices.filter(
v => v.name == voiceName,
)[0];
if (!match) {
throw `TTS Voice name ${voiceName} not found`;
}
return match;
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId);
return response;
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceObjects() {
const response = await fetch(`${this.settings.provider_endpoint}/models/info`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const data = await response.json();
const voices = Object.keys(data).flatMap(key => {
const config = data[key];
const spk2id = config.spk2id;
const style2id = config.style2id;
return Object.entries(spk2id).flatMap(([speaker, speaker_id]) => {
return Object.entries(style2id).map(([style, styleId]) => {
return {
name: `${speaker} (${style})`,
voice_id: `${key}-${speaker_id}-${style}`,
preview_url: false,
};
});
});
});
this.voices = voices; // Assign to the class property
return voices; // Also return this list
}
// Each time a parameter is changed, we change the configuration
async changeTTSSettings() {
}
/**
* Fetch TTS generation from the API.
* @param {string} inputText Text to generate TTS for
* @param {string} voiceId Voice ID to use (model_id-speaker_id-style)
* @returns {Promise<Response>} Fetch response
*/
async fetchTtsGeneration(inputText, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`);
const [model_id, speaker_id, style] = voiceId.split('-');
const params = new URLSearchParams();
// restore for auto_split
inputText = inputText.replaceAll('<br>', '\n');
params.append('text', inputText);
params.append('model_id', model_id);
params.append('speaker_id', speaker_id);
params.append('sdp_ratio', this.settings.sdp_ratio);
params.append('noise', this.settings.noise);
params.append('noisew', this.settings.noisew);
params.append('length', this.settings.length);
params.append('language', this.settings.language);
params.append('auto_split', this.settings.auto_split);
params.append('split_interval', this.settings.split_interval);
if (this.settings.assist_text) {
params.append('assist_text', this.settings.assist_text);
params.append('assist_text_weight', this.settings.assist_text_weight);
}
params.append('style', style);
params.append('style_weight', this.settings.style_weight);
if (this.settings.reference_audio_path) {
params.append('reference_audio_path', this.settings.reference_audio_path);
}
const url = `${this.settings.provider_endpoint}/voice?${params.toString()}`;
const response = await fetch(
url,
{
method: 'POST',
headers: {
},
},
);
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
/**
* Preview TTS for a given voice ID.
* @param {string} id Voice ID
*/
async previewTtsVoice(id) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
const lang_code = this.langKey2LangCode[this.settings.lang] ?? 'ja-JP';
const text = getPreviewString(lang_code);
const response = await this.fetchTtsGeneration(text, id);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const audio = await response.blob();
const url = URL.createObjectURL(audio);
this.audioElement.src = url;
this.audioElement.play();
}
// Interface not used
async fetchTtsFromHistory(history_item_id) {
return Promise.resolve(history_item_id);
}
}

View File

@ -60,7 +60,7 @@ class SpeechT5TtsProvider {
const url = URL.createObjectURL(audio); const url = URL.createObjectURL(audio);
this.audioElement.src = url; this.audioElement.src = url;
this.audioElement.play(); this.audioElement.play();
URL.revokeObjectURL(url); this.audioElement.onended = () => URL.revokeObjectURL(url);
} }
async loadSettings(settings) { async loadSettings(settings) {

View File

@ -10,6 +10,7 @@ import {
setExtensionPrompt, setExtensionPrompt,
substituteParams, substituteParams,
generateRaw, generateRaw,
substituteParamsExtended,
} from '../../../script.js'; } from '../../../script.js';
import { import {
ModuleWorkerWrapper, ModuleWorkerWrapper,
@ -19,7 +20,7 @@ import {
renderExtensionTemplateAsync, renderExtensionTemplateAsync,
doExtrasFetch, getApiUrl, doExtrasFetch, getApiUrl,
} from '../../extensions.js'; } from '../../extensions.js';
import { collapseNewlines } from '../../power-user.js'; import { collapseNewlines, registerDebugFunction } from '../../power-user.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js'; import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence } from '../../utils.js'; import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence } from '../../utils.js';
@ -44,10 +45,12 @@ const settings = {
cohere_model: 'embed-english-v3.0', cohere_model: 'embed-english-v3.0',
ollama_model: 'mxbai-embed-large', ollama_model: 'mxbai-embed-large',
ollama_keep: false, ollama_keep: false,
vllm_model: '',
summarize: false, summarize: false,
summarize_sent: false, summarize_sent: false,
summary_source: 'main', summary_source: 'main',
summary_prompt: 'Pause your roleplay. Summarize the most important parts of the message. Limit yourself to 250 words or less. Your response should include nothing but the summary.', summary_prompt: 'Pause your roleplay. Summarize the most important parts of the message. Limit yourself to 250 words or less. Your response should include nothing but the summary.',
force_chunk_delimiter: '',
// For chats // For chats
enabled_chats: false, enabled_chats: false,
@ -151,6 +154,20 @@ async function onVectorizeAllClick() {
let syncBlocked = false; let syncBlocked = false;
/**
* Gets the chunk delimiters for splitting text.
* @returns {string[]} Array of chunk delimiters
*/
function getChunkDelimiters() {
const delimiters = ['\n\n', '\n', ' ', ''];
if (settings.force_chunk_delimiter) {
delimiters.unshift(settings.force_chunk_delimiter);
}
return delimiters;
}
/** /**
* Splits messages into chunks before inserting them into the vector index. * Splits messages into chunks before inserting them into the vector index.
* @param {object[]} items Array of vector items * @param {object[]} items Array of vector items
@ -164,7 +181,7 @@ function splitByChunks(items) {
const chunkedItems = []; const chunkedItems = [];
for (const item of items) { for (const item of items) {
const chunks = splitRecursive(item.text, settings.message_chunk_size); const chunks = splitRecursive(item.text, settings.message_chunk_size, getChunkDelimiters());
for (const chunk of chunks) { for (const chunk of chunks) {
const chunkedItem = { ...item, text: chunk }; const chunkedItem = { ...item, text: chunk };
chunkedItems.push(chunkedItem); chunkedItems.push(chunkedItem);
@ -440,7 +457,7 @@ async function injectDataBankChunks(queryText, collectionIds) {
return; return;
} }
const insertedText = substituteParams(settings.file_template_db.replace(/{{text}}/i, textResult)); const insertedText = substituteParamsExtended(settings.file_template_db, { text: textResult });
setExtensionPrompt(EXTENSION_PROMPT_TAG_DB, insertedText, settings.file_position_db, settings.file_depth_db, settings.include_wi, settings.file_depth_role_db); setExtensionPrompt(EXTENSION_PROMPT_TAG_DB, insertedText, settings.file_position_db, settings.file_depth_db, settings.include_wi, settings.file_depth_role_db);
} catch (error) { } catch (error) {
console.error('Vectors: Failed to insert Data Bank chunks', error); console.error('Vectors: Failed to insert Data Bank chunks', error);
@ -482,9 +499,10 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla
const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`); const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`);
const overlapSize = Math.round(chunkSize * overlapPercent / 100); const overlapSize = Math.round(chunkSize * overlapPercent / 100);
const delimiters = getChunkDelimiters();
// Overlap should not be included in chunk size. It will be later compensated by overlapChunks // Overlap should not be included in chunk size. It will be later compensated by overlapChunks
chunkSize = overlapSize > 0 ? (chunkSize - overlapSize) : chunkSize; chunkSize = overlapSize > 0 ? (chunkSize - overlapSize) : chunkSize;
const chunks = splitRecursive(fileText, chunkSize).map((x, y, z) => overlapSize > 0 ? overlapChunks(x, y, z, overlapSize) : x); const chunks = splitRecursive(fileText, chunkSize, delimiters).map((x, y, z) => overlapSize > 0 ? overlapChunks(x, y, z, overlapSize) : x);
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks with ${overlapPercent}% overlap`, chunks); console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks with ${overlapPercent}% overlap`, chunks);
const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index })); const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index }));
@ -591,7 +609,7 @@ async function rearrangeChat(chat) {
function getPromptText(queriedMessages) { function getPromptText(queriedMessages) {
const queriedText = queriedMessages.map(x => collapseNewlines(`${x.name}: ${x.mes}`).trim()).join('\n\n'); const queriedText = queriedMessages.map(x => collapseNewlines(`${x.name}: ${x.mes}`).trim()).join('\n\n');
console.log('Vectors: relevant past messages found.\n', queriedText); console.log('Vectors: relevant past messages found.\n', queriedText);
return substituteParams(settings.template.replace(/{{text}}/i, queriedText)); return substituteParamsExtended(settings.template, { text: queriedText });
} }
/** /**
@ -691,6 +709,9 @@ function getVectorHeaders() {
case 'llamacpp': case 'llamacpp':
addLlamaCppHeaders(headers); addLlamaCppHeaders(headers);
break; break;
case 'vllm':
addVllmHeaders(headers);
break;
default: default:
break; break;
} }
@ -761,6 +782,17 @@ function addLlamaCppHeaders(headers) {
}); });
} }
/**
* Add headers for the VLLM API source.
* @param {object} headers Header object
*/
function addVllmHeaders(headers) {
Object.assign(headers, {
'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
'X-Vllm-Model': extension_settings.vectors.vllm_model,
});
}
/** /**
* Inserts vector items into a collection * Inserts vector items into a collection
* @param {string} collectionId - The collection to insert into * @param {string} collectionId - The collection to insert into
@ -801,11 +833,12 @@ function throwIfSourceInvalid() {
} }
if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] || if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||
settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] ||
settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) { settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' }); throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
} }
if (settings.source === 'ollama' && !settings.ollama_model) { if (settings.source === 'ollama' && !settings.ollama_model || settings.source === 'vllm' && !settings.vllm_model) {
throw new Error('Vectors: API model missing', { cause: 'api_model_missing' }); throw new Error('Vectors: API model missing', { cause: 'api_model_missing' });
} }
@ -956,6 +989,28 @@ async function purgeVectorIndex(collectionId) {
} }
} }
/**
* Purges all vector indexes.
*/
async function purgeAllVectorIndexes() {
try {
const response = await fetch('/api/vector/purge-all', {
method: 'POST',
headers: getRequestHeaders(),
});
if (!response.ok) {
throw new Error('Failed to purge all vector indexes');
}
console.log('Vectors: Purged all vector indexes');
toastr.success('All vector indexes purged', 'Purge successful');
} catch (error) {
console.error('Vectors: Failed to purge all', error);
toastr.error('Failed to purge all vector indexes', 'Purge failed');
}
}
function toggleSettings() { function toggleSettings() {
$('#vectors_files_settings').toggle(!!settings.enabled_files); $('#vectors_files_settings').toggle(!!settings.enabled_files);
$('#vectors_chats_settings').toggle(!!settings.enabled_chats); $('#vectors_chats_settings').toggle(!!settings.enabled_chats);
@ -965,6 +1020,7 @@ function toggleSettings() {
$('#cohere_vectorsModel').toggle(settings.source === 'cohere'); $('#cohere_vectorsModel').toggle(settings.source === 'cohere');
$('#ollama_vectorsModel').toggle(settings.source === 'ollama'); $('#ollama_vectorsModel').toggle(settings.source === 'ollama');
$('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp'); $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp');
$('#vllm_vectorsModel').toggle(settings.source === 'vllm');
$('#nomicai_apiKey').toggle(settings.source === 'nomicai'); $('#nomicai_apiKey').toggle(settings.source === 'nomicai');
} }
@ -1274,6 +1330,12 @@ jQuery(async () => {
Object.assign(extension_settings.vectors, settings); Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#vectors_vllm_model').val(settings.vllm_model).on('input', () => {
$('#vectors_modelWarning').show();
settings.vllm_model = String($('#vectors_vllm_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_ollama_keep').prop('checked', settings.ollama_keep).on('input', () => { $('#vectors_ollama_keep').prop('checked', settings.ollama_keep).on('input', () => {
settings.ollama_keep = $('#vectors_ollama_keep').prop('checked'); settings.ollama_keep = $('#vectors_ollama_keep').prop('checked');
Object.assign(extension_settings.vectors, settings); Object.assign(extension_settings.vectors, settings);
@ -1456,6 +1518,19 @@ jQuery(async () => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#vectors_force_chunk_delimiter').prop('checked', settings.force_chunk_delimiter).on('input', () => {
settings.force_chunk_delimiter = String($('#vectors_force_chunk_delimiter').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_ollama_pull').on('click', (e) => {
const presetModel = extension_settings.vectors.ollama_model || '';
e.preventDefault();
$('#ollama_download_model').trigger('click');
$('#dialogue_popup_input').val(presetModel);
});
const validSecret = !!secret_state[SECRET_KEYS.NOMICAI]; const validSecret = !!secret_state[SECRET_KEYS.NOMICAI];
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key'; const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
$('#api_key_nomicai').attr('placeholder', placeholder); $('#api_key_nomicai').attr('placeholder', placeholder);
@ -1525,4 +1600,11 @@ jQuery(async () => {
], ],
returns: ARGUMENT_TYPE.LIST, returns: ARGUMENT_TYPE.LIST,
})); }));
registerDebugFunction('purge-everything', 'Purge all vector indices', 'Obliterate all stored vectors for all sources. No mercy.', async () => {
if (!confirm('Are you sure?')) {
return;
}
await purgeAllVectorIndexes();
});
}); });

View File

@ -1,12 +1,12 @@
<div class="vectors_settings"> <div class="vectors_settings">
<div class="inline-drawer"> <div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<b>Vector Storage</b> <b data-i18n="Vector Storage">Vector Storage</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<label for="vectors_source"> <label for="vectors_source" data-i18n="Vectorization Source">
Vectorization Source Vectorization Source
</label> </label>
<select id="vectors_source" class="text_pole"> <select id="vectors_source" class="text_pole">
@ -14,37 +14,41 @@
<option value="extras">Extras</option> <option value="extras">Extras</option>
<option value="palm">Google MakerSuite</option> <option value="palm">Google MakerSuite</option>
<option value="llamacpp">llama.cpp</option> <option value="llamacpp">llama.cpp</option>
<option value="transformers">Local (Transformers)</option> <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
<option value="mistral">MistralAI</option> <option value="mistral">MistralAI</option>
<option value="nomicai">NomicAI</option> <option value="nomicai">NomicAI</option>
<option value="ollama">Ollama</option> <option value="ollama">Ollama</option>
<option value="openai">OpenAI</option> <option value="openai">OpenAI</option>
<option value="togetherai">TogetherAI</option> <option value="togetherai">TogetherAI</option>
<option value="vllm">vLLM</option>
</select> </select>
</div> </div>
<div class="flex-container flexFlowColumn" id="ollama_vectorsModel"> <div class="flex-container flexFlowColumn" id="ollama_vectorsModel">
<label for="vectors_ollama_model"> <label for="vectors_ollama_model" data-i18n="Vectorization Model">
Vectorization Model Vectorization Model
</label> </label>
<input id="vectors_ollama_model" class="text_pole" type="text" placeholder="Model tag, e.g. llama3" /> <input id="vectors_ollama_model" class="text_pole" type="text" placeholder="Model tag, e.g. llama3" />
<label for="vectors_ollama_keep" class="checkbox_label" title="When checked, the model will not be unloaded after use."> <label for="vectors_ollama_keep" class="checkbox_label" title="When checked, the model will not be unloaded after use.">
<input id="vectors_ollama_keep" type="checkbox" /> <input id="vectors_ollama_keep" type="checkbox" />
<span>Keep model in memory</span> <span data-i18n="Keep model in memory">Keep model in memory</span>
</label> </label>
<i> <div>
Hint: Download models and set the URL in the API connection settings. The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="vectors_ollama_pull">click here</a>.
</div>
<i data-i18n="Hint: Set the URL in the API connection settings.">
Hint: Set the URL in the API connection settings.
</i> </i>
</div> </div>
<div class="flex-container flexFlowColumn" id="llamacpp_vectorsModel"> <div class="flex-container flexFlowColumn" id="llamacpp_vectorsModel">
<span> <span data-i18n="The server MUST be started with the --embedding flag to use this feature!">
The server MUST be started with the <code>--embedding</code> flag to use this feature! The server MUST be started with the <code>--embedding</code> flag to use this feature!
</span> </span>
<i> <i data-i18n="Hint: Set the URL in the API connection settings.">
Hint: Set the URL in the API connection settings. Hint: Set the URL in the API connection settings.
</i> </i>
</div> </div>
<div class="flex-container flexFlowColumn" id="openai_vectorsModel"> <div class="flex-container flexFlowColumn" id="openai_vectorsModel">
<label for="vectors_openai_model"> <label for="vectors_openai_model" data-i18n="Vectorization Model">
Vectorization Model Vectorization Model
</label> </label>
<select id="vectors_openai_model" class="text_pole"> <select id="vectors_openai_model" class="text_pole">
@ -54,7 +58,7 @@
</select> </select>
</div> </div>
<div class="flex-container flexFlowColumn" id="cohere_vectorsModel"> <div class="flex-container flexFlowColumn" id="cohere_vectorsModel">
<label for="vectors_cohere_model"> <label for="vectors_cohere_model" data-i18n="Vectorization Model">
Vectorization Model Vectorization Model
</label> </label>
<select id="vectors_cohere_model" class="text_pole"> <select id="vectors_cohere_model" class="text_pole">
@ -68,7 +72,7 @@
</select> </select>
</div> </div>
<div class="flex-container flexFlowColumn" id="together_vectorsModel"> <div class="flex-container flexFlowColumn" id="together_vectorsModel">
<label for="vectors_togetherai_model"> <label for="vectors_togetherai_model" data-i18n="Vectorization Model">
Vectorization Model Vectorization Model
</label> </label>
<select id="vectors_togetherai_model" class="text_pole"> <select id="vectors_togetherai_model" class="text_pole">
@ -82,6 +86,15 @@
<option value="bert-base-uncased">Bert Base Uncased</option> <option value="bert-base-uncased">Bert Base Uncased</option>
</select> </select>
</div> </div>
<div class="flex-container flexFlowColumn" id="vllm_vectorsModel">
<label for="vectors_vllm_model" data-i18n="Vectorization Model">
Vectorization Model
</label>
<input id="vectors_vllm_model" class="text_pole" type="text" placeholder="Model name, e.g. intfloat/e5-mistral-7b-instruct" />
<i data-i18n="Hint: Set the URL in the API connection settings.">
Hint: Set the URL in the API connection settings.
</i>
</div>
<small id="vectors_modelWarning"> <small id="vectors_modelWarning">
<i class="fa-solid fa-exclamation-triangle"></i> <i class="fa-solid fa-exclamation-triangle"></i>
@ -92,7 +105,7 @@
<div class="flex-container flexFlowColumn" id="nomicai_apiKey"> <div class="flex-container flexFlowColumn" id="nomicai_apiKey">
<label for="api_key_nomicai"> <label for="api_key_nomicai">
<span>NomicAI API Key</span> <span data-i18n="NomicAI API Key">NomicAI API Key</span>
</label> </label>
<div class="flex-container"> <div class="flex-container">
<input id="api_key_nomicai" name="api_key_nomicai" class="text_pole flex1 wide100p" maxlength="500" size="35" type="text" autocomplete="off"> <input id="api_key_nomicai" name="api_key_nomicai" class="text_pole flex1 wide100p" maxlength="500" size="35" type="text" autocomplete="off">
@ -107,48 +120,54 @@
<div class="flex-container marginTopBot5"> <div class="flex-container marginTopBot5">
<div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance."> <div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance.">
<label for="vectors_query"> <label for="vectors_query">
<span>Query messages</span> <small data-i18n="Query messages">Query messages</small>
</label> </label>
<input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" /> <input type="number" id="vectors_query" class="text_pole" min="1" max="99" />
</div> </div>
<div class="flex-container flex1 flexFlowColumn" title="Cut-off score for relevance. Helps to filter out irrelevant data."> <div class="flex-container flex1 flexFlowColumn" title="Cut-off score for relevance. Helps to filter out irrelevant data.">
<label for="vectors_query"> <label for="vectors_query">
<span>Score threshold</span> <small data-i18n="Score threshold">Score threshold</small>
</label> </label>
<input type="number" id="vectors_score_threshold" class="text_pole widthUnset" min="0" max="1" step="0.05" /> <input type="number" id="vectors_score_threshold" class="text_pole" min="0" max="1" step="0.05" />
</div>
<div class="flex-container flex1 flexFlowColumn" title="Prioritize chunking on the preferred delimiter.">
<label for="vectors_force_chunk_delimiter">
<small data-i18n="Chunk boundary">Chunk boundary</small>
</label>
<textarea id="vectors_force_chunk_delimiter" class="text_pole" rows="1" placeholder="(None)"></textarea>
</div> </div>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<label class="checkbox_label expander" for="vectors_include_wi" title="Query results can activate World Info entries."> <label class="checkbox_label expander" for="vectors_include_wi" title="Query results can activate World Info entries.">
<input id="vectors_include_wi" type="checkbox" class="checkbox"> <input id="vectors_include_wi" type="checkbox" class="checkbox">
Include in World Info Scanning <span data-i18n="Include in World Info Scanning">Include in World Info Scanning</span>
</label> </label>
</div> </div>
<hr> <hr>
<h4> <h4 data-i18n="World Info settings">
World Info settings World Info settings
</h4> </h4>
<label class="checkbox_label" for="vectors_enabled_world_info" title="Enable activation of World Info entries based on vector similarity."> <label class="checkbox_label" for="vectors_enabled_world_info" title="Enable activation of World Info entries based on vector similarity.">
<input id="vectors_enabled_world_info" type="checkbox" class="checkbox"> <input id="vectors_enabled_world_info" type="checkbox" class="checkbox">
Enabled for World Info <span data-i18n="Enable for World Info">Enable for World Info</span>
</label> </label>
<div id="vectors_world_info_settings" class="marginTopBot5"> <div id="vectors_world_info_settings" class="marginTopBot5">
<div class="flex-container"> <div class="flex-container">
<label for="vectors_enabled_for_all" class="checkbox_label"> <label for="vectors_enabled_for_all" class="checkbox_label">
<input id="vectors_enabled_for_all" type="checkbox" /> <input id="vectors_enabled_for_all" type="checkbox" />
<span>Enabled for all entries</span> <span data-i18n="Enabled for all entries">Enabled for all entries</span>
</label> </label>
<ul class="margin0"> <ul class="margin0">
<li> <li>
<small>Checked: all entries except ❌ status can be activated.</small> <small data-i18n="Checked: all entries except ❌ status can be activated.">Checked: all entries except ❌ status can be activated.</small>
</li> </li>
<li> <li>
<small>Unchecked: only entries with 🔗 status can be activated.</small> <small data-i18n="Unchecked: only entries with ❌ status can be activated.">Unchecked: only entries with 🔗 status can be activated.</small>
</li> </li>
</ul> </ul>
</div> </div>
@ -158,7 +177,7 @@
</div> </div>
<div class="flex1" title="Maximum number of entries to be activated"> <div class="flex1" title="Maximum number of entries to be activated">
<label for="vectors_max_entries" > <label for="vectors_max_entries" >
<small>Max Entries</small> <small data-i18n="Max Entries">Max Entries</small>
</label> </label>
<input id="vectors_max_entries" type="number" class="text_pole widthUnset" min="1" max="9999" /> <input id="vectors_max_entries" type="number" class="text_pole widthUnset" min="1" max="9999" />
</div> </div>
@ -168,13 +187,13 @@
</div> </div>
</div> </div>
<h4> <h4 data-i18n="File vectorization settings">
File vectorization settings File vectorization settings
</h4> </h4>
<label class="checkbox_label" for="vectors_enabled_files"> <label class="checkbox_label" for="vectors_enabled_files">
<input id="vectors_enabled_files" type="checkbox" class="checkbox"> <input id="vectors_enabled_files" type="checkbox" class="checkbox">
Enabled for files <span data-i18n="Enable for files">Enable for files</span>
</label> </label>
<div id="vectors_files_settings" class="marginTopBot5"> <div id="vectors_files_settings" class="marginTopBot5">
@ -186,152 +205,153 @@
<i class="fa-solid fa-flask" title="Experimental feature"></i> <i class="fa-solid fa-flask" title="Experimental feature"></i>
</label> </label>
<div class="flex justifyCenter" title="These settings apply to files attached directly to messages."> <div class="flex justifyCenter" title="These settings apply to files attached directly to messages.">
<span>Message attachments</span> <span data-i18n="Message attachments">Message attachments</span>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<div class="flex1" title="Only files past this size will be vectorized."> <div class="flex1" title="Only files past this size will be vectorized.">
<label for="vectors_size_threshold"> <label for="vectors_size_threshold">
<small>Size threshold (KB)</small> <small data-i18n="Size threshold (KB)">Size threshold (KB)</small>
</label> </label>
<input id="vectors_size_threshold" type="number" class="text_pole" min="1" max="99999" /> <input id="vectors_size_threshold" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
<div class="flex1" title="Chunk size for file splitting."> <div class="flex1" title="Chunk size for file splitting.">
<label for="vectors_chunk_size"> <label for="vectors_chunk_size">
<small>Chunk size (chars)</small> <small data-i18n="Chunk size (chars)">Chunk size (chars)</small>
</label> </label>
<input id="vectors_chunk_size" type="number" class="text_pole" min="1" max="99999" /> <input id="vectors_chunk_size" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
<div class="flex1" title="The overlap between adjacent chunks in % from chunk size. The overlap text is trimmed to sentence boundaries. 0 = disabled."> <div class="flex1" title="The overlap between adjacent chunks in % from chunk size. The overlap text is trimmed to sentence boundaries. 0 = disabled.">
<label for="vectors_overlap_percent"> <label for="vectors_overlap_percent">
<small>Chunk overlap (%)</small> <small data-i18n="Chunk overlap (%)">Chunk overlap (%)</small>
</label> </label>
<input id="vectors_overlap_percent" type="number" class="text_pole" min="0" max="99" step="1" /> <input id="vectors_overlap_percent" type="number" class="text_pole" min="0" max="99" step="1" />
</div> </div>
<div class="flex1" title="How many chunks to retrieve when querying."> <div class="flex1" title="How many chunks to retrieve when querying.">
<label for="vectors_chunk_count"> <label for="vectors_chunk_count">
<small>Retrieve chunks</small> <small data-i18n="Retrieve chunks">Retrieve chunks</small>
</label> </label>
<input id="vectors_chunk_count" type="number" class="text_pole" min="1" max="99999" /> <input id="vectors_chunk_count" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
</div> </div>
<div class="flex justifyCenter" title="These settings apply to files stored in the Data Bank."> <div class="flex justifyCenter" title="These settings apply to files stored in the Data Bank.">
<span>Data Bank files</span> <span data-i18n="Data Bank files">Data Bank files</span>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<div class="flex1" title="Only files past this size will be vectorized."> <div class="flex1" title="Only files past this size will be vectorized.">
<label for="vectors_size_threshold_db"> <label for="vectors_size_threshold_db">
<small>Size threshold (KB)</small> <small data-i18n="Size threshold (KB)">Size threshold (KB)</small>
</label> </label>
<input id="vectors_size_threshold_db" type="number" class="text_pole" min="1" max="99999" /> <input id="vectors_size_threshold_db" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
<div class="flex1" title="Chunk size for file splitting."> <div class="flex1" title="Chunk size for file splitting.">
<label for="vectors_chunk_size_db"> <label for="vectors_chunk_size_db">
<small>Chunk size (chars)</small> <small data-i18n="Chunk size (chars)">Chunk size (chars)</small>
</label> </label>
<input id="vectors_chunk_size_db" type="number" class="text_pole" min="1" max="99999" /> <input id="vectors_chunk_size_db" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
<div class="flex1" title="The overlap between adjacent chunks in % from chunk size. The overlap text is trimmed to sentence boundaries. 0 = disabled."> <div class="flex1" title="The overlap between adjacent chunks in % from chunk size. The overlap text is trimmed to sentence boundaries. 0 = disabled.">
<label for="vectors_overlap_percent_db"> <label for="vectors_overlap_percent_db">
<small>Chunk overlap (%)</small> <small data-i18n="Chunk overlap (%)">Chunk overlap (%)</small>
</label> </label>
<input id="vectors_overlap_percent_db" type="number" class="text_pole" min="0" max="99" step="1" /> <input id="vectors_overlap_percent_db" type="number" class="text_pole" min="0" max="99" step="1" />
</div> </div>
<div class="flex1" title="How many chunks to retrieve when querying."> <div class="flex1" title="How many chunks to retrieve when querying.">
<label for="vectors_chunk_count_db"> <label for="vectors_chunk_count_db">
<small>Retrieve chunks</small> <small data-i18n="Retrieve chunks">Retrieve chunks</small>
</label> </label>
<input id="vectors_chunk_count_db" type="number" class="text_pole" min="1" max="99999" /> <input id="vectors_chunk_count_db" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
</div> </div>
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<label for="vectors_file_template_db"> <label for="vectors_file_template_db">
<span>Injection Template</span> <span data-i18n="Injection Template">Injection Template</span>
</label> </label>
<textarea id="vectors_file_template_db" class="margin0 text_pole textarea_compact" rows="3" placeholder="Use &lcub;&lcub;text&rcub;&rcub; macro to specify the position of retrieved text."></textarea> <textarea id="vectors_file_template_db" class="margin0 text_pole textarea_compact" rows="3" placeholder="Use &lcub;&lcub;text&rcub;&rcub; macro to specify the position of retrieved text."></textarea>
<label for="vectors_file_position_db">Injection Position</label> <label for="vectors_file_position_db" data-i18n="Injection Position">Injection Position</label>
<div class="radio_group"> <div class="radio_group">
<label> <label>
<input type="radio" name="vectors_file_position_db" value="2" /> <input type="radio" name="vectors_file_position_db" value="2" />
<span>Before Main Prompt / Story String</span> <span data-i18n="Before Main Prompt / Story String">Before Main Prompt / Story String</span>
</label> </label>
<!--Keep these as 0 and 1 to interface with the setExtensionPrompt function--> <!--Keep these as 0 and 1 to interface with the setExtensionPrompt function-->
<label> <label>
<input type="radio" name="vectors_file_position_db" value="0" /> <input type="radio" name="vectors_file_position_db" value="0" />
<span>After Main Prompt / Story String</span> <span data-i18n="After Main Prompt / Story String">After Main Prompt / Story String</span>
</label> </label>
<label for="vectors_file_depth_db" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat."> <label for="vectors_file_depth_db" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
<input type="radio" name="vectors_file_position_db" value="1" /> <input type="radio" name="vectors_file_position_db" value="1" />
<span>In-chat @ Depth</span> <span data-i18n="In-chat @ Depth">In-chat @ Depth</span>
<input id="vectors_file_depth_db" class="text_pole widthUnset" type="number" min="0" max="999" /> <input id="vectors_file_depth_db" class="text_pole widthUnset" type="number" min="0" max="999" />
<span>as</span> <span>as</span>
<select id="vectors_file_depth_role_db" class="text_pole widthNatural"> <select id="vectors_file_depth_role_db" class="text_pole widthNatural">
<option value="0">System</option> <option value="0" data-i18n="System">System</option>
<option value="1">User</option> <option value="1" data-i18n="User">User</option>
<option value="2">Assistant</option> <option value="2" data-i18n="Assistant">Assistant</option>
</select> </select>
</label> </label>
</div> </div>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<div id="vectors_files_vectorize_all" class="menu_button menu_button_icon" title="Vectorize all files in the Data Bank and current chat."> <div id="vectors_files_vectorize_all" class="menu_button menu_button_icon" title="Vectorize all files in the Data Bank and current chat.">
Vectorize All <span data-i18n="Vectorize All">Vectorize All</span>
</div> </div>
<div id="vectors_files_purge" class="menu_button menu_button_icon" title="Purge all file vectors in the Data Bank and current chat."> <div id="vectors_files_purge" class="menu_button menu_button_icon" title="Purge all file vectors in the Data Bank and current chat.">
Purge Vectors <span data-i18n="Purge Vectors">Purge Vectors</span>
</div> </div>
</div> </div>
</div> </div>
<hr> <hr>
<h4> <h4 data-i18n="Chat vectorization settings">
Chat vectorization settings Chat vectorization settings
</h4> </h4>
<label class="checkbox_label" for="vectors_enabled_chats"> <label class="checkbox_label" for="vectors_enabled_chats">
<input id="vectors_enabled_chats" type="checkbox" class="checkbox"> <input id="vectors_enabled_chats" type="checkbox" class="checkbox">
Enabled for chat messages <span data-i18n="Enabled for chat messages">Enabled for chat messages</span>
</label> </label>
<hr> <hr>
<div id="vectors_chats_settings"> <div id="vectors_chats_settings">
<div id="vectors_advanced_settings"> <div id="vectors_advanced_settings">
<label for="vectors_template"> <label for="vectors_template" data-i18n="Injection Template">
Injection Template Injection Template
</label> </label>
<textarea id="vectors_template" class="text_pole textarea_compact" rows="3" placeholder="Use &lcub;&lcub;text&rcub;&rcub; macro to specify the position of retrieved text."></textarea> <textarea id="vectors_template" class="text_pole textarea_compact" rows="3" placeholder="Use &lcub;&lcub;text&rcub;&rcub; macro to specify the position of retrieved text."></textarea>
<label for="vectors_position">Injection Position</label> <label for="vectors_position" data-i18n="Injection Position">Injection Position</label>
<div class="radio_group"> <div class="radio_group">
<label> <label>
<input type="radio" name="vectors_position" value="2" /> <input type="radio" name="vectors_position" value="2" />
Before Main Prompt / Story String <span data-i18n="Before Main Prompt / Story String">Before Main Prompt / Story String</span>
</label> </label>
<!--Keep these as 0 and 1 to interface with the setExtensionPrompt function--> <!--Keep these as 0 and 1 to interface with the setExtensionPrompt function-->
<label> <label>
<input type="radio" name="vectors_position" value="0" /> <input type="radio" name="vectors_position" value="0" />
After Main Prompt / Story String <span data-i18n="After Main Prompt / Story String">After Main Prompt / Story String</span>
</label> </label>
<label for="vectors_depth" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat."> <label for="vectors_depth" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
<input type="radio" name="vectors_position" value="1" /> <input type="radio" name="vectors_position" value="1" />
In-chat @ Depth <input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="999" /> <span data-i18n="In-chat @ Depth">In-chat @ Depth </span>
<input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
</label> </label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<div class="flex1" title="Can increase the retrieval quality for the cost of processing. 0 = disabled."> <div class="flex1" title="Can increase the retrieval quality for the cost of processing. 0 = disabled.">
<label for="vectors_message_chunk_size"> <label for="vectors_message_chunk_size">
<small>Chunk size (chars)</small> <small data-i18n="Chunk size (chars)">Chunk size (chars)</small>
</label> </label>
<input id="vectors_message_chunk_size" type="number" class="text_pole widthUnset" min="0" max="9999" /> <input id="vectors_message_chunk_size" type="number" class="text_pole widthUnset" min="0" max="9999" />
</div> </div>
<div class="flex1" title="Prevents last N messages from being placed out of order."> <div class="flex1" title="Prevents last N messages from being placed out of order.">
<label for="vectors_protect"> <label for="vectors_protect">
<small>Retain#</small> <small data-i18n="Retain#">Retain#</small>
</label> </label>
<input type="number" id="vectors_protect" class="text_pole widthUnset" min="1" max="9999" /> <input type="number" id="vectors_protect" class="text_pole widthUnset" min="1" max="9999" />
</div> </div>
<div class="flex1" title="How many past messages to insert as memories."> <div class="flex1" title="How many past messages to insert as memories.">
<label for="vectors_insert"> <label for="vectors_insert">
<small>Insert#</small> <small data-i18n="Insert#">Insert#</small>
</label> </label>
<input type="number" id="vectors_insert" class="text_pole widthUnset" min="1" max="9999" /> <input type="number" id="vectors_insert" class="text_pole widthUnset" min="1" max="9999" />
</div> </div>
@ -340,43 +360,43 @@
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">
<div class="flex-container alignitemscenter justifyCenter"> <div class="flex-container alignitemscenter justifyCenter">
<i class="fa-solid fa-flask" title="Summarization for vectors is an experimental feature that may improve vectors or may worsen them. Use at your own discretion."></i> <i class="fa-solid fa-flask" title="Summarization for vectors is an experimental feature that may improve vectors or may worsen them. Use at your own discretion."></i>
<span>Vector Summarization</span> <span data-i18n="Vector Summarization">Vector Summarization</span>
</div> </div>
<label class="checkbox_label expander" for="vectors_summarize" title="Summarize chat messages before generating embeddings."> <label class="checkbox_label expander" for="vectors_summarize" title="Summarize chat messages before generating embeddings.">
<input id="vectors_summarize" type="checkbox" class="checkbox"> <input id="vectors_summarize" type="checkbox" class="checkbox">
Summarize chat messages for vector generation <span data-i18n="Summarize chat messages for vector generation">Summarize chat messages for vector generation</span>
</label> </label>
<i class="failure">Warning: This will slow down vector generation drastically, as all messages have to be summarized first.</i> <i class="failure" data-i18n="Warning: This will slow down vector generation drastically, as all messages have to be summarized first.">Warning: This will slow down vector generation drastically, as all messages have to be summarized first.</i>
<label class="checkbox_label expander" for="vectors_summarize_user" title="Summarize sent chat messages before generating embeddings."> <label class="checkbox_label expander" for="vectors_summarize_user" title="Summarize sent chat messages before generating embeddings.">
<input id="vectors_summarize_user" type="checkbox" class="checkbox"> <input id="vectors_summarize_user" type="checkbox" class="checkbox">
Summarize chat messages when sending <span data-i18n="Summarize chat messages when sending">Summarize chat messages when sending</span>
</label> </label>
<i class="failure">Warning: This might cause your sent messages to take a bit to process and slow down response time.</i> <i class="failure" data-i18n="Warning: This might cause your sent messages to take a bit to process and slow down response time.">Warning: This might cause your sent messages to take a bit to process and slow down response time.</i>
<label for="vectors_summary_source">Summarize with:</label> <label for="vectors_summary_source" title="Summarize with:">Summarize with:</label>
<select id="vectors_summary_source" class="text_pole"> <select id="vectors_summary_source" class="text_pole">
<option value="main">Main API</option> <option value="main" data-i18n="Main API">Main API</option>
<option value="extras">Extras API</option> <option value="extras" data-i18n="Extras API">Extras API</option>
</select> </select>
<label for="vectors_summary_prompt">Summary Prompt:</label> <label for="vectors_summary_prompt" title="Summary Prompt:">Summary Prompt:</label>
<small>Only used when Main API is selected.</small> <small data-i18n="Only used when Main API is selected.">Only used when Main API is selected.</small>
<textarea id="vectors_summary_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation."></textarea> <textarea id="vectors_summary_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation."></textarea>
</div> </div>
</div> </div>
<small> <small data-i18n="Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.">
Old messages are vectorized gradually as you chat. Old messages are vectorized gradually as you chat.
To process all previous messages, click the button below. To process all previous messages, click the button below.
</small> </small>
<div class="flex-container"> <div class="flex-container">
<div id="vectors_vectorize_all" class="menu_button menu_button_icon"> <div id="vectors_vectorize_all" class="menu_button menu_button_icon" data-i18n="Vectorize All">
Vectorize All Vectorize All
</div> </div>
<div id="vectors_purge" class="menu_button menu_button_icon"> <div id="vectors_purge" class="menu_button menu_button_icon" data-i18n="Purge Vectors">
Purge Vectors Purge Vectors
</div> </div>
<div id="vectors_view_stats" class="menu_button menu_button_icon"> <div id="vectors_view_stats" class="menu_button menu_button_icon" data-i18n="View Stats">
View Stats View Stats
</div> </div>
</div> </div>

View File

@ -73,7 +73,7 @@ import {
depth_prompt_role_default, depth_prompt_role_default,
shouldAutoContinue, shouldAutoContinue,
} from '../script.js'; } from '../script.js';
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map } from './tags.js'; import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js'; import { FILTER_TYPES, FilterHelper } from './filters.js';
import { isExternalMediaAllowed } from './chats.js'; import { isExternalMediaAllowed } from './chats.js';
@ -183,6 +183,7 @@ export async function getGroupChat(groupId, reload = false) {
const group = groups.find((x) => x.id === groupId); const group = groups.find((x) => x.id === groupId);
const chat_id = group.chat_id; const chat_id = group.chat_id;
const data = await loadGroupChat(chat_id); const data = await loadGroupChat(chat_id);
let freshChat = false;
await loadItemizedPrompts(getCurrentChatId()); await loadItemizedPrompts(getCurrentChatId());
@ -216,6 +217,7 @@ export async function getGroupChat(groupId, reload = false) {
} }
} }
await saveGroupChat(groupId, false); await saveGroupChat(groupId, false);
freshChat = true;
} }
if (group) { if (group) {
@ -228,11 +230,23 @@ export async function getGroupChat(groupId, reload = false) {
} }
await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
if (freshChat) await eventSource.emit(event_types.GROUP_CHAT_CREATED);
}
/**
* Retrieves the members of a group
*
* @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group.
* @returns {import('../script.js').Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned.
*/
export function getGroupMembers(groupId = selected_group) {
const group = groups.find((x) => x.id === groupId);
return group?.members.map(member => characters.find(x => x.avatar === member)) ?? [];
} }
/** /**
* Finds the character ID for a group member. * Finds the character ID for a group member.
* @param {string} arg 1-based member index or character name * @param {string} arg 0-based member index or character name
* @returns {number} 0-based character ID * @returns {number} 0-based character ID
*/ */
export function findGroupMemberId(arg) { export function findGroupMemberId(arg) {
@ -250,8 +264,7 @@ export function findGroupMemberId(arg) {
return; return;
} }
// Index is 1-based const index = parseInt(arg);
const index = parseInt(arg) - 1;
const searchByName = isNaN(index); const searchByName = isNaN(index);
if (searchByName) { if (searchByName) {
@ -1356,7 +1369,7 @@ function select_group_chats(groupId, skipAnimation) {
} }
// render tags // render tags
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } }); applyTagsOnGroupSelect(groupId);
// render characters list // render characters list
printGroupCandidates(); printGroupCandidates();

247
public/scripts/keyboard.js Normal file
View File

@ -0,0 +1,247 @@
/* All selectors that should act as interactables / keyboard buttons by default */
const interactableSelectors = [
'.interactable', // Main interactable class for ALL interactable controls (can also be manually added in code, so that's why its listed here)
'.custom_interactable', // Manually made interactable controls via code (see 'makeKeyboardInteractable()')
'.menu_button', // General menu button in ST
'.right_menu_button', // Button-likes in many menus
'.drawer-icon', // Main "menu bar" icons
'.inline-drawer-icon', // Buttons/icons inside the drawer menus
'.paginationjs-pages li a', // Pagination buttons
'.group_select, .character_select, .bogus_folder_select', // Cards to select char, group or folder in character list and other places
'.avatar-container', // Persona list blocks
'.tag .tag_remove', // Remove button in removable tags
'.bg_example', // Background elements in the background menu
'.bg_example .bg_button', // The inline buttons on the backgrounds
'#options a', // Option entries in the popup options menu
'.mes_buttons .mes_button', // Small inline buttons on the chat messages
'.extraMesButtons>div:not(.mes_button)', // The extra/extension buttons inline on the chat messages
'.swipe_left, .swipe_right', // Swipe buttons on the last message
'.stscript_btn', // STscript buttons in the chat bar
'.select2_choice_clickable+span.select2-container .select2-selection__choice__display', // select2 control elements if they are meant to be clickable
'.avatar_load_preview', // Char display avatar selection
];
if (CSS.supports('selector(:has(*))')) {
// Option entries in the extension menu popup that are coming from extensions
interactableSelectors.push('#extensionsMenu div:has(.extensionsMenuExtensionButton)');
}
export const INTERACTABLE_CONTROL_CLASS = 'interactable';
export const CUSTOM_INTERACTABLE_CONTROL_CLASS = 'custom_interactable';
export const NOT_FOCUSABLE_CONTROL_CLASS = 'not_focusable';
export const DISABLED_CONTROL_CLASS = 'disabled';
/**
* An observer that will check if any new interactables or scroll reset containers are added to the body
* @type {MutationObserver}
*/
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(handleNodeChange);
}
if (mutation.type === 'attributes') {
const target = mutation.target;
if (mutation.attributeName === 'class' && target instanceof Element) {
handleNodeChange(target);
}
}
});
});
/**
* Function to handle node changes (added or modified nodes)
* @param {Element} node
*/
function handleNodeChange(node) {
if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element) {
// Handle keyboard interactables
if (isKeyboardInteractable(node)) {
makeKeyboardInteractable(node);
}
initializeInteractables(node);
// Handle scroll reset containers
if (node.classList.contains('scroll-reset-container')) {
applyScrollResetBehavior(node);
}
initializeScrollResetBehaviors(node);
}
}
/**
* Registers an interactable class (for example for an extension) and makes it keyboard interactable.
* Optionally apply the 'not_focusable' and 'disabled' classes if needed.
*
* @param {string} interactableSelector - The CSS selector for the interactable (Supports class combinations, chained via dots like <c>tag.actionable</c>, and sub selectors)
* @param {object} [options={}] - Optional settings for the interactable
* @param {boolean} [options.disabledByDefault=false] - Whether interactables of this class should be disabled by default
* @param {boolean} [options.notFocusableByDefault=false] - Whether interactables of this class should not be focusable by default
*/
export function registerInteractableType(interactableSelector, { disabledByDefault = false, notFocusableByDefault = false } = {}) {
interactableSelectors.push(interactableSelector);
const interactables = document.querySelectorAll(interactableSelector);
if (disabledByDefault || notFocusableByDefault) {
interactables.forEach(interactable => {
if (disabledByDefault) interactable.classList.add(DISABLED_CONTROL_CLASS);
if (notFocusableByDefault) interactable.classList.add(NOT_FOCUSABLE_CONTROL_CLASS);
});
}
makeKeyboardInteractable(...interactables);
}
/**
* Checks if the given control is a keyboard-enabled interactable.
*
* @param {Element} control - The control element to check
* @returns {boolean} Returns true if the control is a keyboard interactable, false otherwise
*/
export function isKeyboardInteractable(control) {
// Check if this control matches any of the selectors
return interactableSelectors.some(selector => control.matches(selector));
}
/**
* Makes all the given controls keyboard interactable and sets their state.
* If the control doesn't have any of the classes, it will be set to a custom-enabled keyboard interactable.
*
* @param {Element[]} interactables - The controls to make interactable and set their state
*/
export function makeKeyboardInteractable(...interactables) {
interactables.forEach(interactable => {
// If this control doesn't have any of the classes, lets say the caller knows this and wants this to be a custom-enabled keyboard control.
if (!isKeyboardInteractable(interactable)) {
interactable.classList.add(CUSTOM_INTERACTABLE_CONTROL_CLASS);
}
// Just for CSS styling and future reference, every keyboard interactable control should have a common class
if (!interactable.classList.contains(INTERACTABLE_CONTROL_CLASS)) {
interactable.classList.add(INTERACTABLE_CONTROL_CLASS);
}
/**
* Check if the element or any parent element has 'disabled' or 'not_focusable' class
* @param {Element} el
* @returns {boolean}
*/
const hasDisabledOrNotFocusableAncestor = (el) => {
while (el) {
if (el.classList.contains(NOT_FOCUSABLE_CONTROL_CLASS) || el.classList.contains(DISABLED_CONTROL_CLASS)) {
return true;
}
el = el.parentElement;
}
return false;
};
// Set/remove the tabindex accordingly to the classes. Remembering if it had a custom value.
if (!hasDisabledOrNotFocusableAncestor(interactable)) {
if (!interactable.hasAttribute('tabindex')) {
const tabIndex = interactable.getAttribute('data-original-tabindex') ?? '0';
interactable.setAttribute('tabindex', tabIndex);
}
} else {
interactable.setAttribute('data-original-tabindex', interactable.getAttribute('tabindex'));
interactable.removeAttribute('tabindex');
}
});
}
/**
* Initializes the focusability of controls on the given element or the document
*
* @param {Element|Document} [element=document] - The element on which to initialize the interactable state. Defaults to the document.
*/
function initializeInteractables(element = document) {
const interactables = getAllInteractables(element);
makeKeyboardInteractable(...interactables);
}
/**
* Queries all interactables within the given element based on the given selectors and returns them as an array
*
* @param {Element|Document} element - The element within which to query the interactables
* @returns {HTMLElement[]} An array containing all the interactables that match the given selectors
*/
function getAllInteractables(element) {
// Query each selector individually and combine all to a big array to return
return [].concat(...interactableSelectors.map(selector => Array.from(element.querySelectorAll(`${selector}`))));
}
/**
* Function to apply scroll reset behavior to a container
* @param {Element} container - The container
*/
const applyScrollResetBehavior = (container) => {
container.addEventListener('focusout', (e) => {
setTimeout(() => {
const focusedElement = document.activeElement;
if (!container.contains(focusedElement)) {
container.scrollTop = 0;
container.scrollLeft = 0;
}
}, 0);
});
};
/**
* Initializes the scroll reset behavior on the given element or the document
*
* @param {Element|Document} [element=document] - The element on which to initialize the scroll reset behavior. Defaults to the document.
*/
function initializeScrollResetBehaviors(element = document) {
const scrollResetContainers = element.querySelectorAll('.scroll-reset-container');
scrollResetContainers.forEach(container => applyScrollResetBehavior(container));
}
/**
* Handles keydown events on the document to trigger click on Enter key press for interactables
*
* @param {KeyboardEvent} event - The keyboard event
*/
function handleGlobalKeyDown(event) {
if (event.key === 'Enter') {
if (!(event.target instanceof HTMLElement))
return;
// Only count enter on this interactable if no modifier key is pressed
if (event.altKey || event.ctrlKey || event.shiftKey)
return;
// Traverse up the DOM tree to find the actual interactable element
let target = event.target;
while (target && !isKeyboardInteractable(target)) {
target = target.parentElement;
}
// Trigger click if a valid interactable is found and it's not disabled
if (target && !target.classList.contains(DISABLED_CONTROL_CLASS)) {
console.debug('Triggering click on keyboard-focused interactable control via Enter', target);
target.click();
}
}
}
/**
* Initializes several keyboard functionalities for ST
*/
export function initKeyboard() {
// Start observing the body for added elements and attribute changes
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class'],
});
// Initialize already existing controls
initializeInteractables();
initializeScrollResetBehaviors();
// Add a global keydown listener
document.addEventListener('keydown', handleGlobalKeyDown);
}

View File

@ -28,6 +28,7 @@ import {
setOnlineStatus, setOnlineStatus,
startStatusLoading, startStatusLoading,
substituteParams, substituteParams,
substituteParamsExtended,
system_message_types, system_message_types,
this_chid, this_chid,
} from '../script.js'; } from '../script.js';
@ -69,6 +70,8 @@ import { saveLogprobsForActiveMessage } from './logprobs.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
import { renderTemplateAsync } from './templates.js';
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
export { export {
openai_messages_count, openai_messages_count,
@ -197,16 +200,18 @@ const custom_prompt_post_processing_types = {
CLAUDE: 'claude', CLAUDE: 'claude',
}; };
const prefixMap = selected_group ? { function getPrefixMap() {
assistant: '', return selected_group ? {
user: '', assistant: '',
system: 'OOC: ', user: '',
system: 'OOC: ',
}
: {
assistant: '{{char}}:',
user: '{{user}}:',
system: '',
};
} }
: {
assistant: '{{char}}:',
user: '{{user}}:',
system: '',
};
const default_settings = { const default_settings = {
preset_settings_openai: 'Default', preset_settings_openai: 'Default',
@ -722,7 +727,7 @@ function populationInjectionPrompts(prompts, messages) {
const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join(separator); const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join(separator);
if (jointPrompt && jointPrompt.length) { if (jointPrompt && jointPrompt.length) {
roleMessages.push({ 'role': role, 'content': jointPrompt }); roleMessages.push({ 'role': role, 'content': jointPrompt, injected: true });
} }
} }
@ -775,7 +780,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
const promptObject = { const promptObject = {
identifier: 'continueNudge', identifier: 'continueNudge',
role: 'system', role: 'system',
content: oai_settings.continue_nudge_prompt.replace('{{lastChatMessage}}', String(cyclePrompt).trim()), content: substituteParamsExtended(oai_settings.continue_nudge_prompt, { lastChatMessage: String(cyclePrompt).trim() }),
system_prompt: true, system_prompt: true,
}; };
const continuePrompt = new Prompt(promptObject); const continuePrompt = new Prompt(promptObject);
@ -794,6 +799,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
// Insert chat messages as long as there is budget available // Insert chat messages as long as there is budget available
const chatPool = [...messages].reverse(); const chatPool = [...messages].reverse();
const firstNonInjected = chatPool.find(x => !x.injected);
for (let index = 0; index < chatPool.length; index++) { for (let index = 0; index < chatPool.length; index++) {
const chatPrompt = chatPool[index]; const chatPrompt = chatPool[index];
@ -812,6 +818,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
} }
if (chatCompletion.canAfford(chatMessage)) { if (chatCompletion.canAfford(chatMessage)) {
if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
const collection = new MessageCollection('continuePrefill', chatMessage);
chatCompletion.add(collection, -1);
continue;
}
chatCompletion.insertAtStart(chatMessage, 'chatHistory'); chatCompletion.insertAtStart(chatMessage, 'chatHistory');
} else { } else {
break; break;
@ -1700,7 +1712,7 @@ async function sendOpenAIRequest(type, messages, signal) {
if (isAI21) { if (isAI21) {
const joinedMsgs = messages.reduce((acc, obj) => { const joinedMsgs = messages.reduce((acc, obj) => {
const prefix = prefixMap[obj.role]; const prefix = getPrefixMap()[obj.role];
return acc + (prefix ? (selected_group ? '\n' : prefix + ' ') : '') + obj.content + '\n'; return acc + (prefix ? (selected_group ? '\n' : prefix + ' ') : '') + obj.content + '\n';
}, ''); }, '');
messages = substituteParams(joinedMsgs) + (isImpersonate ? `${name1}:` : `${name2}:`); messages = substituteParams(joinedMsgs) + (isImpersonate ? `${name1}:` : `${name2}:`);
@ -3691,8 +3703,8 @@ function onSettingsPresetChange() {
preset.assistant_impersonation = preset.assistant_prefill; preset.assistant_impersonation = preset.assistant_prefill;
} }
const updateInput = (selector, value) => $(selector).val(value).trigger('input'); const updateInput = (selector, value) => $(selector).val(value).trigger('input', { source: 'preset' });
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input'); const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input', { source: 'preset' });
// Allow subscribers to alter the preset before applying deltas // Allow subscribers to alter the preset before applying deltas
eventSource.emit(event_types.OAI_PRESET_CHANGED_BEFORE, { eventSource.emit(event_types.OAI_PRESET_CHANGED_BEFORE, {
@ -4401,23 +4413,8 @@ function updateScaleForm() {
} }
} }
function onCustomizeParametersClick() { async function onCustomizeParametersClick() {
const template = $(` const template = $(await renderTemplateAsync('customEndpointAdditionalParameters'));
<div class="flex-container flexFlowColumn height100p">
<h3>Additional Parameters</h3>
<div class="flex1 flex-container flexFlowColumn">
<h4>Include Body Parameters</h4>
<textarea id="custom_include_body" class="flex1" placeholder="Parameters to be included in the Chat Completion request body (YAML object)&#10;&#10;Example:&#10;- top_k: 20&#10;- repetition_penalty: 1.1"></textarea>
</div>
<div class="flex1 flex-container flexFlowColumn">
<h4>Exclude Body Parameters</h4>
<textarea id="custom_exclude_body" class="flex1" placeholder="Parameters to be excluded from the Chat Completion request body (YAML array)&#10;&#10;Example:&#10;- frequency_penalty&#10;- presence_penalty"></textarea>
</div>
<div class="flex1 flex-container flexFlowColumn">
<h4>Include Request Headers</h4>
<textarea id="custom_include_headers" class="flex1" placeholder="Additional headers for Chat Completion requests (YAML object)&#10;&#10;Example:&#10;- CustomHeader: custom-value&#10;- AnotherHeader: custom-value"></textarea>
</div>
</div>`);
template.find('#custom_include_body').val(oai_settings.custom_include_body).on('input', function () { template.find('#custom_include_body').val(oai_settings.custom_include_body).on('input', function () {
oai_settings.custom_include_body = String($(this).val()); oai_settings.custom_include_body = String($(this).val());
@ -4608,9 +4605,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
returns: 'current proxy', returns: 'current proxy',
namedArgumentList: [], namedArgumentList: [],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'name', [ARGUMENT_TYPE.STRING], true, description: 'name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => proxies.map(preset => new SlashCommandEnumValue(preset.name, preset.url)),
}),
], ],
helpString: 'Sets a proxy preset by name.', helpString: 'Sets a proxy preset by name.',
})); }));
@ -4863,9 +4863,11 @@ $(document).ready(async function () {
eventSource.emit(event_types.CHATCOMPLETION_SOURCE_CHANGED, oai_settings.chat_completion_source); eventSource.emit(event_types.CHATCOMPLETION_SOURCE_CHANGED, oai_settings.chat_completion_source);
}); });
$('#oai_max_context_unlocked').on('input', function () { $('#oai_max_context_unlocked').on('input', function (_e, data) {
oai_settings.max_context_unlocked = !!$(this).prop('checked'); oai_settings.max_context_unlocked = !!$(this).prop('checked');
$('#chat_completion_source').trigger('change'); if (data?.source !== 'preset') {
$('#chat_completion_source').trigger('change');
}
saveSettingsDebounced(); saveSettingsDebounced();
}); });

View File

@ -1,156 +1,290 @@
import { animation_duration, animation_easing } from '../script.js'; import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
import { delay } from './utils.js';
/** @readonly */
/** @enum {Number} */
/**@readonly*/
/**@enum {Number}*/
export const POPUP_TYPE = { export const POPUP_TYPE = {
'TEXT': 1, /** Main popup type. Containing any content displayed, with buttons below. Can also contain additional input controls. */
'CONFIRM': 2, TEXT: 1,
'INPUT': 3, /** Popup mainly made to confirm something, answering with a simple Yes/No or similar. Focus on the button controls. */
CONFIRM: 2,
/** Popup who's main focus is the input text field, which is displayed here. Can contain additional content above. Return value for this is the input string. */
INPUT: 3,
/** Popup without any button controls. Used to simply display content, with a small X in the corner. */
DISPLAY: 4,
}; };
/**@readonly*/ /** @readonly */
/**@enum {Boolean}*/ /** @enum {number?} */
export const POPUP_RESULT = { export const POPUP_RESULT = {
'AFFIRMATIVE': true, AFFIRMATIVE: 1,
'NEGATIVE': false, NEGATIVE: 0,
'CANCELLED': undefined, CANCELLED: null,
}; };
/**
* @typedef {object} PopupOptions
* @property {string|boolean?} [okButton=null] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
* @property {string|boolean?} [cancelButton=null] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
* @property {number?} [rows=1] - The number of rows for the input field
* @property {boolean?} [wide=false] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio)
* @property {boolean?} [wider=false] - Whether to display the popup in wider mode (just wider, no height scaling)
* @property {boolean?} [large=false] - Whether to display the popup in large mode (90% of screen)
* @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
* @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
* @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
* @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
* @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
* @property {(popup: Popup) => boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close
* @property {(popup: Popup) => void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up
*/
/**
* @typedef {object} CustomPopupButton
* @property {string} text - The text of the button
* @property {POPUP_RESULT|number?} result - The result of the button - can also be a custom result value to make be able to find out that this button was clicked. If no result is specified, this button will **not** close the popup.
* @property {string[]|string?} [classes] - Optional custom CSS classes applied to the button
* @property {()=>void?} [action] - Optional action to perform when the button is clicked
* @property {boolean?} [appendAtEnd] - Whether to append the button to the end of the popup - by default it will be prepended
*/
/**
* @typedef {object} ShowPopupHelper
* Local implementation of the helper functionality to show several popups.
*
* Should be called via `Popup.show.xxxx()`.
*/
const showPopupHelper = {
/**
* Asynchronously displays an input popup with the given header and text, and returns the user's input.
*
* @param {string} header - The header text for the popup.
* @param {string} text - The main text for the popup.
* @param {string} [defaultValue=''] - The default value for the input field.
* @param {PopupOptions} [popupOptions={}] - Options for the popup.
* @return {Promise<string?>} A Promise that resolves with the user's input.
*/
input: async (header, text, defaultValue = '', popupOptions = {}) => {
const content = PopupUtils.BuildTextWithHeader(header, text);
const popup = new Popup(content, POPUP_TYPE.INPUT, defaultValue, popupOptions);
const value = await popup.show();
return value ? String(value) : null;
},
}
export class Popup { export class Popup {
/**@type {POPUP_TYPE}*/ type; /** @type {POPUP_TYPE} */ type;
/**@type {HTMLElement}*/ dom; /** @type {string} */ id;
/**@type {HTMLElement}*/ dlg;
/**@type {HTMLElement}*/ text;
/**@type {HTMLTextAreaElement}*/ input;
/**@type {HTMLElement}*/ ok;
/**@type {HTMLElement}*/ cancel;
/**@type {POPUP_RESULT}*/ result; /** @type {HTMLDialogElement} */ dlg;
/**@type {any}*/ value; /** @type {HTMLElement} */ body;
/** @type {HTMLElement} */ content;
/** @type {HTMLTextAreaElement} */ input;
/** @type {HTMLElement} */ controls;
/** @type {HTMLElement} */ okButton;
/** @type {HTMLElement} */ cancelButton;
/** @type {HTMLElement} */ closeButton;
/** @type {POPUP_RESULT|number?} */ defaultResult;
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
/**@type {Promise}*/ promise; /** @type {(popup: Popup) => boolean?} */ onClosing;
/**@type {Function}*/ resolver; /** @type {(popup: Popup) => void?} */ onClose;
/**@type {Function}*/ keyListenerBound; /** @type {POPUP_RESULT|number} */ result;
/** @type {any} */ value;
/** @type {HTMLElement} */ lastFocus;
/** @type {Promise<any>} */ promise;
/** @type {(result: any) => any} */ resolver;
/** /**
* @typedef {{okButton?: string, cancelButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup. * Constructs a new Popup object with the given text content, type, inputValue, and options
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup. *
* @param {POPUP_TYPE} type - One of Popup.TYPE * @param {JQuery<HTMLElement>|string|Element} content - Text content to display in the popup
* @param {string} inputValue - Value to set the input to. * @param {POPUP_TYPE} type - The type of the popup
* @param {PopupOptions} options - Options for the popup. * @param {string} [inputValue=''] - The initial value of the input field
* @param {PopupOptions} [options={}] - Additional options for the popup
*/ */
constructor(text, type, inputValue = '', { okButton, cancelButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null } = {}) {
Popup.util.popups.push(this);
// Make this popup uniquely identifiable
this.id = uuidv4();
this.type = type; this.type = type;
// Utilize event handlers being passed in
this.onClosing = onClosing;
this.onClose = onClose;
/**@type {HTMLTemplateElement}*/ /**@type {HTMLTemplateElement}*/
const template = document.querySelector('#shadow_popup_template'); const template = document.querySelector('#popup_template');
// @ts-ignore // @ts-ignore
this.dom = template.content.cloneNode(true).querySelector('.shadow_popup'); this.dlg = template.content.cloneNode(true).querySelector('.popup');
const dlg = this.dom.querySelector('.dialogue_popup'); this.body = this.dlg.querySelector('.popup-body');
// @ts-ignore this.content = this.dlg.querySelector('.popup-content');
this.dlg = dlg; this.input = this.dlg.querySelector('.popup-input');
this.text = this.dom.querySelector('.dialogue_popup_text'); this.controls = this.dlg.querySelector('.popup-controls');
this.input = this.dom.querySelector('.dialogue_popup_input'); this.okButton = this.dlg.querySelector('.popup-button-ok');
this.ok = this.dom.querySelector('.dialogue_popup_ok'); this.cancelButton = this.dlg.querySelector('.popup-button-cancel');
this.cancel = this.dom.querySelector('.dialogue_popup_cancel'); this.closeButton = this.dlg.querySelector('.popup-button-close');
if (wide) dlg.classList.add('wide_dialogue_popup'); this.dlg.setAttribute('data-id', this.id);
if (wider) dlg.classList.add('wider_dialogue_popup'); if (wide) this.dlg.classList.add('wide_dialogue_popup');
if (large) dlg.classList.add('large_dialogue_popup'); if (wider) this.dlg.classList.add('wider_dialogue_popup');
if (allowHorizontalScrolling) dlg.classList.add('horizontal_scrolling_dialogue_popup'); if (large) this.dlg.classList.add('large_dialogue_popup');
if (allowVerticalScrolling) dlg.classList.add('vertical_scrolling_dialogue_popup'); if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
this.ok.textContent = okButton ?? 'OK'; // If custom button captions are provided, we set them beforehand
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_cancel'); this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK';
this.cancelButton.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel');
this.defaultResult = defaultResult;
this.customButtons = customButtons;
this.customButtons?.forEach((x, index) => {
/** @type {CustomPopupButton} */
const button = typeof x === 'string' ? { text: x, result: index + 2 } : x;
const buttonElement = document.createElement('div');
buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control');
buttonElement.classList.add(...(button.classes ?? []));
buttonElement.dataset.result = String(button.result ?? undefined);
buttonElement.textContent = button.text;
buttonElement.tabIndex = 0;
if (button.appendAtEnd) {
this.controls.appendChild(buttonElement);
} else {
this.controls.insertBefore(buttonElement, this.okButton);
}
});
// Set the default button class
const defaultButton = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
if (defaultButton) defaultButton.classList.add('menu_button_default');
// Styling differences depending on the popup type
// General styling for all types first, that might be overriden for specific types below
this.input.style.display = 'none';
this.closeButton.style.display = 'none';
switch (type) { switch (type) {
case POPUP_TYPE.TEXT: { case POPUP_TYPE.TEXT: {
this.input.style.display = 'none'; if (!cancelButton) this.cancelButton.style.display = 'none';
this.cancel.style.display = 'none';
break; break;
} }
case POPUP_TYPE.CONFIRM: { case POPUP_TYPE.CONFIRM: {
this.input.style.display = 'none'; if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-yes');
this.ok.textContent = okButton ?? template.getAttribute('popup_text_yes'); if (!cancelButton) this.cancelButton.textContent = template.getAttribute('popup-button-no');
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_no');
break; break;
} }
case POPUP_TYPE.INPUT: { case POPUP_TYPE.INPUT: {
this.input.style.display = 'block'; this.input.style.display = 'block';
this.ok.textContent = okButton ?? template.getAttribute('popup_text_save'); if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-save');
break; break;
} }
case POPUP_TYPE.DISPLAY: {
this.controls.style.display = 'none';
this.closeButton.style.display = 'block';
}
default: { default: {
// illegal argument console.warn('Unknown popup type.', type);
break;
} }
} }
this.input.value = inputValue; this.input.value = inputValue;
this.input.rows = rows ?? 1; this.input.rows = rows ?? 1;
this.text.innerHTML = ''; this.content.innerHTML = '';
if (text instanceof jQuery) { if (content instanceof jQuery) {
$(this.text).append(text); $(this.content).append(content);
} else if (text instanceof HTMLElement) { } else if (content instanceof HTMLElement) {
this.text.append(text); this.content.append(content);
} else if (typeof text == 'string') { } else if (typeof content == 'string') {
this.text.innerHTML = text; this.content.innerHTML = content;
} else { } else {
// illegal argument console.warn('Unknown popup text type. Should be jQuery, HTMLElement or string.', content);
} }
this.input.addEventListener('keydown', (evt) => { // Already prepare the auto-focus control by adding the "autofocus" attribute, this should be respected by showModal()
if (evt.key != 'Enter' || evt.altKey || evt.ctrlKey || evt.shiftKey) return; this.setAutoFocus({ applyAutoFocus: true });
// Set focus event that remembers the focused element
this.dlg.addEventListener('focusin', (evt) => { if (evt.target instanceof HTMLElement && evt.target != this.dlg) this.lastFocus = evt.target; });
// Bind event listeners for all result controls to their defined event type
this.dlg.querySelectorAll(`[data-result]`).forEach(resultControl => {
if (!(resultControl instanceof HTMLElement)) return;
const result = Number(resultControl.dataset.result);
if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result);
const type = resultControl.dataset.resultEvent || 'click';
resultControl.addEventListener(type, () => this.complete(result));
});
// Bind dialog listeners manually, so we can be sure context is preserved
const cancelListener = (evt) => {
this.complete(POPUP_RESULT.CANCELLED);
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
this.completeAffirmative(); window.removeEventListener('cancel', cancelListenerBound);
}); };
const cancelListenerBound = cancelListener.bind(this);
this.dlg.addEventListener('cancel', cancelListenerBound);
this.ok.addEventListener('click', () => this.completeAffirmative());
this.cancel.addEventListener('click', () => this.completeNegative());
const keyListener = (evt) => { const keyListener = (evt) => {
switch (evt.key) { switch (evt.key) {
case 'Escape': { case 'Enter': {
// does it really matter where we check? // CTRL+Enter counts as a closing action, but all other modifiers (ALT, SHIFT) should not trigger this
const topModal = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2)?.closest('.shadow_popup'); if (evt.altKey || evt.shiftKey)
if (topModal == this.dom) { return;
evt.preventDefault();
evt.stopPropagation(); // Check if we are the currently active popup
this.completeCancelled(); if (this.dlg != document.activeElement?.closest('.popup'))
window.removeEventListener('keydown', keyListenerBound); return;
break;
} // Check if the current focus is a result control. Only should we apply the compelete action
const resultControl = document.activeElement?.closest('.result-control');
if (!resultControl)
return;
const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult);
this.complete(result);
evt.preventDefault();
evt.stopPropagation();
window.removeEventListener('keydown', keyListenerBound);
break;
} }
} }
}; };
const keyListenerBound = keyListener.bind(this); const keyListenerBound = keyListener.bind(this);
window.addEventListener('keydown', keyListenerBound); this.dlg.addEventListener('keydown', keyListenerBound);
} }
/**
* Asynchronously shows the popup element by appending it to the document body,
* setting its display to 'block' and focusing on the input if the popup type is INPUT.
*
* @returns {Promise<string|number|boolean?>} A promise that resolves with the value of the popup when it is completed.
*/
async show() { async show() {
document.body.append(this.dom); document.body.append(this.dlg);
this.dom.style.display = 'block';
switch (this.type) {
case POPUP_TYPE.INPUT: {
this.input.focus();
break;
}
}
$(this.dom).transition({ // Run opening animation
opacity: 1, this.dlg.setAttribute('opening', '');
duration: animation_duration,
easing: animation_easing, this.dlg.showModal();
});
// We need to fix the toastr to be present inside this dialog
fixToastrForDialogs();
runAfterAnimation(this.dlg, () => {
this.dlg.removeAttribute('opening');
})
this.promise = new Promise((resolve) => { this.promise = new Promise((resolve) => {
this.resolver = resolve; this.resolver = resolve;
@ -158,80 +292,215 @@ export class Popup {
return this.promise; return this.promise;
} }
completeAffirmative() { setAutoFocus({ applyAutoFocus = false } = {}) {
switch (this.type) { /** @type {HTMLElement} */
case POPUP_TYPE.TEXT: let control;
case POPUP_TYPE.CONFIRM: {
this.value = true; // Try to find if we have an autofocus control already present
break; control = this.dlg.querySelector('[autofocus]');
}
case POPUP_TYPE.INPUT: { // If not, find the default control for this popup type
this.value = this.input.value; if (!control) {
break; switch (this.type) {
case POPUP_TYPE.INPUT: {
control = this.input;
break;
}
default:
// Select default button
control = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
break;
} }
} }
this.result = POPUP_RESULT.AFFIRMATIVE;
if (applyAutoFocus) {
control.setAttribute('autofocus', '');
// Manually enable tabindex too, as this might only be applied by the interactable functionality in the background, but too late for HTML autofocus
// interactable only gets applied when inserted into the DOM
control.tabIndex = 0;
} else {
control.focus();
}
}
/**
* Completes the popup and sets its result and value
*
* The completion handling will make the popup return the result to the original show promise.
*
* There will be two different types of result values:
* - popup with `POPUP_TYPE.INPUT` will return the input value - or `false` on negative and `null` on cancelled
* - All other will return the result value as provided as `POPUP_RESULT` or a custom number value
*
* @param {POPUP_RESULT|number} result - The result of the popup (either an existing `POPUP_RESULT` or a custom result value)
*/
complete(result) {
// In all cases besides INPUT the popup value should be the result
/** @type {POPUP_RESULT|number|boolean|string?} */
let value = result;
// Input type have special results, so the input can be accessed directly without the need to save the popup and access both result and value
if (this.type === POPUP_TYPE.INPUT) {
if (result >= POPUP_RESULT.AFFIRMATIVE) value = this.input.value;
else if (result === POPUP_RESULT.NEGATIVE) value = false;
else if (result === POPUP_RESULT.CANCELLED) value = null;
else value = false; // Might a custom negative value?
}
this.value = value;
this.result = result;
if (this.onClosing) {
const shouldClose = this.onClosing(this);
if (!shouldClose) return;
}
Popup.util.lastResult = { value, result };
this.hide(); this.hide();
} }
completeNegative() { /**
switch (this.type) { * Hides the popup, using the internal resolver to return the value to the original show promise
case POPUP_TYPE.TEXT: * @private
case POPUP_TYPE.CONFIRM: */
case POPUP_TYPE.INPUT: {
this.value = false;
break;
}
}
this.result = POPUP_RESULT.NEGATIVE;
this.hide();
}
completeCancelled() {
switch (this.type) {
case POPUP_TYPE.TEXT:
case POPUP_TYPE.CONFIRM:
case POPUP_TYPE.INPUT: {
this.value = null;
break;
}
}
this.result = POPUP_RESULT.CANCELLED;
this.hide();
}
hide() { hide() {
$(this.dom).transition({ // We close the dialog, first running the animation
opacity: 0, this.dlg.setAttribute('closing', '');
duration: animation_duration,
easing: animation_easing, // Once the hiding starts, we need to fix the toastr to the layer below
}); fixToastrForDialogs();
delay(animation_duration).then(() => {
this.dom.remove(); // After the dialog is actually completely closed, remove it from the DOM
runAfterAnimation(this.dlg, () => {
// Call the close on the dialog
this.dlg.close();
// Run a possible custom handler right before DOM removal
if (this.onClose) {
this.onClose(this);
}
// Remove it from the dom
this.dlg.remove();
// Remove it from the popup references
removeFromArray(Popup.util.popups, this);
// If there is any popup below this one, see if we can set the focus
if (Popup.util.popups.length > 0) {
const activeDialog = document.activeElement?.closest('.popup');
const id = activeDialog?.getAttribute('data-id');
const popup = Popup.util.popups.find(x => x.id == id);
if (popup) {
if (popup.lastFocus) popup.lastFocus.focus();
else popup.setAutoFocus();
}
}
}); });
this.resolver(this.value); this.resolver(this.value);
} }
/**
* Show a popup with any of the given helper methods. Use `await` to make them blocking.
*/
static show = showPopupHelper;
/**
* Utility for popup and popup management.
*
* Contains the list of all currently open popups, and it'll remember the result of the last closed popup.
*/
static util = {
/** @type {Popup[]} Remember all popups */
popups: [],
/** @type {{value: any, result: POPUP_RESULT|number?}?} Last popup result */
lastResult: null,
/** @returns {boolean} Checks if any modal popup dialog is open */
isPopupOpen() {
return Popup.util.popups.length > 0;
},
/**
* Returns the topmost modal layer in the document. If there is an open dialog popup,
* it returns the dialog element. Otherwise, it returns the document body.
*
* @return {HTMLElement} The topmost modal layer element
*/
getTopmostModalLayer() {
return getTopmostModalLayer();
},
}
} }
class PopupUtils {
static BuildTextWithHeader(header, text) {
return `
<h3>${header}</h1>
${text}`;
}
}
/** /**
* Displays a blocking popup with a given text and type. * Displays a blocking popup with a given content and type
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup. *
* @param {JQuery<HTMLElement>|string|Element} content - Content or text to display in the popup
* @param {POPUP_TYPE} type * @param {POPUP_TYPE} type
* @param {string} inputValue - Value to set the input to. * @param {string} inputValue - Value to set the input to
* @param {PopupOptions} options - Options for the popup. * @param {PopupOptions} [popupOptions={}] - Options for the popup
* @returns * @returns {Promise<POPUP_RESULT|string|boolean?>} The value for this popup, which can either be the popup retult or the input value if chosen
*/ */
export function callGenericPopup(text, type, inputValue = '', { okButton, cancelButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { export function callGenericPopup(content, type, inputValue = '', popupOptions = {}) {
const popup = new Popup( const popup = new Popup(
text, content,
type, type,
inputValue, inputValue,
{ okButton, cancelButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling }, popupOptions,
); );
return popup.show(); return popup.show();
} }
/**
* Returns the topmost modal layer in the document. If there is an open dialog,
* it returns the dialog element. Otherwise, it returns the document body.
*
* @return {HTMLElement} The topmost modal layer element
*/
export function getTopmostModalLayer() {
const dlg = Array.from(document.querySelectorAll('dialog[open]:not([closing])')).pop();
if (dlg instanceof HTMLElement) return dlg;
return document.body;
}
/**
* Fixes the issue with toastr not displaying on top of the dialog by moving the toastr container inside the dialog or back to the main body
*/
export function fixToastrForDialogs() {
// Hacky way of getting toastr to actually display on top of the popup...
const dlg = Array.from(document.querySelectorAll('dialog[open]:not([closing])')).pop();
let toastContainer = document.getElementById('toast-container');
const isAlreadyPresent = !!toastContainer;
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.setAttribute('id', 'toast-container');
if (toastr.options.positionClass) toastContainer.classList.add(toastr.options.positionClass);
}
// Check if toastr is already a child. If not, we need to move it inside this dialog.
// This is either the existing toastr container or the newly created one.
if (dlg && !dlg.contains(toastContainer)) {
dlg?.appendChild(toastContainer);
return;
}
// Now another case is if we only have one popup and that is currently closing. In that case the toastr container exists,
// but we don't have an open dialog to move it into. It's just inside the existing one that will be gone in milliseconds.
// To prevent new toasts from being showing up in there and then vanish in an instant,
// we move the toastr back to the main body
if (!dlg && isAlreadyPresent) {
document.body.appendChild(toastContainer);
}
}

View File

@ -35,7 +35,7 @@ import {
selectInstructPreset, selectInstructPreset,
} from './instruct-mode.js'; } from './instruct-mode.js';
import { getTagsList, tag_map, tags } from './tags.js'; import { getTagsList, tag_import_setting, tag_map, tags } from './tags.js';
import { tokenizers } from './tokenizers.js'; import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js'; import { BIAS_CACHE } from './logit-bias.js';
import { renderTemplateAsync } from './templates.js'; import { renderTemplateAsync } from './templates.js';
@ -46,6 +46,8 @@ import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandPa
import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js'; import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
export { export {
loadPowerUserSettings, loadPowerUserSettings,
@ -197,6 +199,7 @@ let power_user = {
trim_spaces: true, trim_spaces: true,
relaxed_api_urls: false, relaxed_api_urls: false,
world_import_dialog: true, world_import_dialog: true,
tag_import_setting: tag_import_setting.ASK,
disable_group_trimming: false, disable_group_trimming: false,
single_line: false, single_line: false,
@ -975,6 +978,7 @@ function switchUiMode() {
function toggleWaifu() { function toggleWaifu() {
$('#waifuMode').trigger('click'); $('#waifuMode').trigger('click');
return '';
} }
function switchWaifuMode() { function switchWaifuMode() {
@ -1560,6 +1564,12 @@ function loadPowerUserSettings(settings, data) {
power_user.tokenizer = tokenizers.GPT2; power_user.tokenizer = tokenizers.GPT2;
} }
// Clean up old/legacy settings
if (power_user.import_card_tags !== undefined) {
power_user.tag_import_setting = power_user.import_card_tags ? tag_import_setting.ASK : tag_import_setting.NONE;
delete power_user.import_card_tags;
}
$('#single_line').prop('checked', power_user.single_line); $('#single_line').prop('checked', power_user.single_line);
$('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls); $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
$('#world_import_dialog').prop('checked', power_user.world_import_dialog); $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
@ -1588,7 +1598,6 @@ function loadPowerUserSettings(settings, data) {
$('#zoomed_avatar_magnification').prop('checked', power_user.zoomed_avatar_magnification); $('#zoomed_avatar_magnification').prop('checked', power_user.zoomed_avatar_magnification);
$(`#tokenizer option[value="${power_user.tokenizer}"]`).attr('selected', true); $(`#tokenizer option[value="${power_user.tokenizer}"]`).attr('selected', true);
$(`#send_on_enter option[value=${power_user.send_on_enter}]`).attr('selected', true); $(`#send_on_enter option[value=${power_user.send_on_enter}]`).attr('selected', true);
$('#import_card_tags').prop('checked', power_user.import_card_tags);
$('#confirm_message_delete').prop('checked', power_user.confirm_message_delete !== undefined ? !!power_user.confirm_message_delete : true); $('#confirm_message_delete').prop('checked', power_user.confirm_message_delete !== undefined ? !!power_user.confirm_message_delete : true);
$('#spoiler_free_mode').prop('checked', power_user.spoiler_free_mode); $('#spoiler_free_mode').prop('checked', power_user.spoiler_free_mode);
$('#collapse-newlines-checkbox').prop('checked', power_user.collapse_newlines); $('#collapse-newlines-checkbox').prop('checked', power_user.collapse_newlines);
@ -1630,6 +1639,7 @@ function loadPowerUserSettings(settings, data) {
$('#chat_width_slider').val(power_user.chat_width); $('#chat_width_slider').val(power_user.chat_width);
$('#token_padding').val(power_user.token_padding); $('#token_padding').val(power_user.token_padding);
$('#aux_field').val(power_user.aux_field); $('#aux_field').val(power_user.aux_field);
$('#tag_import_setting').val(power_user.tag_import_setting);
$('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input'); $('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy'); $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
@ -2260,22 +2270,23 @@ async function importTheme(file) {
} }
themes.push(parsed); themes.push(parsed);
await applyTheme(parsed.name); await saveTheme(parsed.name, getNewTheme(parsed));
await saveTheme(parsed.name);
const option = document.createElement('option'); const option = document.createElement('option');
option.selected = true; option.selected = false;
option.value = parsed.name; option.value = parsed.name;
option.innerText = parsed.name; option.innerText = parsed.name;
$('#themes').append(option); $('#themes').append(option);
saveSettingsDebounced(); saveSettingsDebounced();
toastr.success(parsed.name, 'Theme imported');
} }
/** /**
* Saves the current theme to the server. * Saves the current theme to the server.
* @param {string|undefined} name Theme name. If undefined, a popup will be shown to enter a name. * @param {string|undefined} name Theme name. If undefined, a popup will be shown to enter a name.
* @param {object|undefined} theme Theme object. If undefined, the current theme will be saved.
* @returns {Promise<object>} A promise that resolves when the theme is saved. * @returns {Promise<object>} A promise that resolves when the theme is saved.
*/ */
async function saveTheme(name = undefined) { async function saveTheme(name = undefined, theme = undefined) {
if (typeof name !== 'string') { if (typeof name !== 'string') {
name = await callPopup('Enter a theme preset name:', 'input', power_user.theme); name = await callPopup('Enter a theme preset name:', 'input', power_user.theme);
@ -2284,7 +2295,46 @@ async function saveTheme(name = undefined) {
} }
} }
const theme = { if (typeof theme !== 'object') {
theme = getThemeObject(name);
}
const response = await fetch('/api/themes/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(theme),
});
if (response.ok) {
const themeIndex = themes.findIndex(x => x.name == name);
if (themeIndex == -1) {
themes.push(theme);
const option = document.createElement('option');
option.selected = true;
option.value = name;
option.innerText = name;
$('#themes').append(option);
}
else {
themes[themeIndex] = theme;
$(`#themes option[value="${name}"]`).attr('selected', true);
}
power_user.theme = name;
saveSettingsDebounced();
}
return theme;
}
/**
* Gets a snapshot of the current theme settings.
* @param {string} name Name of the theme
* @returns {object} Theme object
*/
function getThemeObject(name) {
return {
name, name,
blur_strength: power_user.blur_strength, blur_strength: power_user.blur_strength,
main_text_color: power_user.main_text_color, main_text_color: power_user.main_text_color,
@ -2322,33 +2372,20 @@ async function saveTheme(name = undefined) {
reduced_motion: power_user.reduced_motion, reduced_motion: power_user.reduced_motion,
compact_input_area: power_user.compact_input_area, compact_input_area: power_user.compact_input_area,
}; };
}
const response = await fetch('/api/themes/save', { /**
method: 'POST', * Applies imported theme properties to the theme object.
headers: getRequestHeaders(), * @param {object} parsed Parsed object to get the theme from.
body: JSON.stringify(theme), * @returns {object} Theme assigned to the parsed object.
}); */
function getNewTheme(parsed) {
if (response.ok) { const theme = getThemeObject(parsed.name);
const themeIndex = themes.findIndex(x => x.name == name); for (const key in parsed) {
if (Object.hasOwn(theme, key)) {
if (themeIndex == -1) { theme[key] = parsed[key];
themes.push(theme);
const option = document.createElement('option');
option.selected = true;
option.value = name;
option.innerText = name;
$('#themes').append(option);
} }
else {
themes[themeIndex] = theme;
$(`#themes option[value="${name}"]`).attr('selected', true);
}
power_user.theme = name;
saveSettingsDebounced();
} }
return theme; return theme;
} }
@ -2483,14 +2520,12 @@ async function resetMovablePanels(type) {
}); });
} }
function doNewChat() { async function doNewChat() {
setTimeout(() => { $('#option_start_new_chat').trigger('click');
$('#option_start_new_chat').trigger('click'); await delay(1);
}, 1); $('#dialogue_popup_ok').trigger('click');
//$("#dialogue_popup").hide(); await delay(1);
setTimeout(() => { return '';
$('#dialogue_popup_ok').trigger('click');
}, 1);
} }
/** /**
@ -2627,17 +2662,15 @@ async function doMesCut(_, text) {
} }
async function doDelMode(_, text) { async function doDelMode(_, text) {
//first enter delmode
$('#option_delete_mes').trigger('click', { fromSlashCommand: true });
//reject invalid args //reject invalid args
if (text && isNaN(text)) { if (text && isNaN(text)) {
toastr.warning('Must enter a number or nothing.'); toastr.warning('Must enter a number or nothing.');
await delay(300); //unsure why 300 is neccessary here, but any shorter and it wont see the delmode UI return '';
$('#dialogue_del_mes_cancel').trigger('click');
return;
} }
//first enter delmode
$('#option_delete_mes').trigger('click', { fromSlashCommand: true });
//parse valid args //parse valid args
if (text) { if (text) {
await delay(300); //same as above, need event signal for 'entered del mode' await delay(300); //same as above, need event signal for 'entered del mode'
@ -2648,16 +2681,16 @@ async function doDelMode(_, text) {
if (oldestMesIDToDel < 0) { if (oldestMesIDToDel < 0) {
toastr.warning(`Cannot delete more than ${chat.length} messages.`); toastr.warning(`Cannot delete more than ${chat.length} messages.`);
return; return '';
} }
let oldestMesToDel = $('#chat').find(`.mes[mesid=${oldestMesIDToDel}]`); let oldestMesToDel = $('#chat').find(`.mes[mesid=${oldestMesIDToDel}]`);
if (!oldestMesIDToDel) { if (!oldestMesIDToDel && lastMesID > 0) {
oldestMesToDel = await loadUntilMesId(oldestMesIDToDel); oldestMesToDel = await loadUntilMesId(oldestMesIDToDel);
if (!oldestMesToDel || !oldestMesToDel.length) { if (!oldestMesToDel || !oldestMesToDel.length) {
return; return '';
} }
} }
@ -2670,12 +2703,15 @@ async function doDelMode(_, text) {
//await delay(1) //await delay(1)
$('#dialogue_del_mes_ok').trigger('click'); $('#dialogue_del_mes_ok').trigger('click');
toastr.success(`Deleted ${trueNumberOfDeletedMessage} messages.`); toastr.success(`Deleted ${trueNumberOfDeletedMessage} messages.`);
return; return '';
} }
return '';
} }
function doResetPanels() { function doResetPanels() {
$('#movingUIreset').trigger('click'); $('#movingUIreset').trigger('click');
return '';
} }
function setAvgBG() { function setAvgBG() {
@ -2882,7 +2918,7 @@ function setAvgBG() {
return `rgba(${rNew.toFixed(0)}, ${gNew.toFixed(0)}, ${bNew.toFixed(0)}, 1)`; return `rgba(${rNew.toFixed(0)}, ${gNew.toFixed(0)}, ${bNew.toFixed(0)}, 1)`;
} }
return '';
} }
async function setThemeCallback(_, text) { async function setThemeCallback(_, text) {
@ -2906,6 +2942,7 @@ async function setThemeCallback(_, text) {
applyTheme(theme.name); applyTheme(theme.name);
$('#themes').val(theme.name); $('#themes').val(theme.name);
saveSettingsDebounced(); saveSettingsDebounced();
return '';
} }
async function setmovingUIPreset(_, text) { async function setmovingUIPreset(_, text) {
@ -2929,6 +2966,7 @@ async function setmovingUIPreset(_, text) {
applyMovingUIPreset(preset.name); applyMovingUIPreset(preset.name);
$('#movingUIPresets').val(preset.name); $('#movingUIPresets').val(preset.name);
saveSettingsDebounced(); saveSettingsDebounced();
return '';
} }
const EPHEMERAL_STOPPING_STRINGS = []; const EPHEMERAL_STOPPING_STRINGS = [];
@ -3486,11 +3524,6 @@ $(document).ready(() => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#import_card_tags').on('input', function () {
power_user.import_card_tags = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#confirm_message_delete').on('input', function () { $('#confirm_message_delete').on('input', function () {
power_user.confirm_message_delete = !!$(this).prop('checked'); power_user.confirm_message_delete = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
@ -3729,6 +3762,12 @@ $(document).ready(() => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#tag_import_setting').on('change', function () {
const value = $(this).find(':selected').val();
power_user.tag_import_setting = Number(value);
saveSettingsDebounced();
});
$('#stscript_autocomplete_autoHide').on('input', function () { $('#stscript_autocomplete_autoHide').on('input', function () {
power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked'); power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();
@ -3884,9 +3923,11 @@ $(document).ready(() => {
name: 'random', name: 'random',
callback: doRandomChat, callback: doRandomChat,
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'optional tag name', [ARGUMENT_TYPE.STRING], false, description: 'optional tag name',
), typeList: [ARGUMENT_TYPE.STRING],
enumProvider: () => tags.filter(tag => Object.values(tag_map).some(x => x.includes(tag.id))).map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.enum, enumIcons.tag)),
}),
], ],
helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.', helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
})); }));
@ -3906,9 +3947,13 @@ $(document).ready(() => {
callback: doMesCut, callback: doMesCut,
returns: 'the text of cut messages separated by a newline', returns: 'the text of cut messages separated by a newline',
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'number or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], true, description: 'number or range',
), typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
isRequired: true,
acceptsMultiple: true,
enumProvider: commonEnumProviders.messages(),
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -3943,9 +3988,12 @@ $(document).ready(() => {
name: 'theme', name: 'theme',
callback: setThemeCallback, callback: setThemeCallback,
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'name', [ARGUMENT_TYPE.STRING], true, description: 'name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => themes.map(theme => new SlashCommandEnumValue(theme.name)),
}),
], ],
helpString: 'sets a UI theme by name', helpString: 'sets a UI theme by name',
})); }));
@ -3953,9 +4001,12 @@ $(document).ready(() => {
name: 'movingui', name: 'movingui',
callback: setmovingUIPreset, callback: setmovingUIPreset,
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'name', [ARGUMENT_TYPE.STRING], true, description: 'name',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => movingUIPresets.map(preset => new SlashCommandEnumValue(preset.name)),
}),
], ],
helpString: 'activates a movingUI preset by name', helpString: 'activates a movingUI preset by name',
})); }));

View File

@ -22,6 +22,8 @@ import { kai_settings } from './kai-settings.js';
import { context_presets, getContextSettings, power_user } from './power-user.js'; import { context_presets, getContextSettings, power_user } from './power-user.js';
import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { import {
textgenerationwebui_preset_names, textgenerationwebui_preset_names,
@ -140,7 +142,10 @@ class PresetManager {
* @param {string} value Preset option value * @param {string} value Preset option value
*/ */
selectPreset(value) { selectPreset(value) {
$(this.select).find(`option[value=${value}]`).prop('selected', true); const option = $(this.select).filter(function() {
return $(this).val() === value;
});
option.prop('selected', true);
$(this.select).val(value).trigger('change'); $(this.select).val(value).trigger('change');
} }
@ -479,11 +484,12 @@ export async function initPresetManager() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'preset', SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'preset',
callback: presetCommandCallback, callback: presetCommandCallback,
returns: 'current preset', returns: 'current preset',
namedArgumentList: [],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'name', [ARGUMENT_TYPE.STRING], false, description: 'name',
), typeList: [ARGUMENT_TYPE.STRING],
enumProvider: () => getPresetManager().getAllPresets().map(preset => new SlashCommandEnumValue(preset, null, enumTypes.enum, enumIcons.preset)),
}),
], ],
helpString: ` helpString: `
<div> <div>

View File

@ -185,7 +185,7 @@ class WebScraper {
const files = []; const files = [];
for (const link of links) { for (const link of links) {
const result = await fetch('/api/serpapi/visit', { const result = await fetch('/api/search/visit', {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders(),
body: JSON.stringify({ url: link }), body: JSON.stringify({ url: link }),
@ -433,6 +433,24 @@ class FandomScraper {
} }
} }
const iso6391Codes = [
'aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az',
'ba', 'be', 'bg', 'bh', 'bi', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce',
'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv', 'dz', 'ee',
'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr',
'fy', 'ga', 'gd', 'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr',
'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig', 'ii', 'ik', 'io', 'is',
'it', 'iu', 'ja', 'jv', 'ka', 'kg', 'ki', 'kj', 'kk', 'kl', 'km', 'kn',
'ko', 'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lg', 'li', 'ln',
'lo', 'lt', 'lu', 'lv', 'mg', 'mh', 'mi', 'mk', 'ml', 'mn', 'mr', 'ms',
'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng', 'nl', 'nn', 'no', 'nr', 'nv',
'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa', 'pi', 'pl', 'ps', 'pt', 'qu',
'rm', 'rn', 'ro', 'ru', 'rw', 'sa', 'sc', 'sd', 'se', 'sg', 'si', 'sk',
'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st', 'su', 'sv', 'sw', 'ta',
'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw',
'ty', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi',
'yo', 'za', 'zh', 'zu'];
/** /**
* Scrape transcript from a YouTube video. * Scrape transcript from a YouTube video.
* @implements {Scraper} * @implements {Scraper}
@ -464,7 +482,7 @@ class YouTubeScraper {
helpString: 'Scrape a transcript from a YouTube video by ID or URL.', helpString: 'Scrape a transcript from a YouTube video by ID or URL.',
returns: ARGUMENT_TYPE.STRING, returns: ARGUMENT_TYPE.STRING,
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument('lang', 'ISO 639-1 language code of the transcript, e.g. "en"', ARGUMENT_TYPE.STRING, false, false, ''), new SlashCommandNamedArgument('lang', 'ISO 639-1 language code of the transcript, e.g. "en"', ARGUMENT_TYPE.STRING, false, false, '', iso6391Codes),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument('URL or ID of the YouTube video', ARGUMENT_TYPE.STRING, true, false), new SlashCommandArgument('URL or ID of the YouTube video', ARGUMENT_TYPE.STRING, true, false),
@ -514,7 +532,7 @@ class YouTubeScraper {
} }
const toast = toastr.info('Working, please wait...'); const toast = toastr.info('Working, please wait...');
const { transcript, id } = await this.getScript(videoUrl, lang); const { transcript, id } = await this.getScript(String(videoUrl), lang);
toastr.clear(toast); toastr.clear(toast);
const file = new File([transcript], `YouTube - ${id} - ${Date.now()}.txt`, { type: 'text/plain' }); const file = new File([transcript], `YouTube - ${id} - ${Date.now()}.txt`, { type: 'text/plain' });
@ -530,7 +548,7 @@ class YouTubeScraper {
async getScript(videoUrl, lang) { async getScript(videoUrl, lang) {
const id = this.parseId(String(videoUrl).trim()); const id = this.parseId(String(videoUrl).trim());
const result = await fetch('/api/serpapi/transcript', { const result = await fetch('/api/search/transcript', {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders(),
body: JSON.stringify({ id, lang }), body: JSON.stringify({ id, lang }),

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,10 @@ import { SlashCommandScope } from './SlashCommandScope.js';
/** /**
* @typedef {{ * @typedef {{
* _pipe:string|SlashCommandClosure,
* _scope:SlashCommandScope, * _scope:SlashCommandScope,
* _parserFlags:{[id:PARSER_FLAG]:boolean}, * _parserFlags:{[id:PARSER_FLAG]:boolean},
* _abortController:SlashCommandAbortController, * _abortController:SlashCommandAbortController,
* _hasUnnamedArgument:boolean,
* [id:string]:string|SlashCommandClosure, * [id:string]:string|SlashCommandClosure,
* }} NamedArguments * }} NamedArguments
*/ */
@ -33,7 +33,7 @@ export class SlashCommand {
* Creates a SlashCommand from a properties object. * Creates a SlashCommand from a properties object.
* @param {Object} props * @param {Object} props
* @param {string} [props.name] * @param {string} [props.name]
* @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback] * @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} [props.callback]
* @param {string} [props.helpString] * @param {string} [props.helpString]
* @param {boolean} [props.splitUnnamedArgument] * @param {boolean} [props.splitUnnamedArgument]
* @param {string[]} [props.aliases] * @param {string[]} [props.aliases]
@ -175,6 +175,11 @@ export class SlashCommand {
} }
li.append(specs); li.append(specs);
} }
const stopgap = document.createElement('span'); {
stopgap.classList.add('stopgap');
stopgap.textContent = '';
li.append(stopgap);
}
const help = document.createElement('span'); { const help = document.createElement('span'); {
help.classList.add('help'); help.classList.add('help');
const content = document.createElement('span'); { const content = document.createElement('span'); {

View File

@ -1,7 +1,7 @@
import { SlashCommandClosure } from './SlashCommandClosure.js'; import { SlashCommandClosure } from './SlashCommandClosure.js';
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
/**@readonly*/ /**@readonly*/
/**@enum {string}*/ /**@enum {string}*/
@ -17,18 +17,18 @@ export const ARGUMENT_TYPE = {
'DICTIONARY': 'dictionary', 'DICTIONARY': 'dictionary',
}; };
export class SlashCommandArgument { export class SlashCommandArgument {
/** /**
* Creates an unnamed argument from a properties object. * Creates an unnamed argument from a properties object.
* @param {Object} props * @param {Object} props
* @param {string} props.description description of the argument * @param {string} props.description description of the argument
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE) * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} [props.typeList=[ARGUMENT_TYPE.STRING]] default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument) * @param {boolean} [props.isRequired=false] default: false - whether the argument is required (false = optional argument)
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
* @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values
*/ */
static fromProps(props) { static fromProps(props) {
return new SlashCommandArgument( return new SlashCommandArgument(
@ -38,27 +38,28 @@ export class SlashCommandArgument {
props.acceptsMultiple ?? false, props.acceptsMultiple ?? false,
props.defaultValue ?? null, props.defaultValue ?? null,
props.enumList ?? [], props.enumList ?? [],
props.enumProvider ?? null,
props.forceEnum ?? false,
); );
} }
/**@type {string}*/ description; /**@type {string}*/ description;
/**@type {ARGUMENT_TYPE[]}*/ typeList = []; /**@type {ARGUMENT_TYPE[]}*/ typeList = [];
/**@type {boolean}*/ isRequired = false; /**@type {boolean}*/ isRequired = false;
/**@type {boolean}*/ acceptsMultiple = false; /**@type {boolean}*/ acceptsMultiple = false;
/**@type {string|SlashCommandClosure}*/ defaultValue; /**@type {string|SlashCommandClosure}*/ defaultValue;
/**@type {SlashCommandEnumValue[]}*/ enumList = []; /**@type {SlashCommandEnumValue[]}*/ enumList = [];
/**@type {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]}*/ enumProvider = null;
/**@type {boolean}*/ forceEnum = false;
/** /**
* @param {string} description * @param {string} description
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
* @param {string|SlashCommandClosure} defaultValue * @param {string|SlashCommandClosure} defaultValue
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
*/ */
constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = []) { constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = true) {
this.description = description; this.description = description;
this.typeList = types ? Array.isArray(types) ? types : [types] : []; this.typeList = types ? Array.isArray(types) ? types : [types] : [];
this.isRequired = isRequired ?? false; this.isRequired = isRequired ?? false;
@ -68,23 +69,28 @@ export class SlashCommandArgument {
if (it instanceof SlashCommandEnumValue) return it; if (it instanceof SlashCommandEnumValue) return it;
return new SlashCommandEnumValue(it); return new SlashCommandEnumValue(it);
}); });
this.enumProvider = enumProvider;
this.forceEnum = forceEnum;
// If no enums were set explictly and the type is one where we know possible enum values, we set them here
if (!this.enumList.length && this.typeList.includes(ARGUMENT_TYPE.BOOLEAN)) this.enumList = commonEnumProviders.boolean()();
} }
} }
export class SlashCommandNamedArgument extends SlashCommandArgument { export class SlashCommandNamedArgument extends SlashCommandArgument {
/** /**
* Creates an unnamed argument from a properties object. * Creates an unnamed argument from a properties object.
* @param {Object} props * @param {Object} props
* @param {string} props.name the argument's name * @param {string} props.name the argument's name
* @param {string[]} [props.aliasList] list of aliases
* @param {string} props.description description of the argument * @param {string} props.description description of the argument
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE) * @param {string[]} [props.aliasList=[]] list of aliases
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument) * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} [props.typeList=[ARGUMENT_TYPE.STRING]] default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values * @param {boolean} [props.isRequired=false] default: false - whether the argument is required (false = optional argument)
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
* @param {boolean} [props.forceEnum=true] default: true - whether the input must match one of the enum values
*/ */
static fromProps(props) { static fromProps(props) {
return new SlashCommandNamedArgument( return new SlashCommandNamedArgument(
@ -96,25 +102,28 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
props.defaultValue ?? null, props.defaultValue ?? null,
props.enumList ?? [], props.enumList ?? [],
props.aliasList ?? [], props.aliasList ?? [],
props.enumProvider ?? null,
props.forceEnum ?? true,
); );
} }
/**@type {string}*/ name; /**@type {string}*/ name;
/**@type {string[]}*/ aliasList = []; /**@type {string[]}*/ aliasList = [];
/** /**
* @param {string} name * @param {string} name
* @param {string} description * @param {string} description
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
* @param {string|SlashCommandClosure} defaultValue * @param {boolean} [isRequired=false]
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums * @param {boolean} [acceptsMultiple=false]
* @param {string|SlashCommandClosure} [defaultValue=null]
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]
* @param {string[]} [aliases=[]]
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
* @param {boolean} [forceEnum=true]
*/ */
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = []) { constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = true) {
super(description, types, isRequired, acceptsMultiple, defaultValue, enums); super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum);
this.name = name; this.name = name;
this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : []; this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : [];
} }

View File

@ -43,6 +43,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
[...namedResult.optionList, ...unnamedResult.optionList], [...namedResult.optionList, ...unnamedResult.optionList],
); );
combinedResult.isRequired = namedResult.isRequired || unnamedResult.isRequired; combinedResult.isRequired = namedResult.isRequired || unnamedResult.isRequired;
combinedResult.forceMatch = namedResult.forceMatch && unnamedResult.forceMatch;
return combinedResult; return combinedResult;
} }
} }
@ -102,18 +103,19 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
if (name.includes('=') && cmdArg) { if (name.includes('=') && cmdArg) {
// if cursor is already behind "=" check for enums // if cursor is already behind "=" check for enums
/**@type {SlashCommandNamedArgument} */ const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
if (cmdArg && cmdArg.enumList?.length) { if (cmdArg && enumList?.length) {
if (isSelect && cmdArg.enumList.includes(value) && argAssign && argAssign.end == index) { if (isSelect && enumList.find(it=>it.value == value) && argAssign && argAssign.end == index) {
return null; return null;
} }
const result = new AutoCompleteSecondaryNameResult( const result = new AutoCompleteSecondaryNameResult(
value, value,
start + name.length, start + name.length,
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)), enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
true, true,
); );
result.isRequired = true; result.isRequired = true;
result.forceMatch = cmdArg.forceEnum;
return result; return result;
} }
} }
@ -148,7 +150,8 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
if (idx > -1) { if (idx > -1) {
argAssign = this.executor.unnamedArgumentList[idx]; argAssign = this.executor.unnamedArgumentList[idx];
cmdArg = this.executor.command.unnamedArgumentList[idx]; cmdArg = this.executor.command.unnamedArgumentList[idx];
if (cmdArg && cmdArg.enumList.length > 0) { const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
if (cmdArg && enumList.length > 0) {
value = argAssign.value.toString().slice(0, index - argAssign.start); value = argAssign.value.toString().slice(0, index - argAssign.start);
start = argAssign.start; start = argAssign.start;
} else { } else {
@ -163,17 +166,19 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
return null; return null;
} }
if (cmdArg == null || cmdArg.enumList.length == 0) return null; const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
if (cmdArg == null || enumList.length == 0) return null;
const result = new AutoCompleteSecondaryNameResult( const result = new AutoCompleteSecondaryNameResult(
value, value,
start, start,
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)), enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
false, false,
); );
const isCompleteValue = cmdArg.enumList.find(it=>it.value == value); const isCompleteValue = enumList.find(it=>it.value == value);
const isSelectedValue = isSelect && isCompleteValue; const isSelectedValue = isSelect && isCompleteValue;
result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue; result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue;
result.forceMatch = cmdArg.forceEnum;
return result; return result;
} }
} }

View File

@ -1,5 +1,6 @@
import { substituteParams } from '../../script.js'; import { substituteParams } from '../../script.js';
import { delay, escapeRegex } from '../utils.js'; import { delay, escapeRegex } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandAbortController } from './SlashCommandAbortController.js'; import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js'; import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js'; import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
@ -17,6 +18,7 @@ export class SlashCommandClosure {
/**@type {SlashCommandExecutor[]}*/ executorList = []; /**@type {SlashCommandExecutor[]}*/ executorList = [];
/**@type {SlashCommandAbortController}*/ abortController; /**@type {SlashCommandAbortController}*/ abortController;
/**@type {(done:number, total:number)=>void}*/ onProgress; /**@type {(done:number, total:number)=>void}*/ onProgress;
/**@type {string}*/ rawText;
/**@type {number}*/ /**@type {number}*/
get commandCount() { get commandCount() {
@ -91,7 +93,7 @@ export class SlashCommandClosure {
/** /**
* *
* @returns Promise<SlashCommandClosureResult> * @returns {Promise<SlashCommandClosureResult>}
*/ */
async execute() { async execute() {
const closure = this.getCopy(); const closure = this.getCopy();
@ -148,6 +150,9 @@ export class SlashCommandClosure {
} }
let done = 0; let done = 0;
if (this.executorList.length == 0) {
this.scope.pipe = '';
}
for (const executor of this.executorList) { for (const executor of this.executorList) {
this.onProgress?.(done, this.commandCount); this.onProgress?.(done, this.commandCount);
if (executor instanceof SlashCommandClosureExecutor) { if (executor instanceof SlashCommandClosureExecutor) {
@ -158,10 +163,12 @@ export class SlashCommandClosure {
const result = await closure.execute(); const result = await closure.execute();
this.scope.pipe = result.pipe; this.scope.pipe = result.pipe;
} else { } else {
/**@type {import('./SlashCommand.js').NamedArguments} */
let args = { let args = {
_scope: this.scope, _scope: this.scope,
_parserFlags: executor.parserFlags, _parserFlags: executor.parserFlags,
_abortController: this.abortController, _abortController: this.abortController,
_hasUnnamedArgument: executor.unnamedArgumentList.length > 0,
}; };
let value; let value;
// substitute named arguments // substitute named arguments
@ -191,6 +198,7 @@ export class SlashCommandClosure {
if (executor.unnamedArgumentList.length == 0) { if (executor.unnamedArgumentList.length == 0) {
if (executor.injectPipe) { if (executor.injectPipe) {
value = this.scope.pipe; value = this.scope.pipe;
args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined;
} }
} else { } else {
value = []; value = [];
@ -214,7 +222,7 @@ export class SlashCommandClosure {
if (value.length == 1) { if (value.length == 1) {
value = value[0]; value = value[0];
} else if (!value.find(it=>it instanceof SlashCommandClosure)) { } else if (!value.find(it=>it instanceof SlashCommandClosure)) {
value = value.join(' '); value = value.join('');
} }
} }
} }
@ -241,6 +249,7 @@ export class SlashCommandClosure {
} }
executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount); executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);
this.scope.pipe = await executor.command.callback(args, value ?? ''); this.scope.pipe = await executor.command.callback(args, value ?? '');
this.#lintPipe(executor.command);
done += executor.commandCount; done += executor.commandCount;
this.onProgress?.(done, this.commandCount); this.onProgress?.(done, this.commandCount);
abortResult = await this.testAbortController(); abortResult = await this.testAbortController();
@ -269,4 +278,15 @@ export class SlashCommandClosure {
return result; return result;
} }
} }
/**
* Auto-fixes the pipe if it is not a valid result for STscript.
* @param {SlashCommand} command Command being executed
*/
#lintPipe(command) {
if (this.scope.pipe === undefined || this.scope.pipe === null) {
console.warn(`${command.name} returned undefined or null. Auto-fixing to empty string.`);
this.scope.pipe = '';
}
}
} }

View File

@ -0,0 +1,232 @@
import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types } from "../../script.js";
import { extension_settings } from "../extensions.js";
import { getGroupMembers, groups, selected_group } from "../group-chats.js";
import { power_user } from "../power-user.js";
import { searchCharByName, getTagsList, tags } from "../tags.js";
import { SlashCommandClosure } from "./SlashCommandClosure.js";
import { SlashCommandEnumValue, enumTypes } from "./SlashCommandEnumValue.js";
import { SlashCommandExecutor } from "./SlashCommandExecutor.js";
/**
* A collection of regularly used enum icons
*/
export const enumIcons = {
default: '◊',
// Variables
variable: '𝑥',
localVariable: 'L',
globalVariable: 'G',
scopeVariable: 'S',
// Common types
character: '👤',
group: '🧑‍🤝‍🧑',
persona: '🧙‍♂️',
qr: 'QR',
closure: '𝑓',
macro: '{{',
tag: '🏷️',
world: '🌐',
preset: '⚙️',
file: '📄',
message: '💬',
voice: '🎤',
true: '✔️',
false: '❌',
// Value types
boolean: '🔲',
string: '📝',
number: '1⃣',
array: '[]',
enum: '📚',
dictionary: '{}',
// Roles
system: '⚙️',
user: '👤',
assistant: '🤖',
// WI Icons
constant: '🔵',
normal: '🟢',
disabled: '❌',
vectorized: '🔗',
/**
* Returns the appropriate state icon based on a boolean
*
* @param {boolean} state - The state to determine the icon for
* @returns {string} The corresponding state icon
*/
getStateIcon: (state) => {
return state ? enumIcons.true : enumIcons.false;
},
/**
* Returns the appropriate WI icon based on the entry
*
* @param {Object} entry - WI entry
* @returns {string} The corresponding WI icon
*/
getWiStatusIcon: (entry) => {
if (entry.constant) return enumIcons.constant;
if (entry.disable) return enumIcons.disabled;
if (entry.vectorized) return enumIcons.vectorized;
return enumIcons.normal;
},
/**
* Returns the appropriate icon based on the role
*
* @param {extension_prompt_roles} role - The role to get the icon for
* @returns {string} The corresponding icon
*/
getRoleIcon: (role) => {
switch (role) {
case extension_prompt_roles.SYSTEM: return enumIcons.system;
case extension_prompt_roles.USER: return enumIcons.user;
case extension_prompt_roles.ASSISTANT: return enumIcons.assistant;
default: return enumIcons.default;
}
},
/**
* A function to get the data type icon
*
* @param {string} type - The type of the data
* @returns {string} The corresponding data type icon
*/
getDataTypeIcon: (type) => {
// Remove possible nullable types definition to match type icon
type = type.replace(/\?$/, '');
return enumIcons[type] ?? enumIcons.default;
}
}
/**
* A collection of common enum providers
*
* Can be used on `SlashCommandNamedArgument` and `SlashCommandArgument` and their `enumProvider` property.
*/
export const commonEnumProviders = {
/**
* Enum values for booleans. Either using true/false or on/off
* Optionally supports "toggle".
*
* @param {('onOff'|'onOffToggle'|'trueFalse')?} [mode='trueFalse'] - The mode to use. Default is 'trueFalse'.
* @returns {() => SlashCommandEnumValue[]}
*/
boolean: (mode = 'trueFalse') => () => {
switch (mode) {
case 'onOff': return [new SlashCommandEnumValue('on', null, 'macro', enumIcons.true), new SlashCommandEnumValue('off', null, 'macro', enumIcons.false)];
case 'onOffToggle': return [new SlashCommandEnumValue('on', null, 'macro', enumIcons.true), new SlashCommandEnumValue('off', null, 'macro', enumIcons.false), new SlashCommandEnumValue('toggle', null, 'macro', enumIcons.boolean)];
case 'trueFalse': return [new SlashCommandEnumValue('true', null, 'macro', enumIcons.true), new SlashCommandEnumValue('false', null, 'macro', enumIcons.false)];
default: throw new Error(`Invalid boolean enum provider mode: ${mode}`);
}
},
/**
* All possible variable names
*
* Can be filtered by `type` to only show global or local variables
*
* @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'.
* @returns {() => SlashCommandEnumValue[]}
*/
variables: (...type) => () => {
const types = type.flat();
const isAll = types.includes('all');
return [
...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [],
...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [],
...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny
]
},
/**
* All possible char entities, like characters and groups. Can be filtered down to just one type.
*
* @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return
* @returns {() => SlashCommandEnumValue[]}
*/
characters: (mode = 'all') => () => {
return [
...['all', 'character'].includes(mode) ? characters.map(char => new SlashCommandEnumValue(char.name, null, enumTypes.name, enumIcons.character)) : [],
...['all', 'group'].includes(mode) ? groups.map(group => new SlashCommandEnumValue(group.name, null, enumTypes.qr, enumIcons.group)) : [],
];
},
/**
* All group members of the given group, or default the current active one
*
* @param {string?} groupId - The id of the group - pass in `undefined` to use the current active group
* @returns {() =>SlashCommandEnumValue[]}
*/
groupMembers: (groupId = undefined) => () => getGroupMembers(groupId).map((character, index) => new SlashCommandEnumValue(String(index), character.name, enumTypes.enum, enumIcons.character)),
/**
* All possible personas
*
* @returns {SlashCommandEnumValue[]}
*/
personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
/**
* All possible tags for a given char/group entity
*
* @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show
* @returns {() => SlashCommandEnumValue[]}
*/
tagsForChar: (mode = 'all') => (/** @type {SlashCommandExecutor} */ executor) => {
// Try to see if we can find the char during execution to filter down the tags list some more. Otherwise take all tags.
const charName = executor.namedArgumentList.find(it => it.name == 'name')?.value;
if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
const assigned = key ? getTagsList(key) : [];
return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
.map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
},
/**
* All messages in the current chat, returning the message id
*
* Optionally supports variable names, and/or a placeholder for the last/new message id
*
* @param {object} [options={}] - Optional arguments
* @param {boolean} [options.allowIdAfter=false] - Whether to add an enum option for the new message id after the last message
* @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names
* @returns {() => SlashCommandEnumValue[]}
*/
messages: ({ allowIdAfter = false, allowVars = false } = {}) => () => {
return [
...chat.map((message, index) => new SlashCommandEnumValue(String(index), `${message.name}: ${message.mes}`, enumTypes.number, message.is_user ? enumIcons.user : message.is_system ? enumIcons.system : enumIcons.assistant)),
...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '')] : [],
...allowVars ? commonEnumProviders.variables('all')() : [],
];
},
/**
* All existing worlds / lorebooks
*
* @returns {SlashCommandEnumValue[]}
*/
worlds: () => $('#world_info').children().toArray().map(x => new SlashCommandEnumValue(x.textContent, null, enumTypes.name, enumIcons.world)),
/**
* All existing injects for the current chat
*
* @returns {SlashCommandEnumValue[]}
*/
injects: () => {
if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) return [];
return Object.entries(chat_metadata.script_injects)
.map(([id, inject]) => {
const positionName = (Object.entries(extension_prompt_types)).find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
return new SlashCommandEnumValue(id, `${enumIcons.getRoleIcon(inject.role ?? extension_prompt_roles.SYSTEM)}[Inject](${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}) ${inject.value}`,
enumTypes.enum, '💉');
});
},
};

View File

@ -13,7 +13,7 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
* @param {SlashCommandEnumValue} enumValue * @param {SlashCommandEnumValue} enumValue
*/ */
constructor(cmd, enumValue) { constructor(cmd, enumValue) {
super(enumValue.value, '◊'); super(enumValue.value, enumValue.typeIcon, enumValue.type);
this.cmd = cmd; this.cmd = cmd;
this.enumValue = enumValue; this.enumValue = enumValue;
} }
@ -21,9 +21,9 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
renderItem() { renderItem() {
let li; let li;
li = this.makeItem(this.name, '◊', true, [], [], null, this.enumValue.description); li = this.makeItem(this.name, this.typeIcon, true, [], [], null, this.enumValue.description);
li.setAttribute('data-name', this.name); li.setAttribute('data-name', this.name);
li.setAttribute('data-option-type', 'enum'); li.setAttribute('data-option-type', this.type);
return li; return li;
} }

View File

@ -1,10 +1,64 @@
/**
* @typedef {'enum' | 'command' | 'namedArgument' | 'variable' | 'qr' | 'macro' | 'number' | 'name'} EnumType
*/
/**
* Collection of the enum types that can be used with `SlashCommandEnumValue`
*
* Contains documentation on which color this will result to
*/
export const enumTypes = {
/** 'enum' - [string] - light orange @type {EnumType} */
enum: 'enum',
/** 'command' - [cmd] - light yellow @type {EnumType} */
command: 'command',
/** 'namedArgument' - [argName] - sky blue @type {EnumType} */
namedArgument: 'namedArgument',
/** 'variable' - [punctuationL1] - pink @type {EnumType} */
variable: 'variable',
/** 'qr' - [variable] - light blue @type {EnumType} */
qr: 'qr',
/** 'macro' - [variableLanguage] - blue @type {EnumType} */
macro: 'macro',
/** 'number' - [number] - light green @type {EnumType} */
number: 'number',
/** 'name' - [type] - forest green @type {EnumType} */
name: 'name',
/**
* Gets the value of the enum type based on the provided index
*
* Can be used to get differing colors or even random colors, by providing the index of a unique set
*
* @param {number?} index - The index used to retrieve the enum type
* @return {EnumType} The enum type corresponding to the index
*/
getBasedOnIndex(index) {
const keys = Object.keys(this);
return this[keys[(index ?? 0) % keys.length]];
}
}
export class SlashCommandEnumValue { export class SlashCommandEnumValue {
/**@type {string}*/ value; /**@type {string}*/ value;
/**@type {string}*/ description; /**@type {string}*/ description;
/**@type {EnumType}*/ type = 'enum';
/**@type {string}*/ typeIcon = '◊';
constructor(value, description = null) { /**
* A constructor for creating a SlashCommandEnumValue instance.
*
* @param {string} value - The value
* @param {string?} description - Optional description, displayed in a second line
* @param {EnumType?} type - type of the enum (defining its color)
* @param {string} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones)
*/
constructor(value, description = null, type = 'enum', typeIcon = '◊') {
this.value = value; this.value = value;
this.description = description; this.description = description;
this.type = type ?? 'enum';
this.typeIcon = typeIcon;
} }
toString() { toString() {

View File

@ -17,6 +17,10 @@ import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNa
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js'; import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js'; import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js';
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
/**@readonly*/ /**@readonly*/
/**@enum {Number}*/ /**@enum {Number}*/
@ -32,7 +36,7 @@ export class SlashCommandParser {
/** /**
* @deprecated Use SlashCommandParser.addCommandObject() instead. * @deprecated Use SlashCommandParser.addCommandObject() instead.
* @param {string} command Command name * @param {string} command Command name
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} callback The function to execute when the command is called * @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} callback callback The function to execute when the command is called
* @param {string[]} aliases List of alternative command names * @param {string[]} aliases List of alternative command names
* @param {string} helpString Help text shown in autocomplete and command browser * @param {string} helpString Help text shown in autocomplete and command browser
*/ */
@ -131,7 +135,7 @@ export class SlashCommandParser {
description: 'The state of the parser flag to set.', description: 'The state of the parser flag to set.',
typeList: [ARGUMENT_TYPE.BOOLEAN], typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'on', defaultValue: 'on',
enumList: ['on', 'off'], enumList: commonEnumProviders.boolean('onOff')(),
}), }),
], ],
splitUnnamedArgument: true, splitUnnamedArgument: true,
@ -598,6 +602,7 @@ export class SlashCommandParser {
this.closureIndex.push(closureIndexEntry); this.closureIndex.push(closureIndexEntry);
let injectPipe = true; let injectPipe = true;
if (!isRoot) this.take(2); // discard opening {: if (!isRoot) this.take(2); // discard opening {:
const textStart = this.index;
let closure = new SlashCommandClosure(this.scope); let closure = new SlashCommandClosure(this.scope);
closure.abortController = this.abortController; closure.abortController = this.abortController;
this.scope = closure.scope; this.scope = closure.scope;
@ -638,13 +643,13 @@ export class SlashCommandParser {
} }
this.discardWhitespace(); // discard further whitespace this.discardWhitespace(); // discard further whitespace
} }
closure.rawText = this.text.slice(textStart, this.index);
if (!isRoot) this.take(2); // discard closing :} if (!isRoot) this.take(2); // discard closing :}
if (this.testSymbol('()')) { if (this.testSymbol('()')) {
this.take(2); // discard () this.take(2); // discard ()
closure.executeNow = true; closure.executeNow = true;
} }
closureIndexEntry.end = this.index - 1; closureIndexEntry.end = this.index - 1;
this.discardWhitespace(); // discard trailing whitespace
this.scope = closure.scope.parent; this.scope = closure.scope.parent;
return closure; return closure;
} }
@ -820,9 +825,8 @@ export class SlashCommandParser {
if (this.testClosure()) { if (this.testClosure()) {
isList = true; isList = true;
if (value.length > 0) { if (value.length > 0) {
assignment.end = assignment.end - (value.length - value.trim().length);
this.indexMacros(this.index - value.length, value); this.indexMacros(this.index - value.length, value);
assignment.value = value.trim(); assignment.value = value;
listValues.push(assignment); listValues.push(assignment);
assignment = new SlashCommandUnnamedArgumentAssignment(); assignment = new SlashCommandUnnamedArgumentAssignment();
assignment.start = this.index; assignment.start = this.index;
@ -834,6 +838,7 @@ export class SlashCommandParser {
listValues.push(assignment); listValues.push(assignment);
assignment = new SlashCommandUnnamedArgumentAssignment(); assignment = new SlashCommandUnnamedArgumentAssignment();
assignment.start = this.index; assignment.start = this.index;
if (split) this.discardWhitespace();
} else if (split) { } else if (split) {
if (this.testQuotedValue()) { if (this.testQuotedValue()) {
assignment.start = this.index; assignment.start = this.index;
@ -862,8 +867,8 @@ export class SlashCommandParser {
assignment.end = this.index; assignment.end = this.index;
} }
} }
if (isList && value.trim().length > 0) { if (isList && value.length > 0) {
assignment.value = value.trim(); assignment.value = value;
listValues.push(assignment); listValues.push(assignment);
} }
if (isList) { if (isList) {

View File

@ -92,7 +92,7 @@ export class SlashCommandScope {
v = v[numIndex]; v = v[numIndex];
} }
if (typeof v == 'object') return JSON.stringify(v); if (typeof v == 'object') return JSON.stringify(v);
return v; return v ?? '';
} else { } else {
const value = this.variables[key]; const value = this.variables[key];
return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value); return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value);

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More