mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-02 02:47:52 +01:00
Merge branch 'staging' into parser-enum-stuff
This commit is contained in:
commit
dcbadcb5f9
@ -476,7 +476,11 @@
|
||||
"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"
|
||||
},
|
||||
{
|
||||
@ -556,7 +560,11 @@
|
||||
"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"
|
||||
},
|
||||
{
|
||||
|
@ -8,5 +8,5 @@
|
||||
"trim_sentences": true,
|
||||
"include_newline": false,
|
||||
"single_line": false,
|
||||
"name": "DreamGen Role-Play V1"
|
||||
"name": "DreamGen Role-Play V1 ChatML"
|
||||
}
|
@ -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"
|
||||
}
|
@ -20,5 +20,5 @@
|
||||
"user_alignment_message": "",
|
||||
"system_same_as_user": true,
|
||||
"last_system_sequence": "",
|
||||
"name": "DreamGen Role-Play V1"
|
||||
"name": "DreamGen Role-Play V1 ChatML"
|
||||
}
|
@ -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"
|
||||
}
|
122
public/css/animations.css
Normal file
122
public/css/animations.css
Normal 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);
|
||||
}
|
||||
}
|
@ -98,7 +98,7 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
131
public/css/popup.css
Normal file
131
public/css/popup.css
Normal file
@ -0,0 +1,131 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/* Fix toastr in dialogs by actually placing it at the top of the screen via transform */
|
||||
.popup #toast-container {
|
||||
height: 100svh;
|
||||
top: calc(50% + var(--topBarBlockSize));
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.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);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
@ -57,7 +57,7 @@
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 7px;
|
||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||
font-family: var(--mainFontFamily);
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 7px;
|
||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||
font-family: var(--mainFontFamily);
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
@ -181,8 +181,9 @@
|
||||
}
|
||||
|
||||
.select2-selection__choice__display {
|
||||
/* Fix weird alignment on the left side */
|
||||
margin-left: 1px;
|
||||
/* Fix weird alignment of the inside block */
|
||||
margin-left: 3px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
/* Styling for choice remove icon */
|
||||
@ -266,4 +267,4 @@ span.select2.select2-container .select2-selection__choice__remove:hover {
|
||||
|
||||
.select2_multi_sameline+span.select2-container.select2-container--focus .select2-selection--multiple .select2-search--inline {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
|
@ -292,6 +292,14 @@
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.alignitemscenter,
|
||||
.alignItemsCenter {
|
||||
align-items: center;
|
||||
@ -348,6 +356,10 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.margin-r2 {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.flex0 {
|
||||
flex: 0;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@ -27,8 +27,19 @@
|
||||
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 {
|
||||
padding-right: 0;
|
||||
padding: 2px 4px;
|
||||
color: var(--SmartThemeBodyColor) !important;
|
||||
}
|
||||
|
||||
@ -108,6 +119,14 @@
|
||||
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 {
|
||||
opacity: 0.6;
|
||||
column-gap: 0.2rem;
|
||||
@ -164,6 +183,7 @@
|
||||
.tag.selected {
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
border: 1px solid lightgreen;
|
||||
}
|
||||
|
||||
.tag.excluded {
|
||||
|
@ -33,7 +33,6 @@
|
||||
<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="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/group-avatars.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/toggle-dependent.css">
|
||||
@ -73,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">
|
||||
<input type="checkbox" id="lm_button_panel_pin">
|
||||
<label for="lm_button_panel_pin">
|
||||
<div class="unchecked fa-solid fa-unlock "></div>
|
||||
<div class="checked fa-solid fa-lock "></div>
|
||||
<div class="unchecked fa-solid fa-unlock right_menu_button"></div>
|
||||
<div class="checked fa-solid fa-lock right_menu_button"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="clickSlidersTips" data-i18n="clickslidertips" class="toggle-description wide100p editable-slider-notification">
|
||||
@ -1754,8 +1753,8 @@
|
||||
<input id="openai_function_calling" type="checkbox" />
|
||||
<span data-i18n="Enable function calling">Enable function calling</span>
|
||||
<div class="flexBasis100p toggle-description justifyLeft">
|
||||
Allows using <a href="https://platform.openai.com/docs/guides/function-calling" target="_blank">function tools</a>.
|
||||
Can be utilized by various extensions to provide additional functionality.
|
||||
<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>.
|
||||
<span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@ -1864,7 +1863,7 @@
|
||||
<div id="openai_logit_bias_new_entry" class="menu_button wide100p flex-container justifyCenter" data-i18n="Add bias entry">
|
||||
Add bias entry
|
||||
</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">
|
||||
<small>
|
||||
<i class="fa-solid fa-lightbulb"></i>
|
||||
@ -1888,7 +1887,7 @@
|
||||
</div>
|
||||
<div id="sys-settings-button" class="drawer">
|
||||
<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 id="rm_api_block" class="drawer-content closedDrawer">
|
||||
<h3 class="margin0" id="title_api">API</h3>
|
||||
@ -2358,7 +2357,7 @@
|
||||
<select id="chat_completion_source">
|
||||
<optgroup>
|
||||
<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>
|
||||
<option value="ai21">AI21</option>
|
||||
@ -2857,16 +2856,16 @@
|
||||
<form id="custom_form" data-source="custom">
|
||||
<h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<h4>
|
||||
<span data-i18n="Custom API Key">Custom API Key</span>
|
||||
<small>(Optional)</small>
|
||||
<small data-i18n="(Optional)">(Optional)</small>
|
||||
</h4>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_custom" name="api_key_custom" class="text_pole flex1" maxlength="5000" value="" type="text" autocomplete="off">
|
||||
@ -2875,9 +2874,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.">
|
||||
For privacy reasons, your API key will be hidden after you reload the page.
|
||||
</div>
|
||||
<h4>Enter a Model ID</h4>
|
||||
<h4 data-i18n="Enter a Model ID">Enter a Model ID</h4>
|
||||
<div class="flex-container">
|
||||
<input id="custom_model_id" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" placeholder="Example: gpt-3.5-turbo">
|
||||
<input 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">
|
||||
</div>
|
||||
<h4 data-i18n="Available Models">Available Models</h4>
|
||||
<div class="flex-container">
|
||||
@ -2885,14 +2884,14 @@
|
||||
</div>
|
||||
<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.">
|
||||
<option value="">None</option>
|
||||
<option data-i18n="prompt_post_processing_none" value="">None</option>
|
||||
<option value="claude">Claude</option>
|
||||
</select>
|
||||
</form>
|
||||
<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 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 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>
|
||||
@ -4247,7 +4246,7 @@
|
||||
<div name="STscriptToggles">
|
||||
<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.">
|
||||
<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.">
|
||||
<input id="stscript_parser_flag_strict_escaping" type="checkbox" />
|
||||
<span data-i18n="STRICT_ESCAPING"><small>STRICT_ESCAPING</small></span>
|
||||
@ -4476,14 +4475,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">
|
||||
<input type="checkbox" id="rm_button_panel_pin">
|
||||
<label for="rm_button_panel_pin">
|
||||
<div class="fa-solid unchecked fa-unlock" alt=""></div>
|
||||
<div class="fa-solid checked fa-lock" alt=""></div>
|
||||
<div class="fa-solid unchecked fa-unlock right_menu_button" alt=""></div>
|
||||
<div class="fa-solid checked fa-lock right_menu_button" alt=""></div>
|
||||
</label>
|
||||
</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 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>
|
||||
<hr>
|
||||
@ -4491,7 +4490,7 @@
|
||||
<div id="rm_PinAndTabs">
|
||||
<div id="right-nav-panel-tabs" class="">
|
||||
<div id="rm_button_selected_ch">
|
||||
<h2></h2>
|
||||
<h2 class="interactable"></h2>
|
||||
</div>
|
||||
<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.">
|
||||
@ -4842,21 +4841,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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 -->
|
||||
<div class="shadow_popup">
|
||||
<div class="dialogue_popup">
|
||||
<div class="dialogue_popup_holder">
|
||||
<div class="dialogue_popup_text">
|
||||
<h3 class="margin0">text</h3>
|
||||
</div>
|
||||
<textarea class="dialogue_popup_input text_pole" rows="1"></textarea>
|
||||
<div class="dialogue_popup_controls">
|
||||
<div class="dialogue_popup_ok menu_button" data-i18n="Delete">Delete</div>
|
||||
<div class="dialogue_popup_cancel menu_button" data-i18n="Cancel">Cancel</div>
|
||||
</div>
|
||||
<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 -->
|
||||
<dialog class="popup">
|
||||
<div class="popup-body">
|
||||
<div class="popup-content">
|
||||
<h3 class="popup-header">text</h3>
|
||||
</div>
|
||||
<textarea class="popup-input text_pole result-control" rows="1" data-result="1"></textarea>
|
||||
<div class="popup-controls">
|
||||
<div class="popup-button-ok menu_button result-control" data-i18n="Delete" data-result="1" tabindex="0">Delete</div>
|
||||
<div class="popup-button-cancel menu_button result-control" data-i18n="Cancel" data-result="0" tabindex="0">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
<div id="shadow_popup">
|
||||
<div id="dialogue_popup">
|
||||
@ -4866,15 +4863,15 @@
|
||||
</div>
|
||||
<textarea id="dialogue_popup_input" class="text_pole" rows="1"></textarea>
|
||||
<div id="dialogue_popup_controls">
|
||||
<div id="dialogue_popup_ok" class="menu_button" data-i18n="Delete">Delete</div>
|
||||
<div id="dialogue_popup_cancel" class="menu_button" data-i18n="Cancel">Cancel</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" data-result="0">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="character_popup" class="flex-container flexFlowColumn flexNoGap">
|
||||
<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>
|
||||
</div>
|
||||
<hr class="margin-bot-10px">
|
||||
@ -5193,8 +5190,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">
|
||||
<span class="tag_folder_indicator"></span>
|
||||
</div>
|
||||
<div class="tagColorPickerHolder"></div>
|
||||
<div class="tagColorPicker2Holder"></div>
|
||||
<div class="tag_view_color_picker" data-value="color"></div>
|
||||
<div class="tag_view_color_picker" data-value="color2"></div>
|
||||
<div class="tag_view_name" contenteditable="true"></div>
|
||||
<div class="tag_view_counter"><span class="tag_view_counter_value"></span> entries</div>
|
||||
<div title="Delete tag" class="tag_delete fa-solid fa-trash-can right_menu_button" data-i18n="[title]Delete tag"></div>
|
||||
@ -5617,21 +5614,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 title="Translate message" class="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="Narrate" class="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="Exclude message from prompts" class="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="Embed file or image" class="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 branch" class="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="Translate message" class="mes_button mes_translate fa-solid fa-language" data-i18n="[title]Translate message"></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_button mes_narrate fa-solid fa-bullhorn" data-i18n="[title]Narrate"></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_button mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from 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_button mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></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_button mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
|
||||
<div title="Copy" class="mes_button mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
|
||||
</div>
|
||||
<div title="Open checkpoint chat" class="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="Open checkpoint chat" class="mes_button mes_bookmark fa-solid fa-flag" data-i18n="[title]Open checkpoint chat"></div>
|
||||
<div title="Edit" class="mes_button mes_edit fa-solid fa-pencil " data-i18n="[title]Edit"></div>
|
||||
</div>
|
||||
<div class="mes_edit_buttons">
|
||||
<div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
|
||||
@ -5833,8 +5830,8 @@
|
||||
</small>
|
||||
<hr>
|
||||
<div class="alternate_greetings_list flexFlowColumn flex-container wide100p">
|
||||
<strong class="alternate_grettings_hint margin-bot-10px" data-i18n="Alternate Greetings Hint">
|
||||
Click the <i class="fa-solid fa-plus"></i> button to get started!
|
||||
<strong class="alternate_grettings_hint margin-bot-10px">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -5842,7 +5839,7 @@
|
||||
<div id="alternate_greeting_form_template" class="template_element">
|
||||
<div class="alternate_greeting">
|
||||
<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>
|
||||
<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 +6248,7 @@
|
||||
</form>
|
||||
<div id="nonQRFormItems">
|
||||
<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>
|
||||
<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">
|
||||
@ -6267,8 +6264,8 @@
|
||||
<div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
|
||||
<i class="fa-solid fa-circle-stop"></i>
|
||||
</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="send_but" class="fa-solid fa-paper-plane displayNone" title="Send a message" data-i18n="[title]Send a 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 interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -6432,15 +6429,6 @@
|
||||
<script type="module" src="scripts/setting-search.js"></script>
|
||||
<script type="module" src="scripts/server-history.js"></script>
|
||||
<script type="module" src="script.js"></script>
|
||||
<script>
|
||||
// Configure toast library:
|
||||
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>
|
||||
window.addEventListener('load', (event) => {
|
||||
const documentHeight = () => {
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "تحرير الشخصيات جميعها",
|
||||
"Bulk select all characters": "تحديد كافة الشخصيات بالجملة",
|
||||
"Bulk delete characters": "حذف الشخصيات جميعها",
|
||||
"popup_text_save": "يحفظ",
|
||||
"popup_text_yes": "نعم",
|
||||
"popup_text_no": "لا",
|
||||
"popup_text_cancel": "يلغي",
|
||||
"popup_text_import": "يستورد",
|
||||
"popup-button-save": "يحفظ",
|
||||
"popup-button-yes": "نعم",
|
||||
"popup-button-no": "لا",
|
||||
"popup-button-cancel": "يلغي",
|
||||
"popup-button-import": "يستورد",
|
||||
"Advanced Defininitions": "تعريفات متقدمة",
|
||||
"Prompt Overrides": "التجاوزات السريعة",
|
||||
"(For Chat Completion and Instruct Mode)": "(لاستكمال الدردشة ووضع التعليمات)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "Massenbearbeitung von Charakteren",
|
||||
"Bulk select all characters": "Massenauswahl aller Zeichen",
|
||||
"Bulk delete characters": "Massenlöschung von Charakteren",
|
||||
"popup_text_save": "Speichern",
|
||||
"popup_text_yes": "Ja",
|
||||
"popup_text_no": "NEIN",
|
||||
"popup_text_cancel": "Stornieren",
|
||||
"popup_text_import": "Importieren",
|
||||
"popup-button-save": "Speichern",
|
||||
"popup-button-yes": "Ja",
|
||||
"popup-button-no": "NEIN",
|
||||
"popup-button-cancel": "Stornieren",
|
||||
"popup-button-import": "Importieren",
|
||||
"Advanced Defininitions": "Erweiterte Definitionen",
|
||||
"Prompt Overrides": "Eingabeaufforderungsüberschreibungen",
|
||||
"(For Chat Completion and Instruct Mode)": "(Für Chat-Abschluss und Anweisungsmodus)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "Editar personajes masivamente",
|
||||
"Bulk select all characters": "Seleccionar de forma masiva todos los personajes",
|
||||
"Bulk delete characters": "Eliminar personajes masivamente",
|
||||
"popup_text_save": "Ahorrar",
|
||||
"popup_text_yes": "Sí",
|
||||
"popup_text_no": "No",
|
||||
"popup_text_cancel": "Cancelar",
|
||||
"popup_text_import": "Importar",
|
||||
"popup-button-save": "Ahorrar",
|
||||
"popup-button-yes": "Sí",
|
||||
"popup-button-no": "No",
|
||||
"popup-button-cancel": "Cancelar",
|
||||
"popup-button-import": "Importar",
|
||||
"Advanced Defininitions": "Definiciones avanzadas",
|
||||
"Prompt Overrides": "Anulaciones rápidas",
|
||||
"(For Chat Completion and Instruct Mode)": "(Para completar el chat y el modo de instrucción)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "Édition en masse des personnages",
|
||||
"Bulk select all characters": "Sélection groupée de tous les caractères",
|
||||
"Bulk delete characters": "Suppression en masse des personnages",
|
||||
"popup_text_save": "Sauvegarder",
|
||||
"popup_text_yes": "Oui",
|
||||
"popup_text_no": "Non",
|
||||
"popup_text_cancel": "Annuler",
|
||||
"popup_text_import": "Importer",
|
||||
"popup-button-save": "Sauvegarder",
|
||||
"popup-button-yes": "Oui",
|
||||
"popup-button-no": "Non",
|
||||
"popup-button-cancel": "Annuler",
|
||||
"popup-button-import": "Importer",
|
||||
"Advanced Defininitions": "Définitions avancées",
|
||||
"Prompt Overrides": "Remplacements d'invite",
|
||||
"(For Chat Completion and Instruct Mode)": "(Pour l'achèvement du chat et le mode instruction)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "Breyta mörgum persónum í einu",
|
||||
"Bulk select all characters": "Velja alla stafi í magni",
|
||||
"Bulk delete characters": "Eyða mörgum persónum í einu",
|
||||
"popup_text_save": "Vista",
|
||||
"popup_text_yes": "Já",
|
||||
"popup_text_no": "Nei",
|
||||
"popup_text_cancel": "Hætta við",
|
||||
"popup_text_import": "Flytja inn",
|
||||
"popup-button-save": "Vista",
|
||||
"popup-button-yes": "Já",
|
||||
"popup-button-no": "Nei",
|
||||
"popup-button-cancel": "Hætta við",
|
||||
"popup-button-import": "Flytja inn",
|
||||
"Advanced Defininitions": "Ítarleg skilgreiningar",
|
||||
"Prompt Overrides": "Hnekkja hvetjandi",
|
||||
"(For Chat Completion and Instruct Mode)": "(Til að ljúka spjalli og leiðbeiningarham)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "Modifica personaggi in blocco",
|
||||
"Bulk select all characters": "Seleziona in blocco tutti i personaggi",
|
||||
"Bulk delete characters": "Elimina personaggi in blocco",
|
||||
"popup_text_save": "Salva",
|
||||
"popup_text_yes": "SÌ",
|
||||
"popup_text_no": "NO",
|
||||
"popup_text_cancel": "Annulla",
|
||||
"popup_text_import": "Importare",
|
||||
"popup-button-save": "Salva",
|
||||
"popup-button-yes": "SÌ",
|
||||
"popup-button-no": "NO",
|
||||
"popup-button-cancel": "Annulla",
|
||||
"popup-button-import": "Importare",
|
||||
"Advanced Defininitions": "Definizioni avanzate",
|
||||
"Prompt Overrides": "Sostituzioni richieste",
|
||||
"(For Chat Completion and Instruct Mode)": "(Per il completamento della chat e la modalità istruzione)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "キャラクターを一括編集",
|
||||
"Bulk select all characters": "すべての文字を一括選択",
|
||||
"Bulk delete characters": "キャラクターを一括削除",
|
||||
"popup_text_save": "保存",
|
||||
"popup_text_yes": "はい",
|
||||
"popup_text_no": "いいえ",
|
||||
"popup_text_cancel": "キャンセル",
|
||||
"popup_text_import": "輸入",
|
||||
"popup-button-save": "保存",
|
||||
"popup-button-yes": "はい",
|
||||
"popup-button-no": "いいえ",
|
||||
"popup-button-cancel": "キャンセル",
|
||||
"popup-button-import": "輸入",
|
||||
"Advanced Defininitions": "高度な定義",
|
||||
"Prompt Overrides": "プロンプトのオーバーライド",
|
||||
"(For Chat Completion and Instruct Mode)": "(チャット補完と指示モード用)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "대량 캐릭터 편집",
|
||||
"Bulk select all characters": "모든 문자 일괄 선택",
|
||||
"Bulk delete characters": "대량 캐릭터 삭제",
|
||||
"popup_text_save": "구하다",
|
||||
"popup_text_yes": "예",
|
||||
"popup_text_no": "아니요",
|
||||
"popup_text_cancel": "취소",
|
||||
"popup_text_import": "수입",
|
||||
"popup-button-save": "구하다",
|
||||
"popup-button-yes": "예",
|
||||
"popup-button-no": "아니요",
|
||||
"popup-button-cancel": "취소",
|
||||
"popup-button-import": "수입",
|
||||
"Advanced Defininitions": "고급 정의",
|
||||
"Prompt Overrides": "프롬프트 무시",
|
||||
"(For Chat Completion and Instruct Mode)": "(채팅 완료 및 지시 모드의 경우)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "Massaal bewerken personages",
|
||||
"Bulk select all characters": "Selecteer alle tekens in bulk",
|
||||
"Bulk delete characters": "Massaal verwijderen personages",
|
||||
"popup_text_save": "Redden",
|
||||
"popup_text_yes": "Ja",
|
||||
"popup_text_no": "Nee",
|
||||
"popup_text_cancel": "Annuleren",
|
||||
"popup_text_import": "Importeren",
|
||||
"popup-button-save": "Redden",
|
||||
"popup-button-yes": "Ja",
|
||||
"popup-button-no": "Nee",
|
||||
"popup-button-cancel": "Annuleren",
|
||||
"popup-button-import": "Importeren",
|
||||
"Advanced Defininitions": "Geavanceerde definities",
|
||||
"Prompt Overrides": "Prompt-overschrijvingen",
|
||||
"(For Chat Completion and Instruct Mode)": "(Voor voltooiing van chat en instructiemodus)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "Editar personagens em massa",
|
||||
"Bulk select all characters": "Selecione em massa todos os caracteres",
|
||||
"Bulk delete characters": "Excluir personagens em massa",
|
||||
"popup_text_save": "Salvar",
|
||||
"popup_text_yes": "Sim",
|
||||
"popup_text_no": "Não",
|
||||
"popup_text_cancel": "Cancelar",
|
||||
"popup_text_import": "Importar",
|
||||
"popup-button-save": "Salvar",
|
||||
"popup-button-yes": "Sim",
|
||||
"popup-button-no": "Não",
|
||||
"popup-button-cancel": "Cancelar",
|
||||
"popup-button-import": "Importar",
|
||||
"Advanced Defininitions": "Definições Avançadas",
|
||||
"Prompt Overrides": "Substituições de prompt",
|
||||
"(For Chat Completion and Instruct Mode)": "(Para conclusão de bate-papo e modo de instrução)",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -885,11 +885,11 @@
|
||||
"Bulk_edit_characters": "Масове редагування персонажів",
|
||||
"Bulk select all characters": "Масове виділення всіх символів",
|
||||
"Bulk delete characters": "Масове видалення персонажів",
|
||||
"popup_text_save": "зберегти",
|
||||
"popup_text_yes": "Так",
|
||||
"popup_text_no": "Немає",
|
||||
"popup_text_cancel": "Скасувати",
|
||||
"popup_text_import": "Імпорт",
|
||||
"popup-button-save": "зберегти",
|
||||
"popup-button-yes": "Так",
|
||||
"popup-button-no": "Немає",
|
||||
"popup-button-cancel": "Скасувати",
|
||||
"popup-button-import": "Імпорт",
|
||||
"Advanced Defininitions": "Розширені визначення",
|
||||
"Prompt Overrides": "Перевизначення підказок",
|
||||
"(For Chat Completion and Instruct Mode)": "(Для завершення чату та режиму інструктажу)",
|
||||
|
@ -885,11 +885,11 @@
|
||||
"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 delete characters": "Xóa nhân vật theo lô",
|
||||
"popup_text_save": "Cứu",
|
||||
"popup_text_yes": "Đúng",
|
||||
"popup_text_no": "KHÔNG",
|
||||
"popup_text_cancel": "Hủy bỏ",
|
||||
"popup_text_import": "Nhập khẩu",
|
||||
"popup-button-save": "Cứu",
|
||||
"popup-button-yes": "Đúng",
|
||||
"popup-button-no": "KHÔNG",
|
||||
"popup-button-cancel": "Hủy bỏ",
|
||||
"popup-button-import": "Nhập khẩu",
|
||||
"Advanced Defininitions": "Các Định nghĩa Nâng cao",
|
||||
"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)",
|
||||
|
@ -238,6 +238,9 @@
|
||||
"Squash system messages": "压缩系统消息",
|
||||
"Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "将连续的系统消息合并为一条(不包括示例对话),可能会提高一些模型的连贯性。",
|
||||
"Enable function calling": "启用函数调用",
|
||||
"enable_functions_desc_1": "允许使用",
|
||||
"enable_functions_desc_2": "功能工具",
|
||||
"enable_functions_desc_3": "可以被各种扩展利用来提供附加功能。",
|
||||
"Send inline images": "发送内联图像",
|
||||
"image_inlining_hint_1": "如果模型支持,则在提示词中发送图像(例如 GPT-4V、Claude 3 或 Llava 13B)。\n对任何消息使用",
|
||||
"image_inlining_hint_2": "或",
|
||||
@ -266,8 +269,10 @@
|
||||
"Delete preset": "删除预设",
|
||||
"View / Edit bias preset": "查看/编辑偏置预设",
|
||||
"Add bias entry": "添加偏置条目",
|
||||
"openai_logit_bias_no_items": "没有相关产品",
|
||||
"Most tokens have a leading space.": "大多数词符都有一个前导空格。",
|
||||
"API Connections": "API连接",
|
||||
"api_no_connection": "无连接...",
|
||||
"Text Completion": "文本补全",
|
||||
"Chat Completion": "聊天补全",
|
||||
"NovelAI": "NovelAI",
|
||||
@ -344,6 +349,7 @@
|
||||
"Legacy API (pre-OAI, no streaming)": "旧版API(OAI之前,无流式传输)",
|
||||
"Bypass status check": "绕过状态检查",
|
||||
"Chat Completion Source": "聊天补全来源",
|
||||
"Custom (OpenAI-compatible)": "自定义(兼容 OpenAI)",
|
||||
"Reverse Proxy": "反向代理",
|
||||
"Proxy Presets": "代理预设",
|
||||
"Saved addresses and passwords.": "保存的地址和密码。",
|
||||
@ -402,10 +408,17 @@
|
||||
"Cohere API Key": "Cohere API 密钥",
|
||||
"Cohere Model": "Cohere 模型",
|
||||
"Custom Endpoint (Base URL)": "自定义端点(基本 URL)",
|
||||
"Example: http://localhost:1234/v1": "例如:http://localhost:1234/v1",
|
||||
"at the end of the URL!": "在 URL 的末尾!",
|
||||
"Custom API Key": "自定义 API 密钥",
|
||||
"(Optional)": "(可选的)",
|
||||
"Enter a Model ID": "输入模型名",
|
||||
"Example: gpt-3.5-turbo": "例如:gpt-3.5-turbo",
|
||||
"Available Models": "可用模型",
|
||||
"Prompt Post-Processing": "提示词后处理",
|
||||
"Applies additional processing to the prompt before sending it to the API.": "在将提示词发送到 API 之前对其进行额外处理。",
|
||||
"prompt_post_processing_none": "未选择",
|
||||
"Additional Parameters": "附加参数",
|
||||
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "通过发送简短的测试消息验证您的API连接。请注意,您将因此而消耗额度!",
|
||||
"Test Message": "发送测试消息",
|
||||
"Auto-connect to Last Server": "自动连接到上次的服务器",
|
||||
@ -886,11 +899,11 @@
|
||||
"Bulk_edit_characters": "批量编辑角色",
|
||||
"Bulk select all characters": "批量选择所有角色",
|
||||
"Bulk delete characters": "批量删除角色",
|
||||
"popup_text_save": "保存",
|
||||
"popup_text_yes": "是",
|
||||
"popup_text_no": "否",
|
||||
"popup_text_cancel": "取消",
|
||||
"popup_text_import": "导入",
|
||||
"popup-button-save": "保存",
|
||||
"popup-button-yes": "是",
|
||||
"popup-button-no": "否",
|
||||
"popup-button-cancel": "取消",
|
||||
"popup-button-import": "导入",
|
||||
"Advanced Defininitions": "高级定义",
|
||||
"Prompt Overrides": "提示词覆盖",
|
||||
"(For Chat Completion and Instruct Mode)": "(用于聊天补全和指导模式)",
|
||||
@ -1086,7 +1099,9 @@
|
||||
"Add to group": "添加到群组中",
|
||||
"Alternate Greetings": "额外问候语",
|
||||
"Alternate_Greetings_desc": "开始新聊天时,这些按钮将显示为第一条消息的滑动选项。\n群组成员可以选择其中之一来发起对话。",
|
||||
"Alternate Greetings Hint": "单击按钮即可开始!",
|
||||
"alternate_greetings_hint_1": "点击",
|
||||
"alternate_greetings_hint_2": "按钮即可开始!",
|
||||
"Alternate Greeting #": "额外问候语 #",
|
||||
"(This will be the first message from the character that starts every chat)": "(这将是角色在每次聊天开始时发送的第一条消息)",
|
||||
"Forbid Media Override explanation": "当前角色/群组在聊天中使用外部媒体的能力。",
|
||||
"Forbid Media Override subtitle": "媒体:图像、视频、音频。外部:不在本地服务器上托管。",
|
||||
@ -1203,7 +1218,6 @@
|
||||
"Extras": "更多",
|
||||
"Horde": "Horde",
|
||||
"API": "API",
|
||||
"Custom (OpenAI-compatible)": "自定义(兼容 OpenAI)",
|
||||
"Text Generation WebUI (oobabooga)": "文本生成 WebUI (oobabooga)",
|
||||
"Model": "模型",
|
||||
"currently_selected": "[当前选定]",
|
||||
@ -1322,12 +1336,15 @@
|
||||
"(if disabled, use ": "(如果禁用,使用",
|
||||
"macro for manual injection)": "宏用于手动注入)",
|
||||
"ext_regex_title": "正则",
|
||||
"ext_regex_new_global_script_desc": "新的全局正则表达式脚本",
|
||||
"ext_regex_new_global_script": "新建全局正则",
|
||||
"ext_regex_new_scoped_script_desc": "新的作用域正则表达式脚本",
|
||||
"ext_regex_new_scoped_script": "新建局部正则",
|
||||
"ext_regex_import_script": "导入正则",
|
||||
"ext_regex_global_scripts": "全局正则脚本",
|
||||
"ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",
|
||||
"ext_regex_scoped_scripts": "局部正则脚本",
|
||||
"ext_regex_allow_scoped": "允许使用范围正则表达式",
|
||||
"ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
|
||||
"Regex Editor": "正则表达式编辑器",
|
||||
"Test Mode": "测试模式",
|
||||
@ -1452,6 +1469,19 @@
|
||||
"Slash Command": "快捷命令",
|
||||
"Interactive Mode": "交互模式",
|
||||
"Image Prompt Templates": "图像提示模板",
|
||||
"ext_translate_btn_chat": "翻译聊天",
|
||||
"ext_translate_btn_input": "翻译输入",
|
||||
"ext_translate_delete_confirm_1": "你确定吗?",
|
||||
"ext_translate_delete_confirm_2": "这将从当前聊天中的所有消息中删除翻译文本。此操作无法撤消。",
|
||||
"ext_translate_title": "聊天翻译",
|
||||
"ext_translate_auto_mode": "自动模式",
|
||||
"ext_translate_mode_none": "没有任何",
|
||||
"ext_translate_mode_responses": "翻译回复",
|
||||
"ext_translate_mode_inputs": "翻译输入",
|
||||
"ext_translate_mode_both": "翻译两者",
|
||||
"ext_translate_mode_provider": "提供者",
|
||||
"ext_translate_target_lang": "目标语言",
|
||||
"ext_translate_clear": "清空设置",
|
||||
"Vector Storage": "向量存储",
|
||||
"Vectorization Source": "向量化源",
|
||||
"Local (Transformers)": "本地(Transformers)",
|
||||
@ -1510,6 +1540,12 @@
|
||||
"Current Password:": "当前密码:",
|
||||
"New Password:": "新密码:",
|
||||
"Confirm New Password:": "确认新密码:",
|
||||
"Include Body Parameters": "包括主体参数",
|
||||
"custom_include_body_desc": "聊天完成请求主体中要包含的参数(YAML 对象)\n\n示例:\n- top_k:20\n- repetition_penalty:1.1",
|
||||
"Exclude Body Parameters": "排除主体参数",
|
||||
"custom_exclude_body_desc": "要从聊天完成请求主体中排除的参数(YAML 数组)\n\n示例:\n- frequency_penalty\n- presence_penalty",
|
||||
"Include Request Headers": "包含请求标头",
|
||||
"custom_include_headers_desc": "聊天完成请求的附加标头(YAML 对象)\n\n示例:\n- CustomHeader:自定义值\n- AnotherHeader:自定义值",
|
||||
"Debug Warning": "此类别中的功能仅供高级用户使用。如果您不确定后果,请不要点击任何内容。",
|
||||
"Are you sure you want to delete this user?": "您确定要删除该用户吗?",
|
||||
"Deleting:": "删除:",
|
||||
@ -1517,6 +1553,76 @@
|
||||
"Warning:": "警告:",
|
||||
"This action is irreversible.": "此操作不可逆。",
|
||||
"Type the user's handle below to confirm:": "在下面输入用户的名称以确认:",
|
||||
"help_format_1": "文本格式化命令:",
|
||||
"help_format_2": "*文本*",
|
||||
"help_format_3": "显示为",
|
||||
"help_format_4": "斜体",
|
||||
"help_format_5": "**文本**",
|
||||
"help_format_6": "显示为",
|
||||
"help_format_7": "大胆的",
|
||||
"help_format_8": "***文本***",
|
||||
"help_format_9": "显示为",
|
||||
"help_format_10": "粗斜体",
|
||||
"help_format_11": "__文本__",
|
||||
"help_format_12": "显示为",
|
||||
"help_format_13": "强调",
|
||||
"help_format_14": "~~文本~~",
|
||||
"help_format_15": "显示为",
|
||||
"help_format_16": "删除线",
|
||||
"help_format_17": "[文本](网址)",
|
||||
"help_format_18": "显示为",
|
||||
"help_format_19": "超级链接",
|
||||
"help_format_20": "",
|
||||
"help_format_21": "显示为图像",
|
||||
"help_format_22": "```文本```",
|
||||
"help_format_23": "显示为代码块(反引号之间允许换行)",
|
||||
"help_format_like_this": "像这样",
|
||||
"help_format_24": "`文本`",
|
||||
"help_format_25": "显示为",
|
||||
"help_format_26": "内联代码",
|
||||
"help_format_27": "> 文本",
|
||||
"help_format_28": "显示为块引用(请注意 > 后面的空格)",
|
||||
"help_format_29": "# 文本",
|
||||
"help_format_30": "显示为大标题(注意空格)",
|
||||
"help_format_32": "## 文本",
|
||||
"help_format_33": "显示为中等标题(注意空格)",
|
||||
"help_format_35": "### 文本",
|
||||
"help_format_36": "显示为小标题(注意空格)",
|
||||
"help_format_38": "$$ 文本 $$",
|
||||
"help_format_39": "呈现 LaTeX 公式(如果启用)",
|
||||
"help_format_40": "$文本$",
|
||||
"help_format_41": "呈现 AsciiMath 公式(如果启用)",
|
||||
"help_1": "您好!请选择您想要详细了解的帮助主题:",
|
||||
"help_2": "斜线命令",
|
||||
"help_or": "或者",
|
||||
"help_3": "格式化",
|
||||
"help_4": "热键",
|
||||
"help_5": "{{宏}}",
|
||||
"help_6": "还有其他问题吗?",
|
||||
"help_7": "SillyTavern 官方文档网站",
|
||||
"help_8": "有更多信息!",
|
||||
"help_hotkeys_0": "热键/按键绑定",
|
||||
"help_hotkeys_1": "向上",
|
||||
"help_hotkeys_2": "编辑聊天中的最后一条消息",
|
||||
"help_hotkeys_3": "Ctrl+向上",
|
||||
"help_hotkeys_4": "编辑聊天中的最后一条用户消息",
|
||||
"help_hotkeys_5": "左边",
|
||||
"help_hotkeys_6": "向左滑动",
|
||||
"help_hotkeys_7": "正确的",
|
||||
"help_hotkeys_8": "向右滑动(注意:当聊天栏中输入内容时,滑动热键将被禁用)",
|
||||
"help_hotkeys_9": "进入",
|
||||
"help_hotkeys_10": "(选中聊天栏)",
|
||||
"help_hotkeys_10_1": "向 AI 发送消息",
|
||||
"help_hotkeys_11": "Ctrl+Enter",
|
||||
"help_hotkeys_12": "重新生成最后的 AI 响应",
|
||||
"help_hotkeys_13": "Alt+Enter",
|
||||
"help_hotkeys_14": "继续上一次AI响应",
|
||||
"help_hotkeys_15": "逃脱",
|
||||
"help_hotkeys_16": "停止 AI 响应生成、关闭 UI 面板、取消消息编辑",
|
||||
"help_hotkeys_17": "Ctrl+Shift+向上",
|
||||
"help_hotkeys_18": "滚动到上下文行",
|
||||
"help_hotkeys_19": "Ctrl+Shift+向下",
|
||||
"help_hotkeys_20": "滚动聊天到底部",
|
||||
"Import Characters": "导入角色",
|
||||
"Enter the URL of the content to import": "输入要导入的内容的URL",
|
||||
"Supported sources:": "支持的来源:",
|
||||
@ -1531,6 +1637,87 @@
|
||||
"char_import_8": "RisuRealm 角色(直链)",
|
||||
"Supports importing multiple characters.": "支持导入多个角色。",
|
||||
"Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
|
||||
"System-wide Replacement Macros (in order of evaluation):": "系统范围的替换宏(按评估顺序):",
|
||||
"help_macros_1": "仅适用于斜线命令批处理。替换为上一个命令的返回结果。",
|
||||
"help_macros_2": "仅插入一个换行符。",
|
||||
"help_macros_3": "修剪此宏周围的换行符。",
|
||||
"help_macros_4": "没有操作,只是一个空字符串。",
|
||||
"help_macros_5": "API 设置中定义的全局提示。仅在高级定义提示覆盖中有效。",
|
||||
"help_macros_6": "用户输入",
|
||||
"help_macros_7": "角色的主提示覆盖",
|
||||
"help_macros_8": "角色越狱提示覆盖",
|
||||
"help_macros_9": "角色描述",
|
||||
"help_macros_10": "人物性格",
|
||||
"help_macros_11": "角色场景",
|
||||
"help_macros_12": "您当前的角色描述",
|
||||
"help_macros_13": "角色对话示例",
|
||||
"help_macros_14": "未格式化的对话示例",
|
||||
"(only for Story String)": "(仅适用于故事字符串)",
|
||||
"help_macros_15": "您当前的 Persona 用户名",
|
||||
"help_macros_16": "角色的名字",
|
||||
"help_macros_17": "角色的版本号",
|
||||
"help_macros_18": "以逗号分隔的群组成员名称列表或单人聊天中的角色名称。别名:{{charIfNotGroup}}",
|
||||
"help_macros_19": "当前选定的 API 的文本生成模型名称。",
|
||||
"Can be inaccurate!": "可能不准确!",
|
||||
"help_macros_20": "最新聊天消息的文本。",
|
||||
"help_macros_21": "最新聊天消息的索引号。对于斜线命令批处理很有用。",
|
||||
"help_macros_22": "上下文中包含的第一条消息的 ID。要求在当前会话中至少运行一次生成。",
|
||||
"help_macros_23": "最后一条聊天消息中当前滑动的 ID(以 1 为基数)。如果最后一条消息是用户或提示隐藏的,则为空字符串。",
|
||||
"help_macros_24": "最后一条聊天消息中的滑动次数。如果最后一条消息是用户隐藏或提示隐藏的,则为空字符串。",
|
||||
"help_macros_25": "您可以在此处留言,宏将被替换为空白内容。AI 看不到。",
|
||||
"help_macros_26": "当前时间",
|
||||
"help_macros_27": "当前日期",
|
||||
"help_macros_28": "当前工作日",
|
||||
"help_macros_29": "当前 ISO 时间(24 小时制)",
|
||||
"help_macros_30": "当前 ISO 日期 (YYYY-MM-DD)",
|
||||
"help_macros_31": "指定格式的当前日期/时间,例如德国日期/时间:",
|
||||
"help_macros_32": "指定 UTC 时区偏移量的当前时间,例如 UTC-4 或 UTC+2",
|
||||
"help_macros_33": "time1 和 time2 之间的时间差。接受时间和日期宏。(例如:{{timeDiff::{{isodate}} {{time}}::2024/5/11 12:30:00}})",
|
||||
"help_macros_34": "距离上次用户消息发送的时间",
|
||||
"help_macros_35": "为 AI 设置行为偏差,直到下一个用户输入。文本周围的引号很重要。",
|
||||
"help_macros_36": "掷骰子。(例如:",
|
||||
"space_ will roll a 6-sided dice and return a number between 1 and 6)": "将掷一个 6 面骰子并返回 1 到 6 之间的数字)",
|
||||
"help_macros_37": "从列表中返回一个随机项目。(例如:",
|
||||
"space_ will return 1 of the 4 numbers at random. Works with text lists too.": "将随机返回 4 个数字中的 1 个。也适用于文本列表。",
|
||||
"help_macros_38": "随机的替代语法允许在列表项中使用逗号。",
|
||||
"help_macros_39": "从列表中随机挑选一项。工作原理与 {{random}} 相同,具有相同的语法选项,但一旦挑选,挑选将一直持续到本次聊天,不会在连续消息和提示处理中重新滚动。",
|
||||
"help_macros_40": "如果使用文本生成 WebUI 后端,则动态地将引号中的文本添加到禁用单词序列中。对其他后端不执行任何操作。可以在任何地方使用(角色描述、WI、AN 等)。文本周围的引号很重要。",
|
||||
"Instruct Mode and Context Template Macros:": "指导模式和上下文模板宏:",
|
||||
"(enabled in the Advanced Formatting settings)": "(在高级格式设置中启用)",
|
||||
"help_macros_41": "令牌中允许的最大提示长度 = (上下文大小 - 响应长度)",
|
||||
"help_macros_42": "上下文模板示例对话分隔符",
|
||||
"help_macros_43": "上下文模板聊天开始行",
|
||||
"help_macros_44": "主系统提示(如果选择,则覆盖字符提示,或 instructSystemPrompt)",
|
||||
"help_macros_45": "指示系统提示",
|
||||
"help_macros_46": "指示系统提示前缀序列",
|
||||
"help_macros_47": "指示系统提示后缀序列",
|
||||
"help_macros_48": "指示用户前缀序列",
|
||||
"help_macros_49": "指示用户后缀序列",
|
||||
"help_macros_50": "指导助理前缀序列",
|
||||
"help_macros_51": "指导助理后缀序列",
|
||||
"help_macros_52": "指导助理第一个输出序列",
|
||||
"help_macros_53": "指导助手最后输出序列",
|
||||
"help_macros_54": "指示系统消息前缀序列",
|
||||
"help_macros_55": "指示系统消息后缀序列",
|
||||
"help_macros_56": "指示系统指令前缀",
|
||||
"help_macros_57": "指示第一个用户消息填充器",
|
||||
"help_macros_58": "指示停止顺序",
|
||||
"Chat variables Macros:": "聊天变量宏:",
|
||||
"Local variables = unique to the current chat": "局部变量 = 当前聊天所独有",
|
||||
"Global variables = works in any chat for any character": "全局变量 = 适用于任何角色的任何聊天",
|
||||
"Scoped variables = works in STscript": "范围变量 = 在 STscript 中有效",
|
||||
"help_macros_59": "替换为局部变量“name”的值",
|
||||
"help_macros_60": "替换为空字符串,将局部变量“name”设置为“value”",
|
||||
"help_macros_61": "替换为空字符串,将“increment”的数值添加到局部变量“name”",
|
||||
"help_macros_62": "替换为变量“name”的值增加 1 的结果",
|
||||
"help_macros_63": "替换为变量“name”的值减 1 的结果",
|
||||
"help_macros_64": "替换为全局变量“name”的值",
|
||||
"help_macros_65": "替换为空字符串,将全局变量“name”设置为“value”",
|
||||
"help_macros_66": "替换为空字符串,将“increment”的数值添加到全局变量“name”",
|
||||
"help_macros_67": "替换为全局变量“name”的值增加 1 的结果",
|
||||
"help_macros_68": "替换为全局变量“name”的值减 1 的结果",
|
||||
"help_macros_69": "替换为范围变量“name”的值",
|
||||
"help_macros_70": "用范围变量“name”的索引处的项目值(对于数组/列表或对象/字典)替换",
|
||||
"Export for character": "导出角色",
|
||||
"Export prompts for this character, including their order.": "导出此角色的提示词,包括其顺序。",
|
||||
"Export all": "全部导出",
|
||||
@ -1550,17 +1737,6 @@
|
||||
"Record a snapshot of your current settings.": "记录当前设置的快照。",
|
||||
"Make a Snapshot": "制作快照",
|
||||
"Restore this snapshot": "恢复此快照",
|
||||
"ext_translate_btn_chat": "翻译聊天",
|
||||
"ext_translate_btn_input": "翻译输入",
|
||||
"Chat Translation": "聊天翻译",
|
||||
"ext_translate_auto_mode": "自动模式",
|
||||
"ext_translate_mode_none": "没有任何",
|
||||
"ext_translate_mode_responses": "翻译回复",
|
||||
"ext_translate_mode_inputs": "翻译输入",
|
||||
"ext_translate_mode_both": "翻译两者",
|
||||
"ext_translate_mode_provider": "提供者",
|
||||
"ext_translate_target_lang": "目标语言",
|
||||
"ext_translate_clear": "清空设置",
|
||||
"Hi,": "嘿,",
|
||||
"To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器",
|
||||
"set to true in the config.yaml file.": "在 config.yaml 文件中设置为 true。",
|
||||
|
@ -886,11 +886,11 @@
|
||||
"Bulk_edit_characters": "批量編輯角色人物\n\n點選以切換角色人物\nShift + 點選以選擇/取消選擇一範圍的角色人物\n右鍵以查看動作",
|
||||
"Bulk select all characters": "全選所有角色人物",
|
||||
"Bulk delete characters": "批量刪除角色人物",
|
||||
"popup_text_save": "儲存",
|
||||
"popup_text_yes": "是",
|
||||
"popup_text_no": "否",
|
||||
"popup_text_cancel": "取消",
|
||||
"popup_text_import": "匯入",
|
||||
"popup-button-save": "儲存",
|
||||
"popup-button-yes": "是",
|
||||
"popup-button-no": "否",
|
||||
"popup-button-cancel": "取消",
|
||||
"popup-button-import": "匯入",
|
||||
"Advanced Defininitions": "- 進階定義",
|
||||
"Prompt Overrides": "提示詞覆寫",
|
||||
"(For Chat Completion and Instruct Mode)": "(用於聊天補充和指令模式)",
|
||||
|
@ -227,7 +227,7 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de
|
||||
import { initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros } from './scripts/macros.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 { ScraperManager } from './scripts/scrapers.js';
|
||||
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
|
||||
@ -235,6 +235,9 @@ import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.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';
|
||||
|
||||
//exporting functions and vars for mods
|
||||
export {
|
||||
@ -263,6 +266,19 @@ showLoader();
|
||||
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
|
||||
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
|
||||
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
|
||||
if ('target' in node) {
|
||||
@ -509,11 +525,15 @@ let is_delete_mode = false;
|
||||
let fav_ch_checked = false;
|
||||
let scrollLock = false;
|
||||
export let abortStatusCheck = new AbortController();
|
||||
let charDragDropHandler = null;
|
||||
|
||||
/** @type {number} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
|
||||
const durationSaveEdit = debounce_timeout.relaxed;
|
||||
export const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit);
|
||||
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit);
|
||||
/** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
|
||||
export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed;
|
||||
/** @type {debounce_timeout} The debounce timeout used for printing. debounce_timeout.quick: 100 ms */
|
||||
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.
|
||||
@ -521,7 +541,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.
|
||||
*/
|
||||
export const printCharactersDebounced = debounce(() => { printCharacters(false); }, debounce_timeout.quick);
|
||||
export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT);
|
||||
|
||||
/**
|
||||
* @enum {string} System message types
|
||||
@ -885,6 +905,8 @@ async function firstLoadInit() {
|
||||
await getSystemMessages();
|
||||
sendSystemMessage(system_message_types.WELCOME);
|
||||
await getSettings();
|
||||
initKeyboard();
|
||||
initDynamicStyles();
|
||||
initTags();
|
||||
await getUserAvatars(true, user_avatar);
|
||||
await getCharacters();
|
||||
@ -912,7 +934,7 @@ function cancelStatusCheck() {
|
||||
export function displayOnlineStatus() {
|
||||
if (online_status == 'no_connection') {
|
||||
$('.online_status_indicator').removeClass('success');
|
||||
$('.online_status_text').text('No connection...');
|
||||
$('.online_status_text').text($('#API-status-top').attr('no_connection_text'));
|
||||
} else {
|
||||
$('.online_status_indicator').addClass('success');
|
||||
$('.online_status_text').text(online_status);
|
||||
@ -925,6 +947,8 @@ export function displayOnlineStatus() {
|
||||
*/
|
||||
export function setAnimationDuration(ms = null) {
|
||||
animation_duration = ms ?? ANIMATION_DURATION_DEFAULT;
|
||||
// Set CSS variable to document
|
||||
document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`);
|
||||
}
|
||||
|
||||
export function setActiveCharacter(entityOrKey) {
|
||||
@ -5482,7 +5506,7 @@ export async function renameCharacter(name = null, { silent = false, renameChats
|
||||
}
|
||||
|
||||
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) {
|
||||
toastr.warning('No character name provided.', 'Rename Character');
|
||||
@ -5730,7 +5754,7 @@ async function read_avatar_load(input) {
|
||||
}
|
||||
|
||||
await createOrEditCharacter();
|
||||
await delay(durationSaveEdit);
|
||||
await delay(DEFAULT_SAVE_EDIT_TIMEOUT);
|
||||
|
||||
const formData = new FormData($('#form_create').get(0));
|
||||
await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
|
||||
@ -5774,7 +5798,7 @@ export function getThumbnailUrl(type, 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) {
|
||||
block.empty();
|
||||
}
|
||||
@ -5809,8 +5833,8 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
|
||||
avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
|
||||
}
|
||||
|
||||
if (selectable) {
|
||||
avatarTemplate.addClass('selectable');
|
||||
if (interactable) {
|
||||
avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS);
|
||||
avatarTemplate.toggleClass('character_select', entity.type === 'character');
|
||||
avatarTemplate.toggleClass('group_select', entity.type === 'group');
|
||||
}
|
||||
@ -6847,7 +6871,7 @@ export function select_selected_character(chid) {
|
||||
|
||||
$('#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);
|
||||
$('#description_textarea').val(characters[chid].description);
|
||||
$('#character_world').val(characters[chid].data?.extensions?.world || '');
|
||||
@ -6924,7 +6948,7 @@ function select_rm_create() {
|
||||
//create text poles
|
||||
$('#rm_button_back').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);
|
||||
$('#description_textarea').val(create_save.description);
|
||||
$('#character_world').val(create_save.world);
|
||||
@ -7070,10 +7094,10 @@ function onScenarioOverrideRemoveClick() {
|
||||
* @param {string} type
|
||||
* @param {string} inputValue - Value to set the input to.
|
||||
* @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
|
||||
*/
|
||||
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() {
|
||||
if (['avatarToCrop'].includes(popup_type)) {
|
||||
return okButton ?? 'Accept';
|
||||
@ -7103,6 +7127,7 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, l
|
||||
const $shadowPopup = $('#shadow_popup');
|
||||
|
||||
$dialoguePopup.toggleClass('wide_dialogue_popup', !!wide)
|
||||
.toggleClass('wider_dialogue_popup', !!wider)
|
||||
.toggleClass('large_dialogue_popup', !!large)
|
||||
.toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling)
|
||||
.toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
|
||||
@ -7215,7 +7240,7 @@ export async function saveMetadata() {
|
||||
|
||||
export async function saveChatConditional() {
|
||||
try {
|
||||
await waitUntilCondition(() => !isChatSaving, durationSaveEdit, 100);
|
||||
await waitUntilCondition(() => !isChatSaving, DEFAULT_SAVE_EDIT_TIMEOUT, 100);
|
||||
} catch {
|
||||
console.warn('Timeout waiting for chat to save');
|
||||
return;
|
||||
@ -7592,7 +7617,7 @@ async function createOrEditCharacter(e) {
|
||||
field.callback && field.callback(fieldValue);
|
||||
});
|
||||
|
||||
$('#character_popup_text_h3').text('Create character');
|
||||
$('#character_popup-button-h3').text('Create character');
|
||||
|
||||
create_save.avatar = '';
|
||||
|
||||
@ -8943,7 +8968,7 @@ jQuery(async function () {
|
||||
$('#send_textarea').on('focusin focus click', () => {
|
||||
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) {
|
||||
$('#send_textarea').focus();
|
||||
}
|
||||
@ -9452,7 +9477,7 @@ jQuery(async function () {
|
||||
}
|
||||
|
||||
function isMouseOverButtonOrMenu() {
|
||||
return menu.is(':hover') || button.is(':hover');
|
||||
return menu.is(':hover, :focus-within') || button.is(':hover, :focus');
|
||||
}
|
||||
|
||||
button.on('click', function () {
|
||||
@ -10352,7 +10377,7 @@ jQuery(async function () {
|
||||
'#character_cross',
|
||||
'#avatar-and-name-block',
|
||||
'#shadow_popup',
|
||||
'.shadow_popup',
|
||||
'.popup',
|
||||
'#world_popup',
|
||||
'.ui-widget',
|
||||
'.text_pole',
|
||||
@ -10657,7 +10682,7 @@ jQuery(async function () {
|
||||
const html = await renderTemplateAsync('importCharacters');
|
||||
|
||||
/** @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) {
|
||||
console.debug('Custom content import cancelled');
|
||||
@ -10712,32 +10737,12 @@ jQuery(async function () {
|
||||
}
|
||||
});
|
||||
|
||||
const $dropzone = $(document.body);
|
||||
|
||||
$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);
|
||||
charDragDropHandler = new DragAndDropHandler('body', async (files, event) => {
|
||||
if (!files.length) {
|
||||
await importFromURL(event.originalEvent.dataTransfer.items, files);
|
||||
}
|
||||
await processDroppedFiles(files);
|
||||
});
|
||||
|
||||
}, { noAnimation: true });
|
||||
|
||||
$('#charListGridToggle').on('click', async () => {
|
||||
doCharListDisplaySwitch();
|
||||
@ -10769,4 +10774,4 @@ jQuery(async function () {
|
||||
});
|
||||
|
||||
initCustomSelectedSamplers();
|
||||
});
|
||||
});
|
@ -303,7 +303,7 @@ export async function favsToHotswap() {
|
||||
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
|
||||
|
@ -6,6 +6,7 @@ import { BlankAutoCompleteOption } from './BlankAutoCompleteOption.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
|
||||
import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
|
||||
import { Popup, getTopmostModalLayer } from '../popup.js';
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Number}*/
|
||||
@ -442,7 +443,7 @@ export class AutoComplete {
|
||||
}
|
||||
this.dom.append(frag);
|
||||
this.updatePosition();
|
||||
document.body.append(this.domWrap);
|
||||
getTopmostModalLayer().append(this.domWrap);
|
||||
} else {
|
||||
this.domWrap.remove();
|
||||
}
|
||||
@ -457,7 +458,7 @@ export class AutoComplete {
|
||||
if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
|
||||
this.detailsDom.innerHTML = '';
|
||||
this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
|
||||
document.body.append(this.detailsWrap);
|
||||
getTopmostModalLayer().append(this.detailsWrap);
|
||||
this.updateDetailsPositionDebounced();
|
||||
}
|
||||
|
||||
@ -473,7 +474,7 @@ export class AutoComplete {
|
||||
const rect = {};
|
||||
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.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.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
|
||||
this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
|
||||
@ -500,7 +501,7 @@ export class AutoComplete {
|
||||
const rect = {};
|
||||
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.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) {
|
||||
this.detailsWrap.classList.remove('full');
|
||||
const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
|
||||
@ -596,7 +597,7 @@ export class AutoComplete {
|
||||
}
|
||||
this.clone.style.position = 'fixed';
|
||||
this.clone.style.visibility = 'hidden';
|
||||
document.body.append(this.clone);
|
||||
getTopmostModalLayer().append(this.clone);
|
||||
const mo = new MutationObserver(muts=>{
|
||||
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
|
||||
this.clone.remove();
|
||||
|
@ -94,6 +94,9 @@ function enableBulkSelect() {
|
||||
});
|
||||
$(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');
|
||||
// We also need to disable the default click event for the character_select divs
|
||||
$(document).on('click', '.bulk_select_checkbox', function (event) {
|
||||
@ -106,6 +109,8 @@ function enableBulkSelect() {
|
||||
*/
|
||||
function disableBulkSelect() {
|
||||
$('.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');
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
import { ScraperManager } from './scrapers.js';
|
||||
import { DragAndDropHandler } from './dragdrop.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} FileAttachment
|
||||
@ -991,49 +992,24 @@ async function openAttachmentManager() {
|
||||
template.find('.chatAttachmentsName').text(chatName);
|
||||
}
|
||||
|
||||
function addDragAndDrop() {
|
||||
$(document.body).on('dragover', '.dialogue_popup', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.target).closest('.dialogue_popup').addClass('dragover');
|
||||
const dragDropHandler = new DragAndDropHandler('.popup', async (files, event) => {
|
||||
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());
|
||||
});
|
||||
|
||||
$(document.body).on('dragleave', '.dialogue_popup', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.target).closest('.dialogue_popup').removeClass('dragover');
|
||||
});
|
||||
|
||||
$(document.body).on('drop', '.dialogue_popup', async (event) => {
|
||||
event.preventDefault();
|
||||
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');
|
||||
}
|
||||
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();
|
||||
});
|
||||
|
||||
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
|
||||
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
|
||||
@ -1129,11 +1105,10 @@ async function openAttachmentManager() {
|
||||
const cleanupFn = await renderButtons();
|
||||
await verifyAttachments();
|
||||
await renderAttachments();
|
||||
addDragAndDrop();
|
||||
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
|
||||
|
||||
cleanupFn();
|
||||
removeDragAndDrop();
|
||||
dragDropHandler.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
|
107
public/scripts/dragdrop.js
vendored
Normal file
107
public/scripts/dragdrop.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
162
public/scripts/dynamic-styles.js
Normal file
162
public/scripts/dynamic-styles.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
@ -349,12 +349,12 @@ function autoConnectInputHandler() {
|
||||
|
||||
function addExtensionsButtonAndMenu() {
|
||||
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>';
|
||||
|
||||
$(document.body).append(extensionsMenuHTML);
|
||||
|
||||
$('#leftSendForm').prepend(buttonHTML);
|
||||
$('#leftSendForm').append(buttonHTML);
|
||||
|
||||
const button = $('#extensionsMenuButton');
|
||||
const dropdown = $('#extensionsMenu');
|
||||
|
@ -21,7 +21,7 @@
|
||||
<div class="expression_api_block m-b-1 m-t-1">
|
||||
<label for="expression_api" data-i18n="Classifier API">Classifier API</label>
|
||||
<small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>
|
||||
<select id="expression_api" class="flex1 margin0" data-i18n="Expression API" placeholder="Expression API">
|
||||
<select id="expression_api" class="flex1 margin0">
|
||||
<option value="0" data-i18n="Local">Local</option>
|
||||
<option value="1" data-i18n="Extras">Extras</option>
|
||||
<option value="2" data-i18n="LLM">LLM</option>
|
||||
|
@ -11,6 +11,7 @@ import { dragElement } from '../../RossAscends-mods.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { DragAndDropHandler } from '../../dragdrop.js';
|
||||
|
||||
const extensionName = 'gallery';
|
||||
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
|
||||
@ -56,7 +57,8 @@ async function getGalleryItems(url) {
|
||||
* @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
|
||||
*/
|
||||
async function initGallery(items, url) {
|
||||
$('#dragGallery').nanogallery2({
|
||||
const gallery = $('#dragGallery');
|
||||
gallery.nanogallery2({
|
||||
'items': items,
|
||||
thumbnailWidth: 'auto',
|
||||
thumbnailHeight: thumbnailHeight,
|
||||
@ -80,44 +82,24 @@ async function initGallery(items, url) {
|
||||
|
||||
|
||||
eventSource.on('resizeUI', function (elmntName) {
|
||||
jQuery('#dragGallery').nanogallery2('resize');
|
||||
gallery.nanogallery2('resize');
|
||||
});
|
||||
|
||||
const dropZone = $('#dragGallery');
|
||||
//remove any existing handlers
|
||||
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];
|
||||
const dragDropHandler = new DragAndDropHandler('#dragGallery', async (files, event) => {
|
||||
let file = files[0];
|
||||
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
|
||||
await delay(100);
|
||||
//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
|
||||
jQuery('#dragGallery').nanogallery2('resize');
|
||||
gallery.nanogallery2('resize');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -220,68 +220,68 @@
|
||||
align-items: baseline;
|
||||
}
|
||||
@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;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
height: 50svh;
|
||||
}
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) {
|
||||
.popup:has(#qr--modalEditor) {
|
||||
aspect-ratio: unset;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text {
|
||||
.popup:has(#qr--modalEditor) .popup-content {
|
||||
display: flex;
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
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;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
@ -289,35 +289,35 @@
|
||||
font-size: smaller;
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
display: grid;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #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) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message {
|
||||
.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) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
|
||||
.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) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax {
|
||||
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
padding: 0;
|
||||
@ -327,10 +327,10 @@
|
||||
min-width: 100%;
|
||||
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%;
|
||||
}
|
||||
.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;
|
||||
@ -338,22 +338,22 @@
|
||||
caret-color: var(--ac-style-color-text);
|
||||
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,
|
||||
.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,
|
||||
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection {
|
||||
.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection {
|
||||
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) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
.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) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner {
|
||||
.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;
|
||||
margin: 0;
|
||||
@ -363,11 +363,11 @@
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
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;
|
||||
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-style: solid;
|
||||
display: flex;
|
||||
@ -375,42 +375,42 @@
|
||||
gap: 0.5em;
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
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;
|
||||
opacity: 0.5;
|
||||
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;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #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-pause,
|
||||
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
pointer-events: none;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #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-pause,
|
||||
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
filter: grayscale(0);
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
--progColor: #92befc;
|
||||
--progFlashColor: #d78872;
|
||||
@ -421,7 +421,7 @@
|
||||
background-color: var(--black50a);
|
||||
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: '';
|
||||
background-color: var(--progColor);
|
||||
position: absolute;
|
||||
@ -429,23 +429,23 @@
|
||||
right: calc(100% - var(--prog) * 1%);
|
||||
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-duration: 1500ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-delay: 0s;
|
||||
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);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.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;
|
||||
text-align: left;
|
||||
font-size: smaller;
|
||||
@ -456,10 +456,10 @@
|
||||
min-width: 100%;
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
text-align: left;
|
||||
font-size: smaller;
|
||||
@ -470,10 +470,10 @@
|
||||
min-width: 100%;
|
||||
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;
|
||||
}
|
||||
.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: ';
|
||||
}
|
||||
@keyframes qr--progressPulse {
|
||||
@ -485,6 +485,6 @@
|
||||
background-color: var(--progFlashColor);
|
||||
}
|
||||
}
|
||||
.shadow_popup.qr--hide {
|
||||
.popup.qr--hide {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
@ -244,7 +244,7 @@
|
||||
|
||||
|
||||
@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;
|
||||
overflow: auto;
|
||||
> #qr--main {
|
||||
@ -259,10 +259,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) {
|
||||
.popup:has(#qr--modalEditor) {
|
||||
aspect-ratio: unset;
|
||||
|
||||
.dialogue_popup_text {
|
||||
.popup-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -507,6 +507,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.shadow_popup.qr--hide {
|
||||
.popup.qr--hide {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
@ -8,11 +8,11 @@
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<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>
|
||||
<small data-i18n="ext_regex_new_global_script">+ Global</small>
|
||||
</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>
|
||||
<small data-i18n="ext_regex_new_scoped_script">+ Scoped</small>
|
||||
</div>
|
||||
@ -39,7 +39,7 @@
|
||||
<label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle">
|
||||
<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-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>
|
||||
</div>
|
||||
<small data-i18n="ext_regex_scoped_scripts_desc">
|
||||
|
@ -438,7 +438,7 @@ async function loadSettings() {
|
||||
$('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
|
||||
$('#sd_snap').prop('checked', extension_settings.sd.snap);
|
||||
$('#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_free_extend').prop('checked', extension_settings.sd.free_extend);
|
||||
$('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible);
|
||||
@ -711,7 +711,7 @@ function onChatChanged() {
|
||||
adjustElementScrollHeight();
|
||||
}
|
||||
|
||||
function adjustElementScrollHeight(){
|
||||
function adjustElementScrollHeight() {
|
||||
if (!$('.sd_settings').is(':visible')) {
|
||||
return;
|
||||
}
|
||||
@ -821,7 +821,7 @@ function onInteractiveVisibleInput() {
|
||||
|
||||
function onClipSkipInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -832,13 +832,13 @@ function onSeedInput() {
|
||||
|
||||
function onScaleInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
function onStepsInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -901,13 +901,23 @@ function onSchedulerChange() {
|
||||
|
||||
function onWidthInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
function onHeightInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -947,7 +957,7 @@ async function onViewAnlasClick() {
|
||||
|
||||
function onNovelUpscaleRatioInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -1049,19 +1059,19 @@ function onHrUpscalerChange() {
|
||||
|
||||
function onHrScaleInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
function onDenoisingStrengthInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
function onHrSecondPassStepsInput() {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -3439,6 +3449,7 @@ jQuery(async () => {
|
||||
$('#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 () {
|
||||
initScrollHeight($('#sd_prompt_prefix'));
|
||||
|
@ -128,16 +128,20 @@
|
||||
<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>
|
||||
<div class="flex-container">
|
||||
<label for="sd_openai_style" data-i18n="Image Style">Image Style</label>
|
||||
<select id="sd_openai_style">
|
||||
<option value="vivid">Vivid</option>
|
||||
<option value="natural">Natural</option>
|
||||
</select>
|
||||
<label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
|
||||
<select id="sd_openai_quality">
|
||||
<option value="standard" data-i18n="Standard">Standard</option>
|
||||
<option value="hd" data-i18n="HD">HD</option>
|
||||
</select>
|
||||
<div class="flex1">
|
||||
<label for="sd_openai_style" data-i18n="Image Style">Image Style</label>
|
||||
<select id="sd_openai_style">
|
||||
<option value="vivid">Vivid</option>
|
||||
<option value="natural">Natural</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
|
||||
<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 data-sd-source="comfy">
|
||||
@ -185,91 +189,164 @@
|
||||
</label>
|
||||
</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 data-sd-source="novel" class="marginTopBot5">
|
||||
<label class="checkbox_label" for="sd_novel_decrisper" title="Reduce artifacts caused by high guidance values.">
|
||||
<input id="sd_novel_decrisper" type="checkbox" />
|
||||
<span data-i18n="Decrisper">
|
||||
Decrisper
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="flex1">
|
||||
<label for="sd_model" data-i18n="Model">Model</label>
|
||||
<select id="sd_model"></select>
|
||||
</div>
|
||||
|
||||
<div class="flex1" data-sd-source="comfy">
|
||||
<label for="sd_vae">VAE</label>
|
||||
<select id="sd_vae"></select>
|
||||
</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}}" />
|
||||
<label for="sd_width" data-i18n="Width">Width (<span id="sd_width_value"></span>)</label>
|
||||
<input id="sd_width" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{width}}" />
|
||||
<label for="sd_height" data-i18n="Height">Height (<span id="sd_height_value"></span>)</label>
|
||||
<input id="sd_height" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{height}}" />
|
||||
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
|
||||
<select id="sd_resolution"><!-- Populated in JS --></select>
|
||||
<label for="sd_model" data-i18n="Model">Model</label>
|
||||
<select id="sd_model"></select>
|
||||
<label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
|
||||
<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 class="flex-container">
|
||||
<div class="flex1">
|
||||
<label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
|
||||
<select id="sd_sampler"></select>
|
||||
</div>
|
||||
|
||||
<div class="flex1" data-sd-source="comfy,auto">
|
||||
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
|
||||
<select id="sd_scheduler"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div data-sd-source="comfy,auto">
|
||||
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
|
||||
<select id="sd_scheduler"></select>
|
||||
|
||||
<div class="flex-container">
|
||||
<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 data-sd-source="comfy">
|
||||
<label for="sd_vae">VAE</label>
|
||||
<select id="sd_vae"></select>
|
||||
|
||||
<div class="flex-container">
|
||||
<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 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">
|
||||
<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 class="flex1 checkbox_label">
|
||||
<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>
|
||||
</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>
|
||||
<input type="range" id="sd_clip_skip" min="{{clip_skip_min}}" max="{{clip_skip_max}}" step="{{clip_skip_step}}" value="{{clip_skip}}" />
|
||||
</div>
|
||||
<div data-sd-source="auto,vlad,drawthings">
|
||||
<label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label>
|
||||
<select id="sd_hr_upscaler"></select>
|
||||
<label for="sd_hr_scale"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_hr_scale_value"></span>)</label>
|
||||
<input id="sd_hr_scale" type="range" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" />
|
||||
</div>
|
||||
<div data-sd-source="auto,vlad">
|
||||
<label for="sd_denoising_strength"><span data-i18n="Denoising strength">Denoising strength</span> (<span id="sd_denoising_strength_value"></span>)</label>
|
||||
<input id="sd_denoising_strength" type="range" min="{{denoising_strength_min}}" max="{{denoising_strength_max}}" step="{{denoising_strength_step}}" value="{{denoising_strength}}" />
|
||||
<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>
|
||||
<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 class="flex-container marginTopBot5" data-sd-source="novel">
|
||||
<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" />
|
||||
<small data-i18n="SMEA">SMEA</small>
|
||||
</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" />
|
||||
<small data-i18n="DYN">DYN</small>
|
||||
</label>
|
||||
<label class="flex1 checkbox_label" for="sd_novel_decrisper" title="Reduce artifacts caused by high guidance values.">
|
||||
<input id="sd_novel_decrisper" type="checkbox" />
|
||||
<small data-i18n="Decrisper">Decrisper</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras" class="marginTop5">
|
||||
<label for="sd_seed">Seed</label>
|
||||
<small>(-1 for random)</small>
|
||||
<label for="sd_seed">
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<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>
|
||||
|
@ -96,3 +96,19 @@
|
||||
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%);
|
||||
}
|
||||
|
@ -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>
|
@ -1,7 +1,7 @@
|
||||
<div class="translation_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b data-i18n="Chat Translation">Chat Translation</b>
|
||||
<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">
|
@ -10,13 +10,12 @@ import {
|
||||
substituteParams,
|
||||
updateMessageBlock,
|
||||
} 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 { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
import { renderTemplateAsync } from '../../templates.js';
|
||||
|
||||
export const autoModeOptions = {
|
||||
NONE: 'none',
|
||||
@ -403,6 +402,10 @@ async function translate(text, lang) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!lang) {
|
||||
lang = extension_settings.translate.target_language;
|
||||
}
|
||||
|
||||
switch (extension_settings.translate.provider) {
|
||||
case 'libre':
|
||||
return await translateProviderLibre(text, lang);
|
||||
@ -505,7 +508,8 @@ async function onTranslateChatClick() {
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
@ -564,9 +568,9 @@ const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
|
||||
window['translate'] = translate;
|
||||
|
||||
jQuery(async () => {
|
||||
const html = await renderTemplateAsync('translateIndex');
|
||||
const html = await renderExtensionTemplateAsync('translate', 'index');
|
||||
|
||||
const buttonHtml = await renderTemplateAsync('translateButtons');
|
||||
const buttonHtml = await renderExtensionTemplateAsync('translate', 'buttons');
|
||||
$('#extensionsMenu').append(buttonHtml);
|
||||
$('#extensions_settings2').append(html);
|
||||
$('#translate_chat').on('click', onTranslateChatClick);
|
||||
|
@ -175,7 +175,7 @@ class AzureTtsProvider {
|
||||
const url = URL.createObjectURL(audio);
|
||||
this.audioElement.src = url;
|
||||
this.audioElement.play();
|
||||
URL.revokeObjectURL(url);
|
||||
this.audioElement.onended = () => URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(text, voiceId) {
|
||||
|
@ -155,7 +155,7 @@ class EdgeTtsProvider {
|
||||
const url = URL.createObjectURL(audio);
|
||||
this.audioElement.src = url;
|
||||
this.audioElement.play();
|
||||
URL.revokeObjectURL(url);
|
||||
this.audioElement.onended = () => URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
|
||||
import { EdgeTtsProvider } from './edge.js';
|
||||
@ -18,6 +18,7 @@ import { AzureTtsProvider } from './azure.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
export { talkingAnimation };
|
||||
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
@ -28,6 +29,8 @@ let talkingHeadState = false;
|
||||
let lastChatId = null;
|
||||
let lastMessage = null;
|
||||
let lastMessageHash = null;
|
||||
let periodicMessageGenerationTimer = null;
|
||||
let lastPositionOfParagraphEnd = -1;
|
||||
|
||||
const DEFAULT_VOICE_MARKER = '[Default Voice]';
|
||||
const DISABLED_VOICE_MARKER = 'disabled';
|
||||
@ -109,7 +112,7 @@ async function onNarrateOneMessage() {
|
||||
|
||||
async function onNarrateText(args, text) {
|
||||
if (!text) {
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
audioElement.src = '/sounds/silence.mp3';
|
||||
@ -135,6 +138,7 @@ async function onNarrateText(args, text) {
|
||||
|
||||
// Return back to the chat voices
|
||||
await initVoiceMap(false);
|
||||
return '';
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
@ -531,6 +535,7 @@ function loadSettings() {
|
||||
$('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only);
|
||||
$('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
|
||||
$('#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_user').prop('checked', extension_settings.tts.narrate_user);
|
||||
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
|
||||
@ -594,6 +599,12 @@ function onAutoGenerationClick() {
|
||||
}
|
||||
|
||||
|
||||
function onPeriodicAutoGenerationClick() {
|
||||
extension_settings.tts.periodic_auto_generation = !!$('#tts_periodic_auto_generation').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
|
||||
function onNarrateDialoguesClick() {
|
||||
extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
@ -679,13 +690,14 @@ export function saveTtsProviderSettings() {
|
||||
//###################//
|
||||
|
||||
async function onChatChanged() {
|
||||
await resetTtsPlayback();
|
||||
await onGenerationEnded();
|
||||
resetTtsPlayback();
|
||||
const voiceMapInit = initVoiceMap();
|
||||
await Promise.race([voiceMapInit, delay(1000)]);
|
||||
await Promise.race([voiceMapInit, delay(debounce_timeout.relaxed)]);
|
||||
lastMessage = null;
|
||||
}
|
||||
|
||||
async function onMessageEvent(messageId) {
|
||||
async function onMessageEvent(messageId, lastCharIndex) {
|
||||
// If TTS is disabled, do nothing
|
||||
if (!extension_settings.tts.enabled) {
|
||||
return;
|
||||
@ -723,12 +735,17 @@ async function onMessageEvent(messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if we only want to process part of the message
|
||||
if (lastCharIndex) {
|
||||
message.mes = message.mes.substring(0, lastCharIndex);
|
||||
}
|
||||
|
||||
const isLastMessageInCurrent = () =>
|
||||
lastMessage &&
|
||||
typeof lastMessage === 'object' &&
|
||||
message.swipe_id === lastMessage.swipe_id &&
|
||||
message.name === lastMessage.name &&
|
||||
message.is_user === lastMessage.is_user &&
|
||||
message.name === lastMessage.name &&
|
||||
message.is_user === lastMessage.is_user &&
|
||||
message.mes.indexOf(lastMessage.mes) !== -1;
|
||||
|
||||
// if last message within current message, message got extended. only send diff to TTS.
|
||||
@ -781,6 +798,83 @@ async function onMessageDeleted() {
|
||||
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
|
||||
* @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
|
||||
@ -1010,6 +1104,10 @@ $(document).ready(function () {
|
||||
<input type="checkbox" id="tts_auto_generation">
|
||||
<small>Auto Generation</small>
|
||||
</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">
|
||||
<input type="checkbox" id="tts_narrate_quoted">
|
||||
<small>Only narrate "quotes"</small>
|
||||
@ -1072,6 +1170,7 @@ $(document).ready(function () {
|
||||
$('#tts_skip_tags').on('click', onSkipTagsClick);
|
||||
$('#tts_pass_asterisks').on('click', onPassAsterisksClick);
|
||||
$('#tts_auto_generation').on('click', onAutoGenerationClick);
|
||||
$('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
|
||||
$('#tts_narrate_user').on('click', onNarrateUserClick);
|
||||
|
||||
$('#playback_rate').on('input', function () {
|
||||
@ -1099,9 +1198,12 @@ $(document).ready(function () {
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
|
||||
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
|
||||
eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);
|
||||
eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded);
|
||||
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent);
|
||||
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'speak',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'speak',
|
||||
callback: onNarrateText,
|
||||
aliases: ['narrate', 'tts'],
|
||||
namedArgumentList: [
|
||||
|
@ -180,7 +180,7 @@ class NovelTtsProvider {
|
||||
const url = URL.createObjectURL(audio);
|
||||
this.audioElement.src = url;
|
||||
this.audioElement.play();
|
||||
URL.revokeObjectURL(url);
|
||||
this.audioElement.onended = () => URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async* fetchTtsGeneration(inputText, voiceId) {
|
||||
|
@ -60,7 +60,7 @@ class SpeechT5TtsProvider {
|
||||
const url = URL.createObjectURL(audio);
|
||||
this.audioElement.src = url;
|
||||
this.audioElement.play();
|
||||
URL.revokeObjectURL(url);
|
||||
this.audioElement.onended = () => URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
|
@ -73,7 +73,7 @@ import {
|
||||
depth_prompt_role_default,
|
||||
shouldAutoContinue,
|
||||
} 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 { isExternalMediaAllowed } from './chats.js';
|
||||
|
||||
@ -1356,7 +1356,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
}
|
||||
|
||||
// render tags
|
||||
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
|
||||
applyTagsOnGroupSelect(groupId);
|
||||
|
||||
// render characters list
|
||||
printGroupCandidates();
|
||||
|
247
public/scripts/keyboard.js
Normal file
247
public/scripts/keyboard.js
Normal 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);
|
||||
}
|
@ -70,6 +70,7 @@ import { saveLogprobsForActiveMessage } from './logprobs.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
|
||||
export {
|
||||
openai_messages_count,
|
||||
@ -3699,8 +3700,8 @@ function onSettingsPresetChange() {
|
||||
preset.assistant_impersonation = preset.assistant_prefill;
|
||||
}
|
||||
|
||||
const updateInput = (selector, value) => $(selector).val(value).trigger('input');
|
||||
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input');
|
||||
const updateInput = (selector, value) => $(selector).val(value).trigger('input', { source: 'preset' });
|
||||
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input', { source: 'preset' });
|
||||
|
||||
// Allow subscribers to alter the preset before applying deltas
|
||||
eventSource.emit(event_types.OAI_PRESET_CHANGED_BEFORE, {
|
||||
@ -4409,23 +4410,8 @@ function updateScaleForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function onCustomizeParametersClick() {
|
||||
const template = $(`
|
||||
<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) Example: - top_k: 20 - 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) Example: - frequency_penalty - 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) Example: - CustomHeader: custom-value - AnotherHeader: custom-value"></textarea>
|
||||
</div>
|
||||
</div>`);
|
||||
async function onCustomizeParametersClick() {
|
||||
const template = $(await renderTemplateAsync('customEndpointAdditionalParameters'));
|
||||
|
||||
template.find('#custom_include_body').val(oai_settings.custom_include_body).on('input', function () {
|
||||
oai_settings.custom_include_body = String($(this).val());
|
||||
@ -4871,9 +4857,11 @@ $(document).ready(async function () {
|
||||
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');
|
||||
$('#chat_completion_source').trigger('change');
|
||||
if (data?.source !== 'preset') {
|
||||
$('#chat_completion_source').trigger('change');
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
|
@ -1,156 +1,262 @@
|
||||
import { animation_duration, animation_easing } from '../script.js';
|
||||
import { delay } from './utils.js';
|
||||
import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
|
||||
|
||||
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Number}*/
|
||||
/** @readonly */
|
||||
/** @enum {Number} */
|
||||
export const POPUP_TYPE = {
|
||||
'TEXT': 1,
|
||||
'CONFIRM': 2,
|
||||
'INPUT': 3,
|
||||
};
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Boolean}*/
|
||||
/** @readonly */
|
||||
/** @enum {number?} */
|
||||
export const POPUP_RESULT = {
|
||||
'AFFIRMATIVE': true,
|
||||
'NEGATIVE': false,
|
||||
'CANCELLED': undefined,
|
||||
'AFFIRMATIVE': 1,
|
||||
'NEGATIVE': 0,
|
||||
'CANCELLED': null,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} PopupOptions
|
||||
* @property {string|boolean?} [okButton] - 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] - 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] - The number of rows for the input field
|
||||
* @property {boolean?} [wide] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio)
|
||||
* @property {boolean?} [wider] - Whether to display the popup in wider mode (just wider, no height scaling)
|
||||
* @property {boolean?} [large] - Whether to display the popup in large mode (90% of screen)
|
||||
* @property {boolean?} [allowHorizontalScrolling] - Whether to allow horizontal scrolling in the popup
|
||||
* @property {boolean?} [allowVerticalScrolling] - Whether to allow vertical scrolling in the popup
|
||||
* @property {POPUP_RESULT|number?} [defaultResult] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
|
||||
* @property {CustomPopupButton[]|string[]?} [customButtons] - 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
/**@type {POPUP_TYPE}*/ type;
|
||||
/** @type {POPUP_TYPE} */ type;
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ dlg;
|
||||
/**@type {HTMLElement}*/ text;
|
||||
/**@type {HTMLTextAreaElement}*/ input;
|
||||
/**@type {HTMLElement}*/ ok;
|
||||
/**@type {HTMLElement}*/ cancel;
|
||||
/** @type {string} */ id;
|
||||
|
||||
/**@type {POPUP_RESULT}*/ result;
|
||||
/**@type {any}*/ value;
|
||||
/** @type {HTMLDialogElement} */ dlg;
|
||||
/** @type {HTMLElement} */ body;
|
||||
/** @type {HTMLElement} */ content;
|
||||
/** @type {HTMLTextAreaElement} */ input;
|
||||
/** @type {HTMLElement} */ controls;
|
||||
/** @type {HTMLElement} */ ok;
|
||||
/** @type {HTMLElement} */ cancel;
|
||||
/** @type {POPUP_RESULT|number?} */ defaultResult;
|
||||
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
|
||||
|
||||
/**@type {Promise}*/ promise;
|
||||
/**@type {Function}*/ resolver;
|
||||
|
||||
/**@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.
|
||||
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup.
|
||||
* @param {POPUP_TYPE} type - One of Popup.TYPE
|
||||
* @param {string} inputValue - Value to set the input to.
|
||||
* @param {PopupOptions} options - Options for the popup.
|
||||
* Constructs a new Popup object with the given text content, type, inputValue, and options
|
||||
*
|
||||
* @param {JQuery<HTMLElement>|string|Element} content - Text content to display in the popup
|
||||
* @param {POPUP_TYPE} type - The type of 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, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null } = {}) {
|
||||
Popup.util.popups.push(this);
|
||||
|
||||
// Make this popup uniquely identifiable
|
||||
this.id = uuidv4();
|
||||
this.type = type;
|
||||
|
||||
/**@type {HTMLTemplateElement}*/
|
||||
const template = document.querySelector('#shadow_popup_template');
|
||||
const template = document.querySelector('#popup_template');
|
||||
// @ts-ignore
|
||||
this.dom = template.content.cloneNode(true).querySelector('.shadow_popup');
|
||||
const dlg = this.dom.querySelector('.dialogue_popup');
|
||||
// @ts-ignore
|
||||
this.dlg = dlg;
|
||||
this.text = this.dom.querySelector('.dialogue_popup_text');
|
||||
this.input = this.dom.querySelector('.dialogue_popup_input');
|
||||
this.ok = this.dom.querySelector('.dialogue_popup_ok');
|
||||
this.cancel = this.dom.querySelector('.dialogue_popup_cancel');
|
||||
this.dlg = template.content.cloneNode(true).querySelector('.popup');
|
||||
this.body = this.dlg.querySelector('.popup-body');
|
||||
this.content = this.dlg.querySelector('.popup-content');
|
||||
this.input = this.dlg.querySelector('.popup-input');
|
||||
this.controls = this.dlg.querySelector('.popup-controls');
|
||||
this.ok = this.dlg.querySelector('.popup-button-ok');
|
||||
this.cancel = this.dlg.querySelector('.popup-button-cancel');
|
||||
|
||||
if (wide) dlg.classList.add('wide_dialogue_popup');
|
||||
if (wider) dlg.classList.add('wider_dialogue_popup');
|
||||
if (large) dlg.classList.add('large_dialogue_popup');
|
||||
if (allowHorizontalScrolling) dlg.classList.add('horizontal_scrolling_dialogue_popup');
|
||||
if (allowVerticalScrolling) dlg.classList.add('vertical_scrolling_dialogue_popup');
|
||||
this.dlg.setAttribute('data-id', this.id);
|
||||
if (wide) this.dlg.classList.add('wide_dialogue_popup');
|
||||
if (wider) this.dlg.classList.add('wider_dialogue_popup');
|
||||
if (large) this.dlg.classList.add('large_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';
|
||||
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_cancel');
|
||||
// If custom button captions are provided, we set them beforehand
|
||||
this.ok.textContent = typeof okButton === 'string' ? okButton : 'OK';
|
||||
this.cancel.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.setAttribute('data-result', String(button.result ?? undefined));
|
||||
buttonElement.textContent = button.text;
|
||||
buttonElement.tabIndex = 0;
|
||||
|
||||
if (button.action) buttonElement.addEventListener('click', button.action);
|
||||
if (button.result) buttonElement.addEventListener('click', () => this.complete(button.result));
|
||||
|
||||
if (button.appendAtEnd) {
|
||||
this.controls.appendChild(buttonElement);
|
||||
} else {
|
||||
this.controls.insertBefore(buttonElement, this.ok);
|
||||
}
|
||||
});
|
||||
|
||||
// Set the default button class
|
||||
const defaultButton = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
|
||||
if (defaultButton) defaultButton.classList.add('menu_button_default');
|
||||
|
||||
switch (type) {
|
||||
case POPUP_TYPE.TEXT: {
|
||||
this.input.style.display = 'none';
|
||||
this.cancel.style.display = 'none';
|
||||
if (!cancelButton) this.cancel.style.display = 'none';
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.CONFIRM: {
|
||||
this.input.style.display = 'none';
|
||||
this.ok.textContent = okButton ?? template.getAttribute('popup_text_yes');
|
||||
this.cancel.textContent = cancelButton ?? template.getAttribute('popup_text_no');
|
||||
if (!okButton) this.ok.textContent = template.getAttribute('popup-button-yes');
|
||||
if (!cancelButton) this.cancel.textContent = template.getAttribute('popup-button-no');
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.input.style.display = 'block';
|
||||
this.ok.textContent = okButton ?? template.getAttribute('popup_text_save');
|
||||
if (!okButton) this.ok.textContent = template.getAttribute('popup-button-save');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// illegal argument
|
||||
console.warn('Unknown popup type.', type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.input.value = inputValue;
|
||||
this.input.rows = rows ?? 1;
|
||||
|
||||
this.text.innerHTML = '';
|
||||
if (text instanceof jQuery) {
|
||||
$(this.text).append(text);
|
||||
} else if (text instanceof HTMLElement) {
|
||||
this.text.append(text);
|
||||
} else if (typeof text == 'string') {
|
||||
this.text.innerHTML = text;
|
||||
this.content.innerHTML = '';
|
||||
if (content instanceof jQuery) {
|
||||
$(this.content).append(content);
|
||||
} else if (content instanceof HTMLElement) {
|
||||
this.content.append(content);
|
||||
} else if (typeof content == 'string') {
|
||||
this.content.innerHTML = content;
|
||||
} else {
|
||||
// illegal argument
|
||||
console.warn('Unknown popup text type. Should be jQuery, HTMLElement or string.', content);
|
||||
}
|
||||
|
||||
this.input.addEventListener('keydown', (evt) => {
|
||||
if (evt.key != 'Enter' || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.completeAffirmative();
|
||||
});
|
||||
// Already prepare the auto-focus control by adding the "autofocus" attribute, this should be respected by showModal()
|
||||
this.setAutoFocus({ applyAutoFocus: true });
|
||||
|
||||
this.ok.addEventListener('click', () => this.completeAffirmative());
|
||||
this.cancel.addEventListener('click', () => this.completeNegative());
|
||||
// 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; });
|
||||
|
||||
this.ok.addEventListener('click', () => this.complete(POPUP_RESULT.AFFIRMATIVE));
|
||||
this.cancel.addEventListener('click', () => this.complete(POPUP_RESULT.NEGATIVE));
|
||||
const keyListener = (evt) => {
|
||||
switch (evt.key) {
|
||||
case 'Escape': {
|
||||
// does it really matter where we check?
|
||||
const topModal = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2)?.closest('.shadow_popup');
|
||||
if (topModal == this.dom) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.completeCancelled();
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
break;
|
||||
}
|
||||
// Check if we are the currently active popup
|
||||
if (this.dlg != document.activeElement?.closest('.popup'))
|
||||
return;
|
||||
|
||||
this.complete(POPUP_RESULT.CANCELLED);
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
break;
|
||||
}
|
||||
case 'Enter': {
|
||||
// CTRL+Enter counts as a closing action, but all other modifiers (ALT, SHIFT) should not trigger this
|
||||
if (evt.altKey || evt.shiftKey)
|
||||
return;
|
||||
|
||||
// Check if we are the currently active popup
|
||||
if (this.dlg != document.activeElement?.closest('.popup'))
|
||||
return;
|
||||
|
||||
// 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);
|
||||
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() {
|
||||
document.body.append(this.dom);
|
||||
this.dom.style.display = 'block';
|
||||
switch (this.type) {
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.input.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
document.body.append(this.dlg);
|
||||
|
||||
$(this.dom).transition({
|
||||
opacity: 1,
|
||||
duration: animation_duration,
|
||||
easing: animation_easing,
|
||||
});
|
||||
// Run opening animation
|
||||
this.dlg.setAttribute('opening', '');
|
||||
|
||||
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.resolver = resolve;
|
||||
@ -158,80 +264,201 @@ export class Popup {
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
completeAffirmative() {
|
||||
switch (this.type) {
|
||||
case POPUP_TYPE.TEXT:
|
||||
case POPUP_TYPE.CONFIRM: {
|
||||
this.value = true;
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.value = this.input.value;
|
||||
break;
|
||||
setAutoFocus({ applyAutoFocus = false } = {}) {
|
||||
/** @type {HTMLElement} */
|
||||
let control;
|
||||
|
||||
// Try to find if we have an autofocus control already present
|
||||
control = this.dlg.querySelector('[autofocus]');
|
||||
|
||||
// If not, find the default control for this popup type
|
||||
if (!control) {
|
||||
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', '');
|
||||
} 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;
|
||||
Popup.util.lastResult = { value, result };
|
||||
this.hide();
|
||||
}
|
||||
|
||||
completeNegative() {
|
||||
switch (this.type) {
|
||||
case POPUP_TYPE.TEXT:
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Hides the popup, using the internal resolver to return the value to the original show promise
|
||||
* @private
|
||||
*/
|
||||
hide() {
|
||||
$(this.dom).transition({
|
||||
opacity: 0,
|
||||
duration: animation_duration,
|
||||
easing: animation_easing,
|
||||
});
|
||||
delay(animation_duration).then(() => {
|
||||
this.dom.remove();
|
||||
// We close the dialog, first running the animation
|
||||
this.dlg.setAttribute('closing', '');
|
||||
|
||||
// Once the hiding starts, we need to fix the toastr to the layer below
|
||||
fixToastrForDialogs();
|
||||
|
||||
// After the dialog is actually completely closed, remove it from the DOM
|
||||
runAfterAnimation(this.dlg, () => {
|
||||
// Call the close on the dialog
|
||||
this.dlg.close();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup.
|
||||
* Displays a blocking popup with a given content and type
|
||||
*
|
||||
* @param {JQuery<HTMLElement>|string|Element} content - Content or text to display in the popup
|
||||
* @param {POPUP_TYPE} type
|
||||
* @param {string} inputValue - Value to set the input to.
|
||||
* @param {PopupOptions} options - Options for the popup.
|
||||
* @returns
|
||||
* @param {string} inputValue - Value to set the input to
|
||||
* @param {PopupOptions} [popupOptions={}] - Options for the popup
|
||||
* @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(
|
||||
text,
|
||||
content,
|
||||
type,
|
||||
inputValue,
|
||||
{ okButton, cancelButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling },
|
||||
popupOptions,
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -2262,22 +2262,23 @@ async function importTheme(file) {
|
||||
}
|
||||
|
||||
themes.push(parsed);
|
||||
await applyTheme(parsed.name);
|
||||
await saveTheme(parsed.name);
|
||||
await saveTheme(parsed.name, getNewTheme(parsed));
|
||||
const option = document.createElement('option');
|
||||
option.selected = true;
|
||||
option.selected = false;
|
||||
option.value = parsed.name;
|
||||
option.innerText = parsed.name;
|
||||
$('#themes').append(option);
|
||||
saveSettingsDebounced();
|
||||
toastr.success(parsed.name, 'Theme imported');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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.
|
||||
*/
|
||||
async function saveTheme(name = undefined) {
|
||||
async function saveTheme(name = undefined, theme = undefined) {
|
||||
if (typeof name !== 'string') {
|
||||
name = await callPopup('Enter a theme preset name:', 'input', power_user.theme);
|
||||
|
||||
@ -2286,7 +2287,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,
|
||||
blur_strength: power_user.blur_strength,
|
||||
main_text_color: power_user.main_text_color,
|
||||
@ -2324,33 +2364,20 @@ async function saveTheme(name = undefined) {
|
||||
reduced_motion: power_user.reduced_motion,
|
||||
compact_input_area: power_user.compact_input_area,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
/**
|
||||
* Applies imported theme properties to the theme object.
|
||||
* @param {object} parsed Parsed object to get the theme from.
|
||||
* @returns {object} Theme assigned to the parsed object.
|
||||
*/
|
||||
function getNewTheme(parsed) {
|
||||
const theme = getThemeObject(parsed.name);
|
||||
for (const key in parsed) {
|
||||
if (Object.hasOwn(theme, key)) {
|
||||
theme[key] = parsed[key];
|
||||
}
|
||||
else {
|
||||
themes[themeIndex] = theme;
|
||||
$(`#themes option[value="${name}"]`).attr('selected', true);
|
||||
}
|
||||
|
||||
power_user.theme = name;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
|
@ -4,22 +4,26 @@ import {
|
||||
this_chid,
|
||||
callPopup,
|
||||
menu_type,
|
||||
getCharacters,
|
||||
entitiesFilter,
|
||||
printCharactersDebounced,
|
||||
buildAvatarList,
|
||||
eventSource,
|
||||
event_types,
|
||||
DEFAULT_PRINT_TIMEOUT,
|
||||
} from '../script.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
|
||||
|
||||
import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } from './utils.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
|
||||
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
|
||||
import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js';
|
||||
|
||||
@ -46,6 +50,8 @@ export {
|
||||
removeTagFromMap,
|
||||
};
|
||||
|
||||
/** @typedef {import('../script.js').Character} Character */
|
||||
|
||||
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
|
||||
const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter';
|
||||
const TAG_TEMPLATE = $('#tag_template .tag');
|
||||
@ -329,7 +335,7 @@ function filterByFolder(filterHelper) {
|
||||
if (!power_user.bogus_folders) {
|
||||
$('#bogus_folders').prop('checked', true).trigger('input');
|
||||
onViewTagsListClick();
|
||||
flashHighlight($('#dialogue_popup .tag_as_folder, #dialogue_popup .tag_folder_indicator'));
|
||||
flashHighlight($('#tag_view_list .tag_as_folder, #tag_view_list .tag_folder_indicator'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -468,29 +474,34 @@ export function getTagKeyForEntityElement(element) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag to a given entity
|
||||
* @param {Tag} tag - The tag to add
|
||||
* @param {string|string[]} entityId - The entity to add this tag to. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* Adds one or more tags to a given entity
|
||||
*
|
||||
* @param {Tag|Tag[]} tag - The tag or tags to add
|
||||
* @param {string|string[]} entityId - The entity or entities to add this tag to. Has to be the entity key (e.g. `addTagToEntity`).
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function)
|
||||
* @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
|
||||
* @returns {boolean} Whether at least one tag was added
|
||||
*/
|
||||
export function addTagToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
|
||||
export function addTagsToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
|
||||
const tags = Array.isArray(tag) ? tag : [tag];
|
||||
const entityIds = Array.isArray(entityId) ? entityId : [entityId];
|
||||
|
||||
let result = false;
|
||||
|
||||
// Add tags to the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = addTagToMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = addTagToMap(tag.id, entityId);
|
||||
}
|
||||
entityIds.forEach((id) => {
|
||||
tags.forEach((tag) => {
|
||||
result = addTagToMap(tag.id, id) || result;
|
||||
});
|
||||
});
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
tagListOptions.addTag = tags;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
if (tagListSelector) printTagList(tagListSelector, tagListOptions);
|
||||
@ -588,10 +599,10 @@ function removeTagFromMap(tagId, characterId = null) {
|
||||
|
||||
function findTag(request, resolve, listSelector) {
|
||||
const skipIds = [...($(listSelector).find('.tag').map((_, el) => $(el).attr('id')))];
|
||||
const haystack = tags.filter(t => !skipIds.includes(t.id)).map(t => t.name).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||||
const needle = request.term.toLowerCase();
|
||||
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
|
||||
const result = haystack.filter(x => x.toLowerCase().includes(needle));
|
||||
const haystack = tags.filter(t => !skipIds.includes(t.id)).sort(compareTagsForSort).map(t => t.name);
|
||||
const needle = request.term;
|
||||
const hasExactMatch = haystack.findIndex(x => equalsIgnoreCaseAndAccents(x, needle)) !== -1;
|
||||
const result = haystack.filter(x => includesIgnoreCaseAndAccents(x, needle));
|
||||
|
||||
if (request.term && !hasExactMatch) {
|
||||
result.unshift(request.term);
|
||||
@ -612,7 +623,7 @@ function findTag(request, resolve, listSelector) {
|
||||
*/
|
||||
function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
|
||||
let tagName = ui.item.value;
|
||||
let tag = tags.find(t => t.name === tagName);
|
||||
let tag = getTag(tagName);
|
||||
|
||||
// create new tag if it doesn't exist
|
||||
if (!tag) {
|
||||
@ -626,7 +637,7 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
|
||||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
|
||||
addTagsToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
|
||||
|
||||
// need to return false to keep the input clear
|
||||
return false;
|
||||
@ -635,70 +646,200 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
|
||||
/**
|
||||
* Get a list of existing tags matching a list of provided new tag names
|
||||
*
|
||||
* @param {string[]} new_tags - A list of strings representing tag names
|
||||
* @returns List of existing tags
|
||||
* @param {string[]} newTags - A list of strings representing tag names
|
||||
* @returns {Tag[]} List of existing tags
|
||||
*/
|
||||
function getExistingTags(new_tags) {
|
||||
let existing_tags = [];
|
||||
for (let tag of new_tags) {
|
||||
let foundTag = tags.find(t => t.name.toLowerCase() === tag.toLowerCase());
|
||||
function getExistingTags(newTags) {
|
||||
let existingTags = [];
|
||||
for (let tagName of newTags) {
|
||||
let foundTag = getTag(tagName);
|
||||
if (foundTag) {
|
||||
existing_tags.push(foundTag.name);
|
||||
existingTags.push(foundTag);
|
||||
}
|
||||
}
|
||||
return existing_tags;
|
||||
return existingTags;
|
||||
}
|
||||
|
||||
async function importTags(imported_char) {
|
||||
let imported_tags = imported_char.tags.filter(t => t !== 'ROOT' && t !== 'TAVERN');
|
||||
let existingTags = await getExistingTags(imported_tags);
|
||||
//make this case insensitive
|
||||
let newTags = imported_tags.filter(t => !existingTags.some(existingTag => existingTag.toLowerCase() === t.toLowerCase()));
|
||||
let selected_tags = '';
|
||||
const existingTagsString = existingTags.length ? (': ' + existingTags.join(', ')) : '';
|
||||
if (newTags.length === 0) {
|
||||
await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p>`, 'text');
|
||||
} else {
|
||||
selected_tags = await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p><p>The following ${newTags.length} new tags will be imported.</p>`, 'input', newTags.join(', '));
|
||||
}
|
||||
// @ts-ignore
|
||||
selected_tags = existingTags.concat(selected_tags.split(','));
|
||||
// @ts-ignore
|
||||
selected_tags = selected_tags.map(t => t.trim()).filter(t => t !== '');
|
||||
//Anti-troll measure
|
||||
if (selected_tags.length > 15) {
|
||||
selected_tags = selected_tags.slice(0, 15);
|
||||
}
|
||||
for (let tagName of selected_tags) {
|
||||
let tag = tags.find(t => t.name === tagName);
|
||||
const tagImportSettings = {
|
||||
ALWAYS_IMPORT_ALL: 1,
|
||||
ONLY_IMPORT_EXISTING: 2,
|
||||
IMPORT_NONE: 3,
|
||||
ASK: 4,
|
||||
};
|
||||
|
||||
if (!tag) {
|
||||
tag = createNewTag(tagName);
|
||||
}
|
||||
let globalTagImportSetting = tagImportSettings.ASK; // Default setting
|
||||
|
||||
if (!tag_map[imported_char.avatar].includes(tag.id)) {
|
||||
tag_map[imported_char.avatar].push(tag.id);
|
||||
console.debug('added tag to map', tag, imported_char.name);
|
||||
}
|
||||
const IMPORT_EXLCUDED_TAGS = ['ROOT', 'TAVERN'];
|
||||
const ANTI_TROLL_MAX_TAGS = 15;
|
||||
|
||||
/**
|
||||
* Imports tags for a given character
|
||||
*
|
||||
* @param {Character} character - The character
|
||||
* @returns {Promise<boolean>} Boolean indicating whether any tag was imported
|
||||
*/
|
||||
async function importTags(character) {
|
||||
// Gather the tags to import based on the selected setting
|
||||
const tagNamesToImport = await handleTagImport(character);
|
||||
if (!tagNamesToImport?.length) {
|
||||
toastr.info('No tags imported', 'Importing Tags');
|
||||
return;
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
const tagsToImport = tagNamesToImport.map(tag => getTag(tag, { createNew: true }));
|
||||
const added = addTagsToEntity(tagsToImport, character.avatar);
|
||||
|
||||
// Await the character list, which will automatically reprint it and all tag filters
|
||||
await getCharacters();
|
||||
toastr.success(`Imported tags:<br />${tagsToImport.map(x => x.name).join(', ')}`, 'Importing Tags', { escapeHtml: false });
|
||||
|
||||
// need to return false to keep the input clear
|
||||
return false;
|
||||
return added;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the import of tags for a given character and returns the resulting list of tags to add
|
||||
*
|
||||
* @param {Character} character - The character
|
||||
* @returns {Promise<string[]>} Array of strings representing the tags to import
|
||||
*/
|
||||
async function handleTagImport(character) {
|
||||
/** @type {string[]} */
|
||||
const importTags = character.tags.map(t => t.trim()).filter(t => t)
|
||||
.filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))
|
||||
.slice(0, ANTI_TROLL_MAX_TAGS);
|
||||
const existingTags = getExistingTags(importTags);
|
||||
const newTags = importTags.filter(t => !existingTags.some(existingTag => existingTag.name.toLowerCase() === t.toLowerCase()))
|
||||
.map(newTag);
|
||||
|
||||
switch (globalTagImportSetting) {
|
||||
case tagImportSettings.ALWAYS_IMPORT_ALL:
|
||||
return existingTags.concat(newTags).map(t => t.name);
|
||||
case tagImportSettings.ONLY_IMPORT_EXISTING:
|
||||
return existingTags.map(t => t.name);
|
||||
case tagImportSettings.ASK:
|
||||
return await showTagImportPopup(character, existingTags, newTags);
|
||||
case tagImportSettings.IMPORT_NONE:
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a popup to import tags for a given character and returns the resulting list of tags to add
|
||||
*
|
||||
* @param {Character} character - The character
|
||||
* @param {Tag[]} existingTags - List of existing tags
|
||||
* @param {Tag[]} newTags - List of new tags
|
||||
* @returns {Promise<string[]>} Array of strings representing the tags to import
|
||||
*/
|
||||
async function showTagImportPopup(character, existingTags, newTags) {
|
||||
/** @type {{[key: string]: import('./popup.js').CustomPopupButton}} */
|
||||
const importButtons = {
|
||||
EXISTING: { result: 2, text: 'Import Existing' },
|
||||
ALL: { result: 3, text: 'Import All' },
|
||||
NONE: { result: 4, text: 'Import None' },
|
||||
};
|
||||
|
||||
const customButtonsCaptions = Object.values(importButtons).map(button => `"${button.text}"`);
|
||||
const customButtonsString = customButtonsCaptions.slice(0, -1).join(', ') + ' or ' + customButtonsCaptions.slice(-1);
|
||||
|
||||
const popupContent = $(`
|
||||
<h3>Import Tags For ${character.name}</h3>
|
||||
<div class="import_avatar_placeholder"></div>
|
||||
<div class="import_tags_content justifyLeft">
|
||||
<small>
|
||||
Click remove on any tag to remove it from this import.<br />
|
||||
Select one of the import options to finish importing the tags.
|
||||
</small>
|
||||
|
||||
<h4 class="m-t-1">Existing Tags</h4>
|
||||
<div id="import_existing_tags_list" class="tags"></div>
|
||||
|
||||
<h4 class="m-t-1">New Tags</h4>
|
||||
<div id="import_new_tags_list" class="tags"></div>
|
||||
|
||||
<small>
|
||||
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-3" for="import_remember_option">
|
||||
<input type="checkbox" id="import_remember_option" name="import_remember_option" />
|
||||
<span data-i18n="Remember my choice">
|
||||
Remember my choice
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Remember the chosen import option\nIf ${customButtonsString} is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify "Tag Import Option".\n\nIf the "Import" option is chosen, the global setting will stay on "Ask"."
|
||||
title="Remember the chosen import option\nIf ${customButtonsString} is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify "Tag Import Option".\n\nIf the "Import" option is chosen, the global setting will stay on "Ask".">
|
||||
</div>
|
||||
</span>
|
||||
</label>
|
||||
</small>
|
||||
</div>`);
|
||||
|
||||
// Print tags after popup is shown, so that events can be added
|
||||
printTagList(popupContent.find('#import_existing_tags_list'), { tags: existingTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(existingTags, tag) } });
|
||||
printTagList(popupContent.find('#import_new_tags_list'), { tags: newTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(newTags, tag) } });
|
||||
|
||||
const result = await callGenericPopup(popupContent, POPUP_TYPE.TEXT, null, { wider: true, okButton: 'Import', cancelButton: true, customButtons: Object.values(importButtons) });
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case 1:
|
||||
case true:
|
||||
case importButtons.ALL.result: // Default 'Import' option where it imports all selected
|
||||
return existingTags.concat(newTags).map(t => t.name);
|
||||
case importButtons.EXISTING.result:
|
||||
return existingTags.map(t => t.name);
|
||||
case importButtons.NONE.result:
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a tag from the tags array based on the provided tag name (insensitive soft matching)
|
||||
* Optionally creates the tag if it doesn't exist
|
||||
*
|
||||
* @param {string} tagName - The name of the tag to search for
|
||||
* @param {object} [options={}] - Optional parameters
|
||||
* @param {boolean} [options.createNew=false] - Whether to create the tag if it doesn't exist
|
||||
* @returns {Tag?} The tag object that matches the provided tag name, or undefined if no match is found
|
||||
*/
|
||||
function getTag(tagName, { createNew = false } = {}) {
|
||||
let tag = tags.find(t => equalsIgnoreCaseAndAccents(t.name, tagName));
|
||||
if (!tag && createNew) {
|
||||
tag = createNewTag(tagName);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tag with default properties and a randomly generated id
|
||||
*
|
||||
* Does **not** trigger a save, so it's up to the caller to do that
|
||||
*
|
||||
* @param {string} tagName - name of the tag
|
||||
* @returns {Tag}
|
||||
* @returns {Tag} the newly created tag, or the existing tag if it already exists (with a logged warning)
|
||||
*/
|
||||
function createNewTag(tagName) {
|
||||
const tag = {
|
||||
const existing = getTag(tagName);
|
||||
if (existing) {
|
||||
toastr.warning(`Cannot create new tag. A tag with the name already exists:<br />${existing.name}`, 'Creating Tag', { escapeHtml: false });
|
||||
return existing;
|
||||
}
|
||||
|
||||
const tag = newTag(tagName);
|
||||
tags.push(tag);
|
||||
console.debug('Created new tag', tag.name, 'with id', tag.id);
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tag object with the given tag name and default properties
|
||||
*
|
||||
* Not to be confused with `createNewTag`, which actually creates the tag and adds it to the existing list of tags.
|
||||
* Use this one to create temporary tag objects, for example for drawing.
|
||||
*
|
||||
* @param {string} tagName - The name of the tag
|
||||
* @return {Tag} The newly created tag object
|
||||
*/
|
||||
function newTag(tagName) {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
name: tagName,
|
||||
folder_type: TAG_FOLDER_DEFAULT_TYPE,
|
||||
@ -708,16 +849,14 @@ function createNewTag(tagName) {
|
||||
color2: '',
|
||||
create_date: Date.now(),
|
||||
};
|
||||
tags.push(tag);
|
||||
console.debug('Created new tag', tag.name, 'with id', tag.id);
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} TagOptions - Options for tag behavior. (Same object will be passed into "appendTagToList")
|
||||
* @property {boolean} [removable=false] - Whether tags can be removed.
|
||||
* @property {boolean} [selectable=false] - Whether tags can be selected.
|
||||
* @property {boolean} [isFilter=false] - Whether tags can be selected as a filter.
|
||||
* @property {function} [action=undefined] - Action to perform on tag interaction.
|
||||
* @property {(tag: Tag)=>boolean} [removeAction=undefined] - Action to perform on tag removal instead of the default remove action. If the action returns false, the tag will not be removed.
|
||||
* @property {boolean} [isGeneralList=false] - If true, indicates that this is the general list of tags.
|
||||
* @property {boolean} [skipExistsCheck=false] - If true, the tag gets added even if a tag with the same id already exists.
|
||||
*/
|
||||
@ -725,7 +864,7 @@ function createNewTag(tagName) {
|
||||
/**
|
||||
* @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list.
|
||||
* @property {Tag[]|function(): Tag[]} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags.
|
||||
* @property {Tag} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check.
|
||||
* @property {Tag|Tag[]} [addTag=undefined] - Optionally provide one or multiple tags that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check.
|
||||
* @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key.
|
||||
* @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean.
|
||||
* @property {boolean} [sort=true] - Whether the tags should be sorted via the sort function, or kept as is.
|
||||
@ -749,8 +888,9 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
|
||||
$element.empty();
|
||||
}
|
||||
|
||||
if (addTag && (tagOptions.skipExistsCheck || !printableTags.some(x => x.id === addTag.id))) {
|
||||
printableTags = [...printableTags, addTag];
|
||||
if (addTag) {
|
||||
const addTags = Array.isArray(addTag) ? addTag : [addTag];
|
||||
printableTags = printableTags.concat(addTags.filter(tag => tagOptions.skipExistsCheck || !printableTags.some(t => t.id === tag.id)));
|
||||
}
|
||||
|
||||
// one last sort, because we might have modified the tag list or manually retrieved it from a function
|
||||
@ -834,7 +974,7 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
|
||||
* @param {TagOptions} [options={}] - Options for tag behavior
|
||||
* @returns {void}
|
||||
*/
|
||||
function appendTagToList(listElement, tag, { removable = false, selectable = false, action = undefined, isGeneralList = false, skipExistsCheck = false } = {}) {
|
||||
function appendTagToList(listElement, tag, { removable = false, isFilter = false, action = undefined, removeAction = undefined, isGeneralList = false, skipExistsCheck = false } = {}) {
|
||||
if (!listElement) {
|
||||
return;
|
||||
}
|
||||
@ -852,6 +992,13 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
|
||||
tagElement.find('.tag_name').text(tag.name);
|
||||
const removeButton = tagElement.find('.tag_remove');
|
||||
removable ? removeButton.show() : removeButton.hide();
|
||||
if (removable && removeAction) {
|
||||
tagElement.attr('custom-remove-action', String(true));
|
||||
removeButton.on('click', () => {
|
||||
const result = removeAction(tag);
|
||||
if (result !== false) tagElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
if (tag.class) {
|
||||
tagElement.addClass(tag.class);
|
||||
@ -867,19 +1014,20 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
|
||||
// We could have multiple ways of actions passed in. The manual arguments have precendence in front of a specified tag action
|
||||
const clickableAction = action ?? tag.action;
|
||||
|
||||
// If this is a tag for a general list and its either selectable or actionable, lets mark its current state
|
||||
if ((selectable || clickableAction) && isGeneralList) {
|
||||
// If this is a tag for a general list and its either a filter or actionable, lets mark its current state
|
||||
if ((isFilter || clickableAction) && isGeneralList) {
|
||||
toggleTagThreeState(tagElement, { stateOverride: tag.filter_state ?? DEFAULT_FILTER_STATE });
|
||||
}
|
||||
|
||||
if (selectable) {
|
||||
if (isFilter) {
|
||||
tagElement.on('click', () => onTagFilterClick.bind(tagElement)(listElement));
|
||||
tagElement.addClass(INTERACTABLE_CONTROL_CLASS);
|
||||
}
|
||||
|
||||
if (clickableAction) {
|
||||
const filter = getFilterHelper($(listElement));
|
||||
tagElement.on('click', (e) => clickableAction.bind(tagElement)(filter, e));
|
||||
tagElement.addClass('clickable-action');
|
||||
tagElement.addClass('clickable-action').addClass(INTERACTABLE_CONTROL_CLASS);
|
||||
}
|
||||
|
||||
$(listElement).append(tagElement);
|
||||
@ -888,6 +1036,7 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
|
||||
function onTagFilterClick(listElement) {
|
||||
const tagId = $(this).attr('id');
|
||||
const existingTag = tags.find((tag) => tag.id === tagId);
|
||||
const parent = $(this).parents('.tags');
|
||||
|
||||
let state = toggleTagThreeState($(this));
|
||||
|
||||
@ -898,6 +1047,9 @@ function onTagFilterClick(listElement) {
|
||||
|
||||
// We don't print anything manually, updating the filter will automatically trigger a redraw of all relevant stuff
|
||||
runTagFilters(listElement);
|
||||
|
||||
// Focus the tag again we were at, if possible. To improve keyboard navigation
|
||||
setTimeout(() => parent.find(`.tag[id="${tagId}"]`).trigger('focus'), DEFAULT_PRINT_TIMEOUT + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -975,7 +1127,7 @@ function printTagFilters(type = tag_filter_types.character) {
|
||||
|
||||
const characterTagIds = Object.values(tag_map).flat();
|
||||
const tagsToDisplay = tags.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort);
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } });
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { isFilter: true, isGeneralList: true } });
|
||||
|
||||
// Print bogus folder navigation
|
||||
const bogusDrilldown = $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown');
|
||||
@ -1010,6 +1162,12 @@ function onTagRemoveClick(event) {
|
||||
const tagElement = $(this).closest('.tag');
|
||||
const tagId = tagElement.attr('id');
|
||||
|
||||
// If we have a custom remove action, we are not executing anything here in the default handler
|
||||
if (tagElement.attr('custom-remove-action')) {
|
||||
console.debug('Custom remove action', tagId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we are inside the drilldown. If so, we call remove on the bogus folder
|
||||
if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) {
|
||||
console.debug('Bogus drilldown remove', tagId);
|
||||
@ -1029,7 +1187,7 @@ function onTagRemoveClick(event) {
|
||||
// @ts-ignore
|
||||
function onTagInput(event) {
|
||||
let val = $(this).val();
|
||||
if (tags.find(t => t.name === val)) return;
|
||||
if (getTag(String(val))) return;
|
||||
// @ts-ignore
|
||||
$(this).autocomplete('search', val);
|
||||
}
|
||||
@ -1047,7 +1205,7 @@ function onGroupCreateClick() {
|
||||
// Nothing to do here at the moment. Tags in group interface get automatically redrawn.
|
||||
}
|
||||
|
||||
export function applyTagsOnCharacterSelect() {
|
||||
export function applyTagsOnCharacterSelect(chid = null) {
|
||||
// If we are in create window, we cannot simply redraw, as there are no real persisted tags. Grab them, and pass them in
|
||||
if (menu_type === 'create') {
|
||||
const currentTagIds = $('#tagList').find('.tag').map((_, el) => $(el).attr('id')).get();
|
||||
@ -1056,11 +1214,11 @@ export function applyTagsOnCharacterSelect() {
|
||||
return;
|
||||
}
|
||||
|
||||
const chid = this_chid ? Number(this_chid) : null;
|
||||
chid = chid ?? Number(this_chid);
|
||||
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
export function applyTagsOnGroupSelect() {
|
||||
export function applyTagsOnGroupSelect(groupId = null) {
|
||||
// If we are in create window, we explicitly have to tell the system to print for the new group, not the one selected in the background
|
||||
if (menu_type === 'group_create') {
|
||||
const currentTagIds = $('#groupTagList').find('.tag').map((_, el) => $(el).attr('id')).get();
|
||||
@ -1069,7 +1227,7 @@ export function applyTagsOnGroupSelect() {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = selected_group;
|
||||
groupId = groupId ?? Number(selected_group);
|
||||
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
@ -1091,9 +1249,7 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {})
|
||||
.focus(onTagInputFocus); // <== show tag list on click
|
||||
}
|
||||
|
||||
function onViewTagsListClick() {
|
||||
const popup = $('#dialogue_popup');
|
||||
popup.addClass('large_dialogue_popup');
|
||||
async function onViewTagsListClick() {
|
||||
const html = $(document.createElement('div'));
|
||||
html.attr('id', 'tag_view_list');
|
||||
html.append(`
|
||||
@ -1134,13 +1290,10 @@ function onViewTagsListClick() {
|
||||
const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
|
||||
html.append(tagContainer);
|
||||
|
||||
callPopup(html, 'text', null, { allowVerticalScrolling: true });
|
||||
|
||||
printViewTagList();
|
||||
printViewTagList(tagContainer);
|
||||
makeTagListDraggable(tagContainer);
|
||||
|
||||
$('#dialogue_popup .tag-color').on('change', (evt) => onTagColorize(evt));
|
||||
$('#dialogue_popup .tag-color2').on('change', (evt) => onTagColorize2(evt));
|
||||
await callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1202,7 +1355,7 @@ function makeTagListDraggable(tagContainer) {
|
||||
// If tags were dragged manually, we have to disable auto sorting
|
||||
if (power_user.auto_sort_tags) {
|
||||
power_user.auto_sort_tags = false;
|
||||
$('#dialogue_popup input[name="auto_sort_tags"]').prop('checked', false);
|
||||
$('#tag_view_list input[name="auto_sort_tags"]').prop('checked', false);
|
||||
toastr.info('Automatic sorting of tags deactivated.');
|
||||
}
|
||||
|
||||
@ -1329,7 +1482,7 @@ async function onTagRestoreFileSelect(e) {
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
onViewTagsListClick();
|
||||
await onViewTagsListClick();
|
||||
}
|
||||
|
||||
function onBackupRestoreClick() {
|
||||
@ -1351,14 +1504,18 @@ function onTagsBackupClick() {
|
||||
}
|
||||
|
||||
function onTagCreateClick() {
|
||||
const tag = createNewTag('New Tag');
|
||||
printViewTagList();
|
||||
const tagName = getFreeName('New Tag', tags.map(x => x.name));
|
||||
const tag = createNewTag(tagName);
|
||||
printViewTagList($('#tag_view_list .tag_view_list_tags'));
|
||||
|
||||
const tagElement = ($('#dialogue_popup .tag_view_list_tags')).find(`.tag_view_item[id="${tag.id}"]`);
|
||||
const tagElement = ($('#tag_view_list .tag_view_list_tags')).find(`.tag_view_item[id="${tag.id}"]`);
|
||||
tagElement[0]?.scrollIntoView();
|
||||
flashHighlight(tagElement);
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
toastr.success('Tag created', 'Create Tag');
|
||||
}
|
||||
|
||||
function appendViewTagToList(list, tag, everything) {
|
||||
@ -1382,25 +1539,47 @@ function appendViewTagToList(list, tag, everything) {
|
||||
|
||||
const primaryColorPicker = $('<toolcool-color-picker></toolcool-color-picker>')
|
||||
.addClass('tag-color')
|
||||
.attr({ id: colorPickerId, color: tag.color });
|
||||
.attr({ id: colorPickerId, color: tag.color || 'rgba(0, 0, 0, 0.3)', 'data-default-color': 'rgba(0, 0, 0, 0.3)' });
|
||||
|
||||
const secondaryColorPicker = $('<toolcool-color-picker></toolcool-color-picker>')
|
||||
.addClass('tag-color2')
|
||||
.attr({ id: colorPicker2Id, color: tag.color2 });
|
||||
.attr({ id: colorPicker2Id, color: tag.color2 || power_user.main_text_color, 'data-default-color': power_user.main_text_color });
|
||||
|
||||
template.find('.tagColorPickerHolder').append(primaryColorPicker);
|
||||
template.find('.tagColorPicker2Holder').append(secondaryColorPicker);
|
||||
template.find('.tag_view_color_picker[data-value="color"]').append(primaryColorPicker)
|
||||
.append($('<div class="fas fa-link fa-xs link_icon right_menu_button" title="Link to theme color"></div>'));
|
||||
template.find('.tag_view_color_picker[data-value="color2"]').append(secondaryColorPicker)
|
||||
.append($('<div class="fas fa-link fa-xs link_icon right_menu_button" title="Link to theme color"></div>'));
|
||||
|
||||
template.find('.tag_as_folder').attr('id', tagAsFolderId);
|
||||
|
||||
primaryColorPicker.on('change', (evt) => onTagColorize(evt, (tag, color) => tag.color = color, 'background-color'));
|
||||
secondaryColorPicker.on('change', (evt) => onTagColorize(evt, (tag, color) => tag.color2 = color, 'color'));
|
||||
template.find('.tag_view_color_picker .link_icon').on('click', (evt) => {
|
||||
const colorPicker = $(evt.target).closest('.tag_view_color_picker').find('toolcool-color-picker');
|
||||
const defaultColor = colorPicker.attr('data-default-color');
|
||||
// @ts-ignore
|
||||
colorPicker[0].color = defaultColor;
|
||||
});
|
||||
|
||||
list.append(template);
|
||||
|
||||
updateDrawTagFolder(template, tag);
|
||||
// We prevent the popup from auto-close on Escape press on the color pickups. If the user really wants to, he can hit it again
|
||||
// Not the "cleanest" way, that would be actually using and observer, remembering whether the popup was open just before, but eh
|
||||
// Not gonna invest too much time into this small control here
|
||||
let lastHit = 0;
|
||||
template.on('keydown', (evt) => {
|
||||
if (evt.key === 'Escape') {
|
||||
if (evt.target === primaryColorPicker[0] || evt.target === secondaryColorPicker[0]) {
|
||||
if (Date.now() - lastHit < 5000) // If user hits it twice in five seconds
|
||||
return;
|
||||
lastHit = Date.now();
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
$(colorPickerId).color = tag.color;
|
||||
// @ts-ignore
|
||||
$(colorPicker2Id).color = tag.color2;
|
||||
updateDrawTagFolder(template, tag);
|
||||
}
|
||||
|
||||
function onTagAsFolderClick() {
|
||||
@ -1438,20 +1617,56 @@ function updateDrawTagFolder(element, tag) {
|
||||
indicator.css('font-size', `calc(var(--mainFontSize) * ${tagFolder.size})`);
|
||||
}
|
||||
|
||||
function onTagDeleteClick() {
|
||||
if (!confirm('Are you sure?')) {
|
||||
async function onTagDeleteClick() {
|
||||
const id = $(this).closest('.tag_view_item').attr('id');
|
||||
const tag = tags.find(x => x.id === id);
|
||||
const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name })));
|
||||
|
||||
const popupContent = $(`
|
||||
<h3>Delete Tag</h3>
|
||||
<div>Do you want to delete the tag <div id="tag_to_delete" class="tags_inline inline-flex margin-r2"></div>?</div>
|
||||
<div class="m-t-2 marginBot5">If you want to merge all references to this tag into another tag, select it below:</div>
|
||||
<select id="merge_tag_select">
|
||||
<option value="">--- None ---</option>
|
||||
${otherTags.map(x => `<option value="${x.id}">${x.name}</option>`).join('')}
|
||||
</select>`);
|
||||
|
||||
appendTagToList(popupContent.find('#tag_to_delete'), tag);
|
||||
|
||||
// Make the select control more fancy on not mobile
|
||||
if (!isMobile()) {
|
||||
// Delete the empty option in the dropdown, and make the select2 be empty by default
|
||||
popupContent.find('#merge_tag_select option[value=""]').remove();
|
||||
popupContent.find('#merge_tag_select').select2({
|
||||
width: '50%',
|
||||
placeholder: 'Select tag to merge into',
|
||||
allowClear: true,
|
||||
}).val(null).trigger('change');
|
||||
}
|
||||
|
||||
const result = await callGenericPopup(popupContent, POPUP_TYPE.CONFIRM);
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = $(this).closest('.tag_view_item').attr('id');
|
||||
const mergeTagId = $('#merge_tag_select').val() ? String($('#merge_tag_select').val()) : null;
|
||||
|
||||
// Remove the tag from all entities that use it
|
||||
// If we have a replacement tag, add that one instead
|
||||
for (const key of Object.keys(tag_map)) {
|
||||
tag_map[key] = tag_map[key].filter(x => x !== id);
|
||||
if (tag_map[key].includes(id)) {
|
||||
tag_map[key] = tag_map[key].filter(x => x !== id);
|
||||
if (mergeTagId) tag_map[key].push(mergeTagId);
|
||||
}
|
||||
}
|
||||
|
||||
const index = tags.findIndex(x => x.id === id);
|
||||
tags.splice(index, 1);
|
||||
$(`.tag[id="${id}"]`).remove();
|
||||
$(`.tag_view_item[id="${id}"]`).remove();
|
||||
|
||||
toastr.success(`'${tag.name}' deleted${mergeTagId ? ` and merged into '${tags.find(x => x.id === mergeTagId).name}'` : ''}`, 'Delete Tag');
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
@ -1461,35 +1676,41 @@ function onTagRenameInput() {
|
||||
const newName = $(this).text();
|
||||
const tag = tags.find(x => x.id === id);
|
||||
tag.name = newName;
|
||||
$(this).attr('dirty', '');
|
||||
$(`.tag[id="${id}"] .tag_name`).text(newName);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onTagColorize(evt) {
|
||||
/**
|
||||
* Handles the colorization of a tag when the user interacts with the color picker
|
||||
*
|
||||
* @param {*} evt - The custom colorize event object
|
||||
* @param {(tag: Tag, val: string) => void} setColor - A function that sets the color of the tag
|
||||
* @param {string} cssProperty - The CSS property to apply the color to
|
||||
*/
|
||||
function onTagColorize(evt, setColor, cssProperty) {
|
||||
console.debug(evt);
|
||||
const isDefaultColor = $(evt.target).data('default-color') === evt.detail.rgba;
|
||||
$(evt.target).closest('.tag_view_color_picker').find('.link_icon').toggle(!isDefaultColor);
|
||||
|
||||
const id = $(evt.target).closest('.tag_view_item').attr('id');
|
||||
const newColor = evt.detail.rgba;
|
||||
$(evt.target).parent().parent().find('.tag_view_name').css('background-color', newColor);
|
||||
$(`.tag[id="${id}"]`).css('background-color', newColor);
|
||||
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css('background-color', newColor);
|
||||
let newColor = evt.detail.rgba;
|
||||
if (isDefaultColor) newColor = '';
|
||||
|
||||
$(evt.target).closest('.tag_view_item').find('.tag_view_name').css(cssProperty, newColor);
|
||||
const tag = tags.find(x => x.id === id);
|
||||
tag.color = newColor;
|
||||
setColor(tag, newColor);
|
||||
console.debug(tag);
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Debounce redrawing color of the tag in other elements
|
||||
debouncedTagColoring(tag.id, cssProperty, newColor);
|
||||
}
|
||||
|
||||
function onTagColorize2(evt) {
|
||||
console.debug(evt);
|
||||
const id = $(evt.target).closest('.tag_view_item').attr('id');
|
||||
const newColor = evt.detail.rgba;
|
||||
$(evt.target).parent().parent().find('.tag_view_name').css('color', newColor);
|
||||
$(`.tag[id="${id}"]`).css('color', newColor);
|
||||
$(`.bogus_folder_select[tagid="${id}"] .avatar`).css('color', newColor);
|
||||
const tag = tags.find(x => x.id === id);
|
||||
tag.color2 = newColor;
|
||||
console.debug(tag);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
const debouncedTagColoring = debounce((tagId, cssProperty, newColor) => {
|
||||
$(`.tag[id="${tagId}"]`).css(cssProperty, newColor);
|
||||
$(`.bogus_folder_select[tagid="${tagId}"] .avatar`).css(cssProperty, newColor);
|
||||
}, debounce_timeout.quick);
|
||||
|
||||
function onTagListHintClick() {
|
||||
$(this).toggleClass('selected');
|
||||
@ -1529,9 +1750,7 @@ function copyTags(data) {
|
||||
tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));
|
||||
}
|
||||
|
||||
function printViewTagList(empty = true) {
|
||||
const tagContainer = $('#dialogue_popup .tag_view_list_tags');
|
||||
|
||||
function printViewTagList(tagContainer, empty = true) {
|
||||
if (empty) tagContainer.empty();
|
||||
const everything = Object.values(tag_map).flat();
|
||||
const sortedTags = sortTags(tags);
|
||||
@ -1569,7 +1788,7 @@ function registerTagsSlashCommands() {
|
||||
toastr.warning('Tag name must be provided.');
|
||||
return null;
|
||||
}
|
||||
let tag = tags.find(t => t.name === tagName);
|
||||
let tag = getTag(tagName);
|
||||
if (allowCreate && !tag) {
|
||||
tag = createNewTag(tagName);
|
||||
}
|
||||
@ -1580,21 +1799,6 @@ function registerTagsSlashCommands() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
function updateTagsList() {
|
||||
switch (menu_type) {
|
||||
case 'characters':
|
||||
printTagFilters(tag_filter_types.character);
|
||||
printTagFilters(tag_filter_types.group_member);
|
||||
break;
|
||||
case 'character_edit':
|
||||
applyTagsOnCharacterSelect();
|
||||
break;
|
||||
case 'group_edit':
|
||||
select_group_chats(selected_group, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-add',
|
||||
returns: 'true/false - Whether the tag was added or was assigned already',
|
||||
@ -1604,8 +1808,7 @@ function registerTagsSlashCommands() {
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName, { allowCreate: true });
|
||||
if (!tag) return 'false';
|
||||
const result = addTagToEntity(tag, key);
|
||||
updateTagsList();
|
||||
const result = addTagsToEntity(tag, key);
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
@ -1658,7 +1861,6 @@ function registerTagsSlashCommands() {
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
const result = removeTagFromEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
@ -1781,22 +1983,31 @@ export function initTags() {
|
||||
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
|
||||
eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
|
||||
|
||||
$(document).on('input', '#dialogue_popup input[name="auto_sort_tags"]', (evt) => {
|
||||
$(document).on('input', '#tag_view_list input[name="auto_sort_tags"]', (evt) => {
|
||||
const toggle = $(evt.target).is(':checked');
|
||||
toggleAutoSortTags(evt.originalEvent, toggle);
|
||||
printViewTagList();
|
||||
printViewTagList($('#tag_view_list .tag_view_list_tags'));
|
||||
});
|
||||
$(document).on('focusout', '#dialogue_popup .tag_view_name', (evt) => {
|
||||
$(document).on('focusout', '#tag_view_list .tag_view_name', (evt) => {
|
||||
// Reorder/reprint tags, but only if the name actually has changed, and only if we auto sort tags
|
||||
if (!power_user.auto_sort_tags || !$(evt.target).is('[dirty]')) return;
|
||||
|
||||
// Remember the order, so we can flash highlight if it changed after reprinting
|
||||
const tagId = $(evt.target).parent('.tag_view_item').attr('id');
|
||||
const oldOrder = $('#dialogue_popup .tag_view_item').map((_, el) => el.id).get();
|
||||
const tagId = ($(evt.target).closest('.tag_view_item')).attr('id');
|
||||
const oldOrder = $('#tag_view_list .tag_view_item').map((_, el) => el.id).get();
|
||||
|
||||
printViewTagList();
|
||||
printViewTagList($('#tag_view_list .tag_view_list_tags'));
|
||||
|
||||
const newOrder = $('#dialogue_popup .tag_view_item').map((_, el) => el.id).get();
|
||||
// If the new focus would've been inside the now redrawn tag list, we should at least move back the focus to the current name
|
||||
// Otherwise tab-navigation gets a bit weird
|
||||
if (evt.relatedTarget instanceof HTMLElement && $(evt.relatedTarget).closest('#tag_view_list')) {
|
||||
$(`#tag_view_list .tag_view_item[id="${tagId}"] .tag_view_name`)[0]?.focus();
|
||||
}
|
||||
|
||||
const newOrder = $('#tag_view_list .tag_view_item').map((_, el) => el.id).get();
|
||||
const orderChanged = !oldOrder.every((id, index) => id === newOrder[index]);
|
||||
if (orderChanged) {
|
||||
flashHighlight($(`#dialogue_popup .tag_view_item[id="${tagId}"]`));
|
||||
flashHighlight($(`#tag_view_list .tag_view_item[id="${tagId}"]`));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
<div class="flex-container flexFlowColumn height100p">
|
||||
<h3 data-i18n="Additional Parameters">Additional Parameters</h3>
|
||||
<div class="flex1 flex-container flexFlowColumn">
|
||||
<h4 data-i18n="Include Body Parameters">Include Body Parameters</h4>
|
||||
<textarea id="custom_include_body" class="flex1" placeholder="Parameters to be included in the Chat Completion request body (YAML object) Example: - top_k: 20 - repetition_penalty: 1.1" data-i18n="[placeholder]custom_include_body_desc"></textarea>
|
||||
</div>
|
||||
<div class="flex1 flex-container flexFlowColumn">
|
||||
<h4 data-i18n="Exclude Body Parameters">Exclude Body Parameters</h4>
|
||||
<textarea id="custom_exclude_body" class="flex1" placeholder="Parameters to be excluded from the Chat Completion request body (YAML array) Example: - frequency_penalty - presence_penalty" data-i18n="[placeholder]custom_exclude_body_desc"></textarea>
|
||||
</div>
|
||||
<div class="flex1 flex-container flexFlowColumn">
|
||||
<h4 data-i18n="Include Request Headers">Include Request Headers</h4>
|
||||
<textarea id="custom_include_headers" class="flex1" placeholder="Additional headers for Chat Completion requests (YAML object) Example: - CustomHeader: custom-value - AnotherHeader: custom-value" data-i18n="[placeholder]custom_include_headers_desc"></textarea>
|
||||
</div>
|
||||
</div>
|
@ -1,25 +1,26 @@
|
||||
Text formatting commands:
|
||||
<ul>
|
||||
<li><tt>*text*</tt> - displays as <i>italics</i></li>
|
||||
<li><tt>**text**</tt> - displays as <b>bold</b></li>
|
||||
<li><tt>***text***</tt> - displays as <b><i>bold italics</i></b></li>
|
||||
<li><tt>__text__</tt> - displays as an <u>underline</u></li>
|
||||
<li><tt>~~text~~</tt> - displays as a <del>strikethough</del></li>
|
||||
<li><tt>[text](url)</tt> - displays as a <a href="#">hyperlink</a></li>
|
||||
<li><tt></tt> - displays as an image</li>
|
||||
<li><tt>```text```</tt> - displays as a code block (new lines allowed between the backticks)</li>
|
||||
<span data-i18n="help_format_1">Text formatting commands:</span>
|
||||
<ul> <!-- I know it seems like all these 'display as' could fit into just a couple entries, but other languages might need it with their grammar
|
||||
Actually I feel that languages with SOV might need even more extra entries, but let's KISS it for now, those can be easily added in the future without breaking anything -->
|
||||
<li><tt data-i18n="help_format_2">*text*</tt> - <span data-i18n="help_format_3">displays as </span><i data-i18n="help_format_4">italics</i></li>
|
||||
<li><tt data-i18n="help_format_5">**text**</tt> - <span data-i18n="help_format_6">displays as </span><b data-i18n="help_format_7">bold</b></li>
|
||||
<li><tt data-i18n="help_format_8">***text***</tt> - <span data-i18n="help_format_9">displays as </span><b><i data-i18n="help_format_10">bold italics</i></b></li>
|
||||
<li><tt data-i18n="help_format_11">__text__</tt> - <span data-i18n="help_format_12">displays as an </span><u data-i18n="help_format_13">underline</u></li>
|
||||
<li><tt data-i18n="help_format_14">~~text~~</tt> - <span data-i18n="help_format_15">displays as a </span><del data-i18n="help_format_16">strikethough</del></li>
|
||||
<li><tt data-i18n="help_format_17">[text](url)</tt> - <span data-i18n="help_format_18">displays as a </span><a href="#" data-i18n="help_format_19">hyperlink</a></li>
|
||||
<li><tt data-i18n="help_format_20"></tt> - <span data-i18n="help_format_21">displays as an image</span></li>
|
||||
<li><tt data-i18n="help_format_22">```text```</tt> - <span data-i18n="help_format_23">displays as a code block (new lines allowed between the backticks)</span></li>
|
||||
</ul>
|
||||
<pre><code> like this</code></pre>
|
||||
<pre><code data-i18n="help_format_like_this"> like this</code></pre>
|
||||
<ul>
|
||||
<li><tt>`text`</tt> - displays as <code>inline code</code></li>
|
||||
<li><tt>> text</tt> - displays as a blockquote (note the space after >)</li>
|
||||
<blockquote>like this</blockquote>
|
||||
<li><tt># text</tt> - displays as a large header (note the space)</li>
|
||||
<h1>like this</h1>
|
||||
<li><tt>## text</tt> - displays as a medium header (note the space)</li>
|
||||
<h2>like this</h2>
|
||||
<li><tt>### text</tt> - displays as a small header (note the space)</li>
|
||||
<h3>like this</h3>
|
||||
<li><tt>$$ text $$</tt> - renders a LaTeX formula (if enabled)</li>
|
||||
<li><tt>$ text $</tt> - renders an AsciiMath formula (if enabled)</li>
|
||||
<li><tt data-i18n="help_format_24">`text`</tt> - <span data-i18n="help_format_25">displays as </span><code data-i18n="help_format_26">inline code</code></li>
|
||||
<li><tt data-i18n="help_format_27">> text</tt> - <span data-i18n="help_format_28">displays as a blockquote (note the space after >)</span></li>
|
||||
<blockquote data-i18n="help_format_like_this">like this</blockquote>
|
||||
<li><tt data-i18n="help_format_29"># text</tt> - <span data-i18n="help_format_30">displays as a large header (note the space)</span></li>
|
||||
<h1 data-i18n="help_format_like_this">like this</h1>
|
||||
<li><tt data-i18n="help_format_32">## text</tt> - <span data-i18n="help_format_33">displays as a medium header (note the space)</span></li>
|
||||
<h2 data-i18n="help_format_like_this">like this</h2>
|
||||
<li><tt data-i18n="help_format_35">### text</tt> - <span data-i18n="help_format_36">displays as a small header (note the space)</span></li>
|
||||
<h3 data-i18n="help_format_like_this">like this</h3>
|
||||
<li><tt data-i18n="help_format_38">$$ text $$</tt> - <span data-i18n="help_format_39">renders a LaTeX formula (if enabled)</span></li>
|
||||
<li><tt data-i18n="help_format_40">$ text $</tt> - <span data-i18n="help_format_41">renders an AsciiMath formula (if enabled)</span></li>
|
||||
</ul>
|
||||
|
@ -1,11 +1,11 @@
|
||||
Hello there! Please select the help topic you would like to learn more about:
|
||||
<span data-i18n="help_1">Hello there! Please select the help topic you would like to learn more about:</span>
|
||||
<ul>
|
||||
<li><a href="#" data-displayHelp="1">Slash Commands</a> (or <tt>/help slash</tt>)</li>
|
||||
<li><a href="#" data-displayHelp="2">Formatting</a> (or <tt>/help format</tt>)</li>
|
||||
<li><a href="#" data-displayHelp="3">Hotkeys</a> (or <tt>/help hotkeys</tt>)</li>
|
||||
<li><a href="#" data-displayHelp="4">{{Macros}}</a> (or <tt>/help macros</tt>)</li>
|
||||
<li><a href="#" data-displayHelp="1" data-i18n="help_2">Slash Commands</a> (<span data-i18n="help_or">or</span> <tt>/help slash</tt>)</li>
|
||||
<li><a href="#" data-displayHelp="2" data-i18n="help_3">Formatting</a> (<span data-i18n="help_or">or</span> <tt>/help format</tt>)</li>
|
||||
<li><a href="#" data-displayHelp="3" data-i18n="help_4">Hotkeys</a> (<span data-i18n="help_or">or</span> <tt>/help hotkeys</tt>)</li>
|
||||
<li><a href="#" data-displayHelp="4" data-i18n="help_5">{{Macros}}</a> (<span data-i18n="help_or">or</span> <tt>/help macros</tt>)</li>
|
||||
</ul>
|
||||
<br>
|
||||
<b>
|
||||
Still got questions left? The <a target="_blank" href="https://docs.sillytavern.app/">Official SillyTavern Documentation Website</a> has much more information!
|
||||
<span data-i18n="help_6">Still got questions left? The</span> <a target="_blank" href="https://docs.sillytavern.app/" data-i18n="help_7">Official SillyTavern Documentation Website</a><span data-i18n="help_8"> has much more information!</span>
|
||||
</b>
|
||||
|
@ -1,13 +1,13 @@
|
||||
Hotkeys/Keybinds:
|
||||
<span data-i18n="help_hotkeys_0">Hotkeys/Keybinds</span>:
|
||||
<ul>
|
||||
<li><tt>Up</tt> = Edit last message in chat</li>
|
||||
<li><tt>Ctrl+Up</tt> = Edit last USER message in chat</li>
|
||||
<li><tt>Left</tt> = swipe left</li>
|
||||
<li><tt>Right</tt> = swipe right (NOTE: swipe hotkeys are disabled when chatbar has something typed into it)</li>
|
||||
<li><tt>Enter</tt> (with chat bar selected) = send your message to AI</li>
|
||||
<li><tt>Ctrl+Enter</tt> = Regenerate the last AI response</li>
|
||||
<li><tt>Alt+Enter</tt> = Continue the last AI response</li>
|
||||
<li><tt>Escape</tt> = stop AI response generation, close UI panels, cancel message edit</li>
|
||||
<li><tt>Ctrl+Shift+Up</tt> = Scroll to context line</li>
|
||||
<li><tt>Ctrl+Shift+Down</tt> = Scroll chat to bottom</li>
|
||||
<li><tt data-i18n="help_hotkeys_1">Up</tt> = <span data-i18n="help_hotkeys_2">Edit last message in chat</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_3">Ctrl+Up</tt> = <span data-i18n="help_hotkeys_4">Edit last USER message in chat</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_5">Left</tt> = <span data-i18n="help_hotkeys_6">swipe left</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_7">Right</tt> = <span data-i18n="help_hotkeys_8">swipe right (NOTE: swipe hotkeys are disabled when chatbar has something typed into it)</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_9">Enter</tt> <span data-i18n="help_hotkeys_10">(with chat bar selected)</span> = <span data-i18n="help_hotkeys_10_1">send your message to AI</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_11">Ctrl+Enter</tt> = <span data-i18n="help_hotkeys_12">Regenerate the last AI response</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_13">Alt+Enter</tt> = <span data-i18n="help_hotkeys_14">Continue the last AI response</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_15">Escape</tt> = <span data-i18n="help_hotkeys_16">stop AI response generation, close UI panels, cancel message edit</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_17">Ctrl+Shift+Up</tt> = <span data-i18n="help_hotkeys_18">Scroll to context line</span></li>
|
||||
<li><tt data-i18n="help_hotkeys_19">Ctrl+Shift+Down</tt> = <span data-i18n="help_hotkeys_20">Scroll chat to bottom</span></li>
|
||||
</ul>
|
||||
|
@ -1,91 +1,91 @@
|
||||
<div>
|
||||
<div data-i18n="System-wide Replacement Macros (in order of evaluation):">
|
||||
System-wide Replacement Macros (in order of evaluation):
|
||||
</div>
|
||||
<ul>
|
||||
<li><tt>{{pipe}}</tt> – only for slash command batching. Replaced with the returned result of the previous command.</li>
|
||||
<li><tt>{{newline}}</tt> – just inserts a newline.</li>
|
||||
<li><tt>{{trim}}</tt> – trims newlines surrounding this macro.</li>
|
||||
<li><tt>{{noop}}</tt> – no operation, just an empty string.</li>
|
||||
<li><tt>{{original}}</tt> – global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.</li>
|
||||
<li><tt>{{input}}</tt> – the user input</li>
|
||||
<li><tt>{{charPrompt}}</tt> – the Character's Main Prompt override</li>
|
||||
<li><tt>{{charJailbreak}}</tt> – the Character's Jailbreak Prompt override</li>
|
||||
<li><tt>{{description}}</tt> – the Character's Description</li>
|
||||
<li><tt>{{personality}}</tt> – the Character's Personality</li>
|
||||
<li><tt>{{scenario}}</tt> – the Character's Scenario</li>
|
||||
<li><tt>{{persona}}</tt> – your current Persona Description</li>
|
||||
<li><tt>{{mesExamples}}</tt> – the Character's Dialogue Examples</li>
|
||||
<li><tt>{{mesExamplesRaw}}</tt> – unformatted Dialogue Examples <b>(only for Story String)</b></li>
|
||||
<li><tt>{{user}}</tt> – your current Persona username</li>
|
||||
<li><tt>{{char}}</tt> – the Character's name</li>
|
||||
<li><tt>{{char_version}}</tt> – the Character's version number</li>
|
||||
<li><tt>{{group}}</tt> – a comma-separated list of group member names or the character name in solo chats. Alias: {{charIfNotGroup}}</li>
|
||||
<li><tt>{{model}}</tt> – a text generation model name for the currently selected API. <b>Can be inaccurate!</b></li>
|
||||
<li><tt>{{lastMessage}}</tt> - the text of the latest chat message.</li>
|
||||
<li><tt>{{lastMessageId}}</tt> – index # of the latest chat message. Useful for slash command batching.</li>
|
||||
<li><tt>{{firstIncludedMessageId}}</tt> - the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</li>
|
||||
<li><tt>{{currentSwipeId}}</tt> – the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</li>
|
||||
<li><tt>{{lastSwipeId}}</tt> – the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</li>
|
||||
<li><tt>{{// (note)}}</tt> – you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li>
|
||||
<li><tt>{{time}}</tt> – the current time</li>
|
||||
<li><tt>{{date}}</tt> – the current date</li>
|
||||
<li><tt>{{weekday}}</tt> – the current weekday</li>
|
||||
<li><tt>{{isotime}}</tt> – the current ISO time (24-hour clock)</li>
|
||||
<li><tt>{{isodate}}</tt> – the current ISO date (YYYY-MM-DD)</li>
|
||||
<li><tt>{{datetimeformat …}}</tt> – the current date/time in the specified format, e. g. for German date/time: <tt>{{datetimeformat DD.MM.YYYY HH:mm}}</tt></li>
|
||||
<li><tt>{{time_UTC±#}}</tt> – the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2</li>
|
||||
<li><tt>{{timeDiff::(time1)::(time2)}}</tt> – the time difference between time1 and time2. Accepts time and date macros. (Ex: {{timeDiff::{{isodate}} {{time}}::2024/5/11 12:30:00}})</li>
|
||||
<li><tt>{{idle_duration}}</tt> – the time since the last user message was sent</li>
|
||||
<li><tt>{{bias "text here"}}</tt> – sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li>
|
||||
<li><tt>{{roll:(formula)}}</tt> – rolls a dice. (ex: <tt>>{{roll:1d6}}</tt> will roll a 6-sided dice and return a number between 1 and 6)</li>
|
||||
<li><tt>{{random:(args)}}</tt> – returns a random item from the list. (ex: <tt>{{random:1,2,3,4}}</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li>
|
||||
<li><tt>{{random::(arg1)::(arg2)}}</tt> – alternative syntax for random that allows to use commas in the list items.</li>
|
||||
<li><tt>{{pick::(args)}}</tt> – picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won't be re-rolled on consecutive messages and prompt processing.</li>
|
||||
<li><tt>{{banned "text here"}}</tt> – dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li>
|
||||
<li><tt>{{pipe}}</tt> – <span data-i18n="help_macros_1">only for slash command batching. Replaced with the returned result of the previous command.</span></li>
|
||||
<li><tt>{{newline}}</tt> – <span data-i18n="help_macros_2">just inserts a newline.</span></li>
|
||||
<li><tt>{{trim}}</tt> – <span data-i18n="help_macros_3">trims newlines surrounding this macro.</span></li>
|
||||
<li><tt>{{noop}}</tt> – <span data-i18n="help_macros_4">no operation, just an empty string.</span></li>
|
||||
<li><tt>{{original}}</tt> – <span data-i18n="help_macros_5">global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.</span></li>
|
||||
<li><tt>{{input}}</tt> – <span data-i18n="help_macros_6">the user input</span></li>
|
||||
<li><tt>{{charPrompt}}</tt> – <span data-i18n="help_macros_7">the Character's Main Prompt override</span></li>
|
||||
<li><tt>{{charJailbreak}}</tt> – <span data-i18n="help_macros_8">the Character's Jailbreak Prompt override</span></li>
|
||||
<li><tt>{{description}}</tt> – <span data-i18n="help_macros_9">the Character's Description</span></li>
|
||||
<li><tt>{{personality}}</tt> – <span data-i18n="help_macros_10">the Character's Personality</span></li>
|
||||
<li><tt>{{scenario}}</tt> – <span data-i18n="help_macros_11">the Character's Scenario</span></li>
|
||||
<li><tt>{{persona}}</tt> – <span data-i18n="help_macros_12">your current Persona Description</span></li>
|
||||
<li><tt>{{mesExamples}}</tt> – <span data-i18n="help_macros_13">the Character's Dialogue Examples</span></li>
|
||||
<li><tt>{{mesExamplesRaw}}</tt> – <span data-i18n="help_macros_14">unformatted Dialogue Examples </span><b data-i18n="(only for Story String)">(only for Story String)</b></li>
|
||||
<li><tt>{{user}}</tt> – <span data-i18n="help_macros_15">your current Persona username</span></li>
|
||||
<li><tt>{{char}}</tt> – <span data-i18n="help_macros_16">the Character's name</span></li>
|
||||
<li><tt>{{char_version}}</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li>
|
||||
<li><tt>{{group}}</tt> – <span data-i18n="help_macros_18">a comma-separated list of group member names or the character name in solo chats. Alias: {{charIfNotGroup}}</span></li>
|
||||
<li><tt>{{model}}</tt> – <span data-i18n="help_macros_19">a text generation model name for the currently selected API. </span><b data-i18n="Can be inaccurate!">Can be inaccurate!</b></li>
|
||||
<li><tt>{{lastMessage}}</tt> - <span data-i18n="help_macros_20">the text of the latest chat message.</span></li>
|
||||
<li><tt>{{lastMessageId}}</tt> – <span data-i18n="help_macros_21">index # of the latest chat message. Useful for slash command batching.</span></li>
|
||||
<li><tt>{{firstIncludedMessageId}}</tt> - <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li>
|
||||
<li><tt>{{currentSwipeId}}</tt> – <span data-i18n="help_macros_23">the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
|
||||
<li><tt>{{lastSwipeId}}</tt> – <span data-i18n="help_macros_24">the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
|
||||
<li><tt>{{// (note)}}</tt> – <span data-i18n="help_macros_25">you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</span></li>
|
||||
<li><tt>{{time}}</tt> – <span data-i18n="help_macros_26">the current time</span></li>
|
||||
<li><tt>{{date}}</tt> – <span data-i18n="help_macros_27">the current date</span></li>
|
||||
<li><tt>{{weekday}}</tt> – <span data-i18n="help_macros_28">the current weekday</span></li>
|
||||
<li><tt>{{isotime}}</tt> – <span data-i18n="help_macros_29">the current ISO time (24-hour clock)</span></li>
|
||||
<li><tt>{{isodate}}</tt> – <span data-i18n="help_macros_30">the current ISO date (YYYY-MM-DD)</span></li>
|
||||
<li><tt>{{datetimeformat …}}</tt> – <span data-i18n="help_macros_31">the current date/time in the specified format, e. g. for German date/time: </span><tt>{{datetimeformat DD.MM.YYYY HH:mm}}</tt></li>
|
||||
<li><tt>{{time_UTC±#}}</tt> – <span data-i18n="help_macros_32">the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2</span></li>
|
||||
<li><tt>{{timeDiff::(time1)::(time2)}}</tt> – <span data-i18n="help_macros_33">the time difference between time1 and time2. Accepts time and date macros. (Ex: {{timeDiff::{{isodate}} {{time}}::2024/5/11 12:30:00}})</span></li>
|
||||
<li><tt>{{idle_duration}}</tt> – <span data-i18n="help_macros_34">the time since the last user message was sent</span></li>
|
||||
<li><tt>{{bias "text here"}}</tt> – <span data-i18n="help_macros_35">sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</span></li>
|
||||
<li><tt>{{roll:(formula)}}</tt> – <span data-i18n="help_macros_36">rolls a dice. (ex: </span><tt>{{roll:1d6}}</tt><span data-i18n="space_ will roll a 6-sided dice and return a number between 1 and 6)"> will roll a 6-sided dice and return a number between 1 and 6)</span></li>
|
||||
<li><tt>{{random:(args)}}</tt> – <span data-i18n="help_macros_37">returns a random item from the list. (ex: </span><tt>{{random:1,2,3,4}}</tt><span data-i18n="space_ will return 1 of the 4 numbers at random. Works with text lists too."> will return 1 of the 4 numbers at random. Works with text lists too.</span></li>
|
||||
<li><tt>{{random::(arg1)::(arg2)}}</tt> – <span data-i18n="help_macros_38">alternative syntax for random that allows to use commas in the list items.</span></li>
|
||||
<li><tt>{{pick::(args)}}</tt> – <span data-i18n="help_macros_39">picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won't be re-rolled on consecutive messages and prompt processing.</span></li>
|
||||
<li><tt>{{banned "text here"}}</tt> – <span data-i18n="help_macros_40">dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</span></li>
|
||||
</ul>
|
||||
<div>
|
||||
<div data-i18n="Instruct Mode and Context Template Macros:">
|
||||
Instruct Mode and Context Template Macros:
|
||||
</div>
|
||||
<div>
|
||||
<small>(enabled in the Advanced Formatting settings)</small>
|
||||
<small data-i18n="(enabled in the Advanced Formatting settings)">(enabled in the Advanced Formatting settings)</small>
|
||||
</div>
|
||||
<ul>
|
||||
<li><tt>{{maxPrompt}}</tt> – max allowed prompt length in tokens = (context size - response length)</li>
|
||||
<li><tt>{{exampleSeparator}}</tt> – context template example dialogues separator</li>
|
||||
<li><tt>{{chatStart}}</tt> – context template chat start line</li>
|
||||
<li><tt>{{systemPrompt}}</tt> – main system prompt (either character prompt override if chosen, or instructSystemPrompt)</li>
|
||||
<li><tt>{{instructSystemPrompt}}</tt> – instruct system prompt</li>
|
||||
<li><tt>{{instructSystemPromptPrefix}}</tt> – instruct system prompt prefix sequence</li>
|
||||
<li><tt>{{instructSystemPromptSuffix}}</tt> – instruct system prompt suffix sequence</li>
|
||||
<li><tt>{{instructUserPrefix}}</tt> – instruct user prefix sequence</li>
|
||||
<li><tt>{{instructUserSuffix}}</tt> – instruct user suffix sequence</li>
|
||||
<li><tt>{{instructAssistantPrefix}}</tt> – instruct assistant prefix sequence</li>
|
||||
<li><tt>{{instructAssistantSuffix}}</tt> – instruct assistant suffix sequence</li>
|
||||
<li><tt>{{instructFirstAssistantPrefix}}</tt> – instruct assistant first output sequence</li>
|
||||
<li><tt>{{instructLastAssistantPrefix}}</tt> – instruct assistant last output sequence</li>
|
||||
<li><tt>{{instructSystemPrefix}}</tt> – instruct system message prefix sequence</li>
|
||||
<li><tt>{{instructSystemSuffix}}</tt> – instruct system message suffix sequence</li>
|
||||
<li><tt>{{instructSystemInstructionPrefix}}</tt> – instruct system instruction prefix</li>
|
||||
<li><tt>{{instructUserFiller}}</tt> – instruct first user message filler</li>
|
||||
<li><tt>{{instructStop}}</tt> – instruct stop sequence</li>
|
||||
<li><tt>{{maxPrompt}}</tt> – <span data-i18n="help_macros_41">max allowed prompt length in tokens = (context size - response length)</span></li>
|
||||
<li><tt>{{exampleSeparator}}</tt> – <span data-i18n="help_macros_42">context template example dialogues separator</span></li>
|
||||
<li><tt>{{chatStart}}</tt> – <span data-i18n="help_macros_43">context template chat start line</span></li>
|
||||
<li><tt>{{systemPrompt}}</tt> – <span data-i18n="help_macros_44">main system prompt (either character prompt override if chosen, or instructSystemPrompt)</span></li>
|
||||
<li><tt>{{instructSystemPrompt}}</tt> – <span data-i18n="help_macros_45">instruct system prompt</span></li>
|
||||
<li><tt>{{instructSystemPromptPrefix}}</tt> – <span data-i18n="help_macros_46">instruct system prompt prefix sequence</span></li>
|
||||
<li><tt>{{instructSystemPromptSuffix}}</tt> – <span data-i18n="help_macros_47">instruct system prompt suffix sequence</span></li>
|
||||
<li><tt>{{instructUserPrefix}}</tt> – <span data-i18n="help_macros_48">instruct user prefix sequence</span></li>
|
||||
<li><tt>{{instructUserSuffix}}</tt> – <span data-i18n="help_macros_49">instruct user suffix sequence</span></li>
|
||||
<li><tt>{{instructAssistantPrefix}}</tt> – <span data-i18n="help_macros_50">instruct assistant prefix sequence</span></li>
|
||||
<li><tt>{{instructAssistantSuffix}}</tt> – <span data-i18n="help_macros_51">instruct assistant suffix sequence</span></li>
|
||||
<li><tt>{{instructFirstAssistantPrefix}}</tt> – <span data-i18n="help_macros_52">instruct assistant first output sequence</span></li>
|
||||
<li><tt>{{instructLastAssistantPrefix}}</tt> – <span data-i18n="help_macros_53">instruct assistant last output sequence</span></li>
|
||||
<li><tt>{{instructSystemPrefix}}</tt> – <span data-i18n="help_macros_54">instruct system message prefix sequence</span></li>
|
||||
<li><tt>{{instructSystemSuffix}}</tt> – <span data-i18n="help_macros_55">instruct system message suffix sequence</span></li>
|
||||
<li><tt>{{instructSystemInstructionPrefix}}</tt> – <span data-i18n="help_macros_56">instruct system instruction prefix</span></li>
|
||||
<li><tt>{{instructUserFiller}}</tt> – <span data-i18n="help_macros_57">instruct first user message filler</span></li>
|
||||
<li><tt>{{instructStop}}</tt> – <span data-i18n="help_macros_58">instruct stop sequence</span></li>
|
||||
</ul>
|
||||
<div>
|
||||
<div data-i18n="Chat variables Macros:">
|
||||
Chat variables Macros:
|
||||
</div>
|
||||
<div><small>Local variables = unique to the current chat</small></div>
|
||||
<div><small>Global variables = works in any chat for any character</small></div>
|
||||
<div><small>Scoped variables = works in STscript</small></div>
|
||||
<div><small data-i18n="Local variables = unique to the current chat">Local variables = unique to the current chat</small></div>
|
||||
<div><small data-i18n="Global variables = works in any chat for any character">Global variables = works in any chat for any character</small></div>
|
||||
<div><small data-i18n="Scoped variables = works in STscript">Scoped variables = works in STscript</small></div>
|
||||
<ul>
|
||||
<li><tt>{{getvar::name}}</tt> – replaced with the value of the local variable "name"</li>
|
||||
<li><tt>{{setvar::name::value}}</tt> – replaced with empty string, sets the local variable "name" to "value"</li>
|
||||
<li><tt>{{addvar::name::increment}}</tt> – replaced with empty strings, adds a numeric value of "increment" to the local variable "name"</li>
|
||||
<li><tt>{{incvar::name}}</tt> – replaced with the result of the increment of value of the variable "name" by 1</li>
|
||||
<li><tt>{{decvar::name}}</tt> – replaced with the result of the decrement of value of the variable "name" by 1</li>
|
||||
<li><tt>{{getglobalvar::name}}</tt> – replaced with the value of the global variable "name"</li>
|
||||
<li><tt>{{setglobalvar::name::value}}</tt> – replaced with empty string, sets the global variable "name" to "value"</li>
|
||||
<li><tt>{{addglobalvar::name::value}}</tt> – replaced with empty string, adds a numeric value of "increment" to the global variable "name"</li>
|
||||
<li><tt>{{incglobalvar::name}}</tt> – replaced with the result of the increment of value of the global variable "name" by 1</li>
|
||||
<li><tt>{{decglobalvar::name}}</tt> – replaced with the result of the decrement of value of the global variable "name" by 1</li>
|
||||
<li><tt>{{var::name}}</tt> – replaced with the value of the scoped variable "name"</li>
|
||||
<li><tt>{{var::name::index}}</tt> – replaced with the value of item at index (for arrays / lists or objects / dictionaries) of the scoped variable "name"</li>
|
||||
<li><tt>{{getvar::name}}</tt> – <span data-i18n="help_macros_59">replaced with the value of the local variable "name"</span></li>
|
||||
<li><tt>{{setvar::name::value}}</tt> – <span data-i18n="help_macros_60">replaced with empty string, sets the local variable "name" to "value"</span></li>
|
||||
<li><tt>{{addvar::name::increment}}</tt> – <span data-i18n="help_macros_61">replaced with empty strings, adds a numeric value of "increment" to the local variable "name"</span></li>
|
||||
<li><tt>{{incvar::name}}</tt> – <span data-i18n="help_macros_62">replaced with the result of the increment of value of the variable "name" by 1</span></li>
|
||||
<li><tt>{{decvar::name}}</tt> – <span data-i18n="help_macros_63">replaced with the result of the decrement of value of the variable "name" by 1</span></li>
|
||||
<li><tt>{{getglobalvar::name}}</tt> – <span data-i18n="help_macros_64">replaced with the value of the global variable "name"</span></li>
|
||||
<li><tt>{{setglobalvar::name::value}}</tt> – <span data-i18n="help_macros_65">replaced with empty string, sets the global variable "name" to "value"</span></li>
|
||||
<li><tt>{{addglobalvar::name::value}}</tt> – <span data-i18n="help_macros_66">replaced with empty string, adds a numeric value of "increment" to the global variable "name"</span></li>
|
||||
<li><tt>{{incglobalvar::name}}</tt> – <span data-i18n="help_macros_67">replaced with the result of the increment of value of the global variable "name" by 1</span></li>
|
||||
<li><tt>{{decglobalvar::name}}</tt> – <span data-i18n="help_macros_68">replaced with the result of the decrement of value of the global variable "name" by 1</span></li>
|
||||
<li><tt>{{var::name}}</tt> – <span data-i18n="help_macros_69">replaced with the value of the scoped variable "name"</span></li>
|
||||
<li><tt>{{var::name::index}}</tt> – <span data-i18n="help_macros_70">replaced with the value of item at index (for arrays / lists or objects / dictionaries) of the scoped variable "name"</span></li>
|
||||
</ul>
|
||||
|
@ -74,6 +74,20 @@ export function onlyUnique(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first occurrence of a specified item from an array
|
||||
*
|
||||
* @param {*[]} array - The array from which to remove the item
|
||||
* @param {*} item - The item to remove from the array
|
||||
* @returns {boolean} - Returns true if the item was successfully removed, false otherwise.
|
||||
*/
|
||||
export function removeFromArray(array, item) {
|
||||
const index = array.indexOf(item);
|
||||
if (index === -1) return false;
|
||||
array.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string only contains digits.
|
||||
* @param {string} str The string to check.
|
||||
@ -1518,6 +1532,35 @@ export function flashHighlight(element, timespan = 2000) {
|
||||
setTimeout(() => element.removeClass('flash animated'), timespan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given control has an animation applied to it
|
||||
*
|
||||
* @param {HTMLElement} control - The control element to check for animation
|
||||
* @returns {boolean} Whether the control has an animation applied
|
||||
*/
|
||||
export function hasAnimation(control) {
|
||||
const animatioName = getComputedStyle(control, null)["animation-name"];
|
||||
return animatioName != "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
|
||||
*
|
||||
* @param {HTMLElement} control - The control element to listen for animation end event
|
||||
* @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
|
||||
*/
|
||||
export function runAfterAnimation(control, callback) {
|
||||
if (hasAnimation(control)) {
|
||||
const onAnimationEnd = () => {
|
||||
control.removeEventListener('animationend', onAnimationEnd);
|
||||
callback(control);
|
||||
};
|
||||
control.addEventListener('animationend', onAnimationEnd);
|
||||
} else {
|
||||
callback(control);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A common base function for case-insensitive and accent-insensitive string comparisons.
|
||||
*
|
||||
@ -1774,3 +1817,22 @@ export async function checkOverwriteExistingData(type, existingNames, name, { in
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a free name by appending a counter to the given name if it already exists in the list
|
||||
*
|
||||
* @param {string} name - The original name to check for existence in the list
|
||||
* @param {string[]} list - The list of names to check for existence
|
||||
* @param {(n: number) => string} [numberFormatter=(n) => ` #${n}`] - The function used to format the counter
|
||||
* @returns {string} The generated free name
|
||||
*/
|
||||
export function getFreeName(name, list, numberFormatter = (n) => ` #${n}`) {
|
||||
if (!list.includes(name)) {
|
||||
return name;
|
||||
}
|
||||
let counter = 1;
|
||||
while (list.includes(`${name} #${counter}`)) {
|
||||
counter++;
|
||||
}
|
||||
return `${name}${numberFormatter(counter)}`;
|
||||
}
|
||||
|
@ -2669,7 +2669,6 @@ async function getCharacterLore() {
|
||||
worldsToSearch.add(baseWorldName);
|
||||
} else {
|
||||
console.debug(`Character ${name}'s base world could not be found or is empty! Skipping...`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: Maybe make the utility function not use the window context?
|
||||
@ -2696,7 +2695,7 @@ async function getCharacterLore() {
|
||||
entries = entries.concat(newEntries);
|
||||
}
|
||||
|
||||
console.debug(`Character ${characters[this_chid]?.name} lore (${baseWorldName}) has ${entries.length} world info entries`);
|
||||
console.debug(`Character ${name} lore (${Array.from(worldsToSearch)}) has ${entries.length} world info entries`);
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
296
public/style.css
296
public/style.css
@ -1,11 +1,14 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
@import url(css/animations.css);
|
||||
@import url(css/popup.css);
|
||||
@import url(css/promptmanager.css);
|
||||
@import url(css/loader.css);
|
||||
@import url(css/character-group-overlay.css);
|
||||
@import url(css/file-form.css);
|
||||
@import url(css/logprobs.css);
|
||||
@import url(css/accounts.css);
|
||||
@import url(css/tags.css);
|
||||
|
||||
:root {
|
||||
--doc-height: 100%;
|
||||
@ -48,6 +51,9 @@
|
||||
--active: rgb(88, 182, 0);
|
||||
--preferred: rgb(244, 67, 54);
|
||||
|
||||
--interactable-outline-color: var(--white100);
|
||||
--interactable-outline-color-faint: var(--white20a);
|
||||
|
||||
|
||||
/*Default Theme, will be changed by ToolCool Color Picker*/
|
||||
--SmartThemeBodyColor: rgb(220, 220, 210);
|
||||
@ -106,6 +112,9 @@
|
||||
--avatar-base-border-radius: 2px;
|
||||
--avatar-base-border-radius-round: 50%;
|
||||
--inline-avatar-small-factor: 0.6;
|
||||
|
||||
--animation-duration: 125ms;
|
||||
--animation-duration-slow: calc(var(--animation-duration) * 3);
|
||||
}
|
||||
|
||||
* {
|
||||
@ -205,25 +214,43 @@ table.responsiveTable {
|
||||
box-shadow: inset 0 0 5px var(--SmartThemeQuoteColor);
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
|
||||
20%,
|
||||
60%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
0%,
|
||||
40%,
|
||||
80% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
.flash {
|
||||
animation-name: flash;
|
||||
}
|
||||
|
||||
/* Keyboard/focus navigation styling */
|
||||
/* Mimic the outline of keyboard navigation for most most focusable controls */
|
||||
.interactable {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Outline for "normal" buttons should only be when actual keyboard navigation is used */
|
||||
.interactable:focus-visible {
|
||||
outline: 1px solid var(--interactable-outline-color);
|
||||
}
|
||||
|
||||
/* The specific input controls can always have a faint outline, even on mouse interaction, to give a better hint */
|
||||
select:focus-visible,
|
||||
input:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 1px solid var(--interactable-outline-color-faint);
|
||||
}
|
||||
|
||||
input[type='checkbox']:focus-visible {
|
||||
outline: 1px solid var(--interactable-outline-color);
|
||||
}
|
||||
|
||||
/* General dragover styling */
|
||||
.dragover {
|
||||
filter: brightness(1.1) saturate(1.0);
|
||||
outline: 3px dashed var(--SmartThemeBorderColor);
|
||||
animation: pulse 0.5s infinite alternate;
|
||||
}
|
||||
|
||||
.dragover.no_animation {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.tokenItemizingSubclass {
|
||||
font-size: calc(var(--mainFontSize) * 0.8);
|
||||
color: var(--SmartThemeEmColor);
|
||||
@ -246,6 +273,11 @@ table.responsiveTable {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fa-lock.right_menu_button,
|
||||
.fa-unlock.right_menu_button {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.text_muted {
|
||||
font-size: calc(var(--mainFontSize) - 0.2rem);
|
||||
color: var(--white50a);
|
||||
@ -670,7 +702,6 @@ body .panelControlBar {
|
||||
width: var(--bottomFormBlockSize);
|
||||
height: var(--bottomFormBlockSize);
|
||||
margin: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
@ -787,7 +818,6 @@ body .panelControlBar {
|
||||
height: var(--bottomFormBlockSize);
|
||||
font-size: var(--bottomFormIconSize);
|
||||
margin: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
position: relative;
|
||||
opacity: 0.7;
|
||||
@ -802,14 +832,14 @@ body .panelControlBar {
|
||||
}
|
||||
|
||||
.font-family-reset {
|
||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||
font-family: var(--mainFontFamily);
|
||||
font-size: var(--mainFontSize);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#options,
|
||||
#extensionsMenu,
|
||||
.shadow_popup .popper-modal {
|
||||
.popup .popper-modal {
|
||||
display: flex;
|
||||
z-index: 29999;
|
||||
background-color: var(--SmartThemeBlurTintColor);
|
||||
@ -818,6 +848,12 @@ body .panelControlBar {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
flex-flow: column;
|
||||
border-radius: 10px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#extensionsMenu,
|
||||
.options-content {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.options-content,
|
||||
@ -853,11 +889,6 @@ body .panelControlBar {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
#extensionsMenuButton:hover {
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.options-content a,
|
||||
#extensionsMenu>div,
|
||||
.list-group>div,
|
||||
@ -951,7 +982,7 @@ body .panelControlBar {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||
font-family: var(--mainFontFamily);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@ -1026,11 +1057,11 @@ body .panelControlBar {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.avatar.selectable {
|
||||
.avatar.interactable {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.avatar.selectable:hover {
|
||||
.avatar.interactable:hover {
|
||||
opacity: 1;
|
||||
background-color: transparent !important;
|
||||
cursor: pointer;
|
||||
@ -1131,7 +1162,7 @@ textarea {
|
||||
border-radius: 5px;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
font-size: var(--mainFontSize);
|
||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||
font-family: var(--mainFontFamily);
|
||||
padding: 5px 10px;
|
||||
max-height: 90vh;
|
||||
max-height: 90svh;
|
||||
@ -1144,6 +1175,7 @@ textarea.autoSetHeight {
|
||||
|
||||
input,
|
||||
select {
|
||||
font-family: var(--mainFontFamily);
|
||||
font-size: var(--mainFontSize);
|
||||
color: var(--SmartThemeBodyColor);
|
||||
}
|
||||
@ -1160,7 +1192,7 @@ select {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 6px;
|
||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||
font-family: var(--mainFontFamily);
|
||||
margin: 0;
|
||||
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
|
||||
flex: 1;
|
||||
@ -1185,16 +1217,14 @@ select {
|
||||
transition: clip-path 200ms;
|
||||
}
|
||||
|
||||
@keyframes script_progress_pulse {
|
||||
#send_textarea:focus-visible {
|
||||
/* Disable outline for the chat bar itself, we add it to the outer div */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
0%,
|
||||
100% {
|
||||
border-top-color: var(--progColor);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-top-color: var(--progFlashColor);
|
||||
}
|
||||
#send_form:has(#send_textarea:focus-visible) {
|
||||
border-color: var(--interactable-outline-color-faint);
|
||||
outline: 1px solid var(--interactable-outline-color-faint);
|
||||
}
|
||||
|
||||
#form_sheld.isExecutingCommandsFromChatInput.script_paused #send_textarea {
|
||||
@ -2027,9 +2057,13 @@ body[data-stscript-style] .hljs.language-stscript {
|
||||
}
|
||||
}
|
||||
|
||||
.editor_maximize {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#character_popup .editor_maximize {
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
margin: 3px;
|
||||
opacity: 0.75;
|
||||
filter: grayscale(1);
|
||||
-webkit-transition: all 250ms ease-in-out;
|
||||
@ -2103,7 +2137,7 @@ textarea::placeholder {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 5px;
|
||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||
font-family: var(--mainFontFamily);
|
||||
padding: 3px 5px;
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
@ -2146,12 +2180,6 @@ h3 {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
@ -2172,12 +2200,6 @@ input[type="file"] {
|
||||
padding: 0px 10px 0px 5px;
|
||||
}
|
||||
|
||||
#right-nav-panel-tabs .right_menu_button,
|
||||
#CharListButtonAndHotSwaps .right_menu_button {
|
||||
padding-right: 0;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
#chartokenwarning.menu_button {
|
||||
font-size: unset;
|
||||
height: fit-content;
|
||||
@ -2193,13 +2215,14 @@ input[type="file"] {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
padding: 1px;
|
||||
filter: grayscale(1) brightness(75%);
|
||||
-webkit-transition: all 0.5s ease-in-out;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.right_menu_button:hover {
|
||||
filter: brightness(150%) grayscale(1);
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
#rm_button_characters,
|
||||
@ -2208,6 +2231,7 @@ input[type="file"] {
|
||||
#WI_button_panel_pin_div {
|
||||
font-size: 24px;
|
||||
display: inline;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
#rm_button_panel_pin_div,
|
||||
@ -2218,8 +2242,11 @@ input[type="file"] {
|
||||
}
|
||||
|
||||
#rm_button_panel_pin_div:hover,
|
||||
#rm_button_panel_pin_div:has(:focus-visible),
|
||||
#lm_button_panel_pin_div:hover,
|
||||
#WI_button_panel_pin_div:hover {
|
||||
#lm_button_panel_pin_div:has(:focus-visible),
|
||||
#WI_button_panel_pin_div:hover,
|
||||
#WI_button_panel_pin_div:has(:focus-visible) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -2267,6 +2294,7 @@ input[type="file"] {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
opacity: 0.5;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
#rm_button_selected_ch:hover {
|
||||
@ -2294,6 +2322,7 @@ input[type="file"] {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.bulk_select_checkbox {
|
||||
@ -2331,12 +2360,13 @@ input[type="file"] {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
#rm_ch_create_block {
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
padding: 5px;
|
||||
padding: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -2746,19 +2776,23 @@ input[type=search]:focus::-webkit-search-cancel-button {
|
||||
outline: 2px solid var(--golden);
|
||||
}
|
||||
|
||||
.bg_example:hover.locked .bg_example_lock {
|
||||
.bg_example:hover.locked .bg_example_lock,
|
||||
.bg_example:focus-within.locked .bg_example_lock {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg_example:hover:not(.locked) .bg_example_unlock {
|
||||
.bg_example:hover:not(.locked) .bg_example_unlock,
|
||||
.bg_example:focus-within:not(.locked) .bg_example_unlock {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg_example:hover[custom="true"] .bg_example_edit {
|
||||
.bg_example:hover[custom="true"] .bg_example_edit,
|
||||
.bg_example:focus-within[custom="true"] .bg_example_edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg_example:hover[custom="false"] .bg_example_copy {
|
||||
.bg_example:hover[custom="false"] .bg_example_copy,
|
||||
.bg_example:focus-within[custom="false"] .bg_example_copy {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -2783,24 +2817,23 @@ input[type=search]:focus::-webkit-search-cancel-button {
|
||||
}
|
||||
|
||||
.bg_button {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
border-radius: 50%;
|
||||
border-radius: 3px;
|
||||
font-size: 20px;
|
||||
color: var(--black70a);
|
||||
text-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
filter: drop-shadow(0px 0px 3px white);
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg_example:hover .bg_button {
|
||||
.bg_example:hover .bg_button,
|
||||
.bg_example:focus-within .bg_button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -2809,15 +2842,15 @@ input[type=search]:focus::-webkit-search-cancel-button {
|
||||
}
|
||||
|
||||
.bg_example_cross {
|
||||
right: 10px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.bg_example_edit {
|
||||
left: 10px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.bg_example_copy {
|
||||
left: 10px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.bg_example_lock,
|
||||
@ -2846,6 +2879,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.avatar_div {
|
||||
@ -2935,6 +2969,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
margin: 1px;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
@ -3034,7 +3069,8 @@ grammarly-extension {
|
||||
font-size: calc(var(--mainFontSize) * 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
#result_info_text {
|
||||
@ -3044,15 +3080,14 @@ grammarly-extension {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rm_stats_button {
|
||||
cursor: pointer;
|
||||
#result_info .right_menu_button {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Focus */
|
||||
|
||||
#bulk_tag_popup,
|
||||
#dialogue_popup,
|
||||
.dialogue_popup {
|
||||
#dialogue_popup {
|
||||
width: 500px;
|
||||
max-width: 90vw;
|
||||
max-width: 90svw;
|
||||
@ -3100,17 +3135,16 @@ grammarly-extension {
|
||||
min-width: 750px;
|
||||
}
|
||||
|
||||
.horizontal_scrolling_dialogue_popup {
|
||||
#dialogue_popup .horizontal_scrolling_dialogue_popup {
|
||||
overflow-x: unset !important;
|
||||
}
|
||||
|
||||
.vertical_scrolling_dialogue_popup {
|
||||
#dialogue_popup .vertical_scrolling_dialogue_popup {
|
||||
overflow-y: unset !important;
|
||||
}
|
||||
|
||||
#bulk_tag_popup_holder,
|
||||
#dialogue_popup_holder,
|
||||
.dialogue_popup_holder {
|
||||
#dialogue_popup_holder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@ -3118,15 +3152,14 @@ grammarly-extension {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#dialogue_popup_text,
|
||||
.dialogue_popup_text {
|
||||
#dialogue_popup_text {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#dialogue_popup_controls,
|
||||
.dialogue_popup_controls {
|
||||
#dialogue_popup_controls {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
gap: 20px;
|
||||
@ -3134,16 +3167,14 @@ grammarly-extension {
|
||||
|
||||
#bulk_tag_popup_reset,
|
||||
#bulk_tag_popup_remove_mutual,
|
||||
#dialogue_popup_ok,
|
||||
.dialogue_popup_ok {
|
||||
#dialogue_popup_ok {
|
||||
background-color: var(--crimson70a);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#bulk_tag_popup_reset:hover,
|
||||
#bulk_tag_popup_remove_mutual:hover,
|
||||
#dialogue_popup_ok:hover,
|
||||
.dialogue_popup_ok:hover {
|
||||
#dialogue_popup_ok:hover {
|
||||
background-color: var(--crimson-hover);
|
||||
}
|
||||
|
||||
@ -3151,15 +3182,13 @@ grammarly-extension {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
#dialogue_popup_input,
|
||||
.dialogue_popup_input {
|
||||
margin: 10px 0;
|
||||
#dialogue_popup_input {
|
||||
margin: 10px 0 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#bulk_tag_popup_cancel,
|
||||
#dialogue_popup_cancel,
|
||||
.dialogue_popup_cancel {
|
||||
#dialogue_popup_cancel {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -3214,13 +3243,11 @@ grammarly-extension {
|
||||
}
|
||||
|
||||
#dialogue_del_mes .menu_button {
|
||||
|
||||
margin-left: 25px;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
#shadow_popup,
|
||||
.shadow_popup {
|
||||
#shadow_popup {
|
||||
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
|
||||
background-color: var(--black30a);
|
||||
@ -3232,15 +3259,6 @@ grammarly-extension {
|
||||
height: 100svh;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
|
||||
&.shadow_popup {
|
||||
z-index: 9998;
|
||||
}
|
||||
}
|
||||
|
||||
.dialogue_popup.dragover {
|
||||
filter: brightness(1.1) saturate(1.1);
|
||||
outline: 3px dashed var(--SmartThemeBorderColor);
|
||||
}
|
||||
|
||||
#bgtest {
|
||||
@ -3385,7 +3403,7 @@ body #toast-container>div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin) {
|
||||
input[type='checkbox'] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
@ -3403,13 +3421,10 @@ input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
|
||||
flex-shrink: 0;
|
||||
place-content: center;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):not(.del_checkbox) {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin)::before {
|
||||
input[type="checkbox"]::before {
|
||||
content: "";
|
||||
width: 0.65em;
|
||||
height: 0.65em;
|
||||
@ -3420,16 +3435,16 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):checked::before {
|
||||
input[type="checkbox"]:checked::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button_panel_pin):not(#WI_panel_pin):disabled {
|
||||
input[type="checkbox"]:disabled {
|
||||
color: grey;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.del_checkbox {
|
||||
input[type='checkbox'].del_checkbox {
|
||||
display: none;
|
||||
opacity: 0.7;
|
||||
margin-top: 12px;
|
||||
@ -3441,6 +3456,7 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.avatar-container .avatar {
|
||||
@ -3558,6 +3574,16 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.neo-range-slider:hover,
|
||||
input[type="range"]:hover {
|
||||
filter: brightness(1.25);
|
||||
}
|
||||
|
||||
.neo-range-slider:focus-visible,
|
||||
input[type="range"]:focus-visible {
|
||||
outline: 1px solid var(--interactable-outline-color);
|
||||
}
|
||||
|
||||
.range-block-range {
|
||||
margin: 0;
|
||||
flex: 5;
|
||||
@ -3757,35 +3783,29 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
/* height: 20px; */
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
transition: all 200ms;
|
||||
overflow-x: hidden;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.extraMesButtons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mes_buttons .mes_edit,
|
||||
.mes_buttons .mes_bookmark,
|
||||
.mes_buttons .mes_create_bookmark,
|
||||
.extraMesButtonsHint,
|
||||
.tagListHint,
|
||||
.extraMesButtons div {
|
||||
.mes_button,
|
||||
.extraMesButtons>div {
|
||||
cursor: pointer;
|
||||
transition: 0.3s ease-in-out;
|
||||
filter: drop-shadow(0px 0px 2px black);
|
||||
opacity: 0.3;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.mes_buttons .mes_edit:hover,
|
||||
.mes_buttons .mes_bookmark:hover,
|
||||
.mes_buttons .mes_create_bookmark:hover,
|
||||
.extraMesButtonsHint:hover,
|
||||
.tagListHint:hover,
|
||||
.extraMesButtons div:hover {
|
||||
.mes_button:hover,
|
||||
.extraMesButtons>div:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -4092,17 +4112,6 @@ h5 {
|
||||
animation: infinite-spinning 1s ease-out 0s infinite normal;
|
||||
}
|
||||
|
||||
/* HEINOUS */
|
||||
@keyframes infinite-spinning {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#export_character_div {
|
||||
display: grid;
|
||||
grid-template-columns: 340px auto;
|
||||
@ -4256,24 +4265,6 @@ body.big-avatars .missing-avatar {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@keyframes ellipsis {
|
||||
0% {
|
||||
content: ""
|
||||
}
|
||||
|
||||
25% {
|
||||
content: "."
|
||||
}
|
||||
|
||||
50% {
|
||||
content: ".."
|
||||
}
|
||||
|
||||
75% {
|
||||
content: "..."
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--warning);
|
||||
font-weight: bolder;
|
||||
@ -4587,6 +4578,7 @@ body:has(#character_popup.open) #top-settings-holder:has(.drawer-content.openDra
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
font-size: var(--topBarIconSize);
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.drawer-icon.openIcon {
|
||||
@ -5176,5 +5168,3 @@ body:not(.movingUI) .drawer-content.maximized {
|
||||
.regex-highlight {
|
||||
color: #FAF8F6;
|
||||
}
|
||||
|
||||
/* Pastel White */
|
||||
|
Loading…
x
Reference in New Issue
Block a user