mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-09 16:40:10 +01:00
Merge branch 'staging' into slash-command-enums
This commit is contained in:
commit
ffc84f5118
60
.github/readme.md
vendored
60
.github/readme.md
vendored
@ -144,12 +144,14 @@ A full list of included extensions and tutorials on how to use them can be found
|
||||
8. The server will then start, and SillyTavern will pop up in your browser.
|
||||
|
||||
## Installing via SillyTavern Launcher
|
||||
1. Install [Git for Windows](https://gitforwindows.org/)
|
||||
2. Open Windows Explorer (`Win+E`) and make or choose a folder where you wanna install the launcher to
|
||||
3. Open a Command Prompt inside that folder by clicking in the 'Address Bar' at the top, typing `cmd`, and pressing Enter.
|
||||
4. When you see a black box, insert the following command: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
|
||||
5. Double-click on `installer.bat` and choose what you wanna install
|
||||
6. After installation double-click on `launcher.bat`
|
||||
1. On your keyboard: press **`WINDOWS + R`** to open Run dialog box. Then, run the following command to install git:
|
||||
```shell
|
||||
cmd /c winget install -e --id Git.Git
|
||||
```
|
||||
2. On your keyboard: press **`WINDOWS + E`** to open File Explorer, then navigate to the folder where you want to install the launcher. Once in the desired folder, type `cmd` into the address bar and press enter. Then, run the following command:
|
||||
```shell
|
||||
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
|
||||
```
|
||||
|
||||
## Installing via GitHub Desktop
|
||||
(This allows git usage **only** in GitHub Desktop, if you want to use `git` on the command line too, you also need to install [Git for Windows](https://gitforwindows.org/))
|
||||
@ -183,18 +185,40 @@ For MacOS / Linux all of these will be done in a Terminal.
|
||||
|
||||
### For Linux users
|
||||
1. Open your favorite terminal and install git
|
||||
2. Download Sillytavern Launcher with: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
|
||||
3. Navigate to the SillyTavern-Launcher with: `cd SillyTavern-Launcher`
|
||||
4. Start the install launcher with: `chmod +x install.sh && ./install.sh` and choose what you wanna install
|
||||
5. After installation start the launcher with: `chmod +x launcher.sh && ./launcher.sh`
|
||||
2. Git clone the Sillytavern-Launcher with:
|
||||
```shell
|
||||
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
|
||||
```
|
||||
3. Start the installer.sh with:
|
||||
```shell
|
||||
chmod +x install.sh && ./install.sh
|
||||
```
|
||||
4. After installation start the launcher.sh with:
|
||||
```shell
|
||||
chmod +x launcher.sh && ./launcher.sh
|
||||
```
|
||||
|
||||
### For Mac users
|
||||
1. Open a terminal and install brew with: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
|
||||
2. Then install git with: `brew install git`
|
||||
3. Download Sillytavern Launcher with: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
|
||||
4. Navigate to the SillyTavern-Launcher with: `cd SillyTavern-Launcher`
|
||||
5. Start the install launcher with: `chmod +x install.sh && ./install.sh` and choose what you wanna install
|
||||
6. After installation start the launcher with: `chmod +x launcher.sh && ./launcher.sh`
|
||||
1. Open a terminal and install brew with:
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
2. Install git with:
|
||||
```shell
|
||||
brew install git
|
||||
```
|
||||
3. Git clone the Sillytavern-Launcher with:
|
||||
```shell
|
||||
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
|
||||
```
|
||||
4. Start the installer.sh with:
|
||||
```shell
|
||||
chmod +x install.sh && ./install.sh
|
||||
```
|
||||
5. After installation start the launcher.sh with:
|
||||
```shell
|
||||
chmod +x launcher.sh && ./launcher.sh
|
||||
```
|
||||
|
||||
## 🐋 Installing via Docker
|
||||
|
||||
@ -244,7 +268,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
|
||||
|
||||
## API keys management
|
||||
|
||||
SillyTavern saves your API keys to a `secrets.json` file in the server directory.
|
||||
SillyTavern saves your API keys to a `secrets.json` file in the user data directory (`/data/default-user/secrets.json` is the default path).
|
||||
|
||||
By default, they will not be exposed to a frontend after you enter them and reload the page.
|
||||
|
||||
@ -259,7 +283,7 @@ Most often this is for people who want to use SillyTavern on their mobile phones
|
||||
|
||||
However, it can be used to allow remote connections from anywhere as well.
|
||||
|
||||
**IMPORTANT: SillyTavern is a single-user program, so anyone who logs in will be able to see all characters and chats, and be able to change any settings inside the UI.**
|
||||
**IMPORTANT: Refer to the official guide if you want to configure SillyTavern user accounts with (optional) password protection: [Users](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users).**
|
||||
|
||||
### 1. Managing whitelisted IPs
|
||||
|
||||
|
@ -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"
|
||||
}
|
7
package-lock.json
generated
7
package-lock.json
generated
@ -48,7 +48,7 @@
|
||||
"vectra": "^0.2.2",
|
||||
"wavefile": "^11.0.0",
|
||||
"write-file-atomic": "^5.0.1",
|
||||
"ws": "^8.13.0",
|
||||
"ws": "^8.17.1",
|
||||
"yaml": "^2.3.4",
|
||||
"yargs": "^17.7.1",
|
||||
"yauzl": "^2.10.0"
|
||||
@ -4659,8 +4659,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.13.0",
|
||||
"license": "MIT",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
@ -38,7 +38,7 @@
|
||||
"vectra": "^0.2.2",
|
||||
"wavefile": "^11.0.0",
|
||||
"write-file-atomic": "^5.0.1",
|
||||
"ws": "^8.13.0",
|
||||
"ws": "^8.17.1",
|
||||
"yaml": "^2.3.4",
|
||||
"yargs": "^17.7.1",
|
||||
"yauzl": "^2.10.0"
|
||||
|
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);
|
||||
}
|
||||
|
@ -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 */
|
||||
|
@ -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">
|
||||
@ -2552,6 +2551,7 @@
|
||||
<h4 data-i18n="Claude Model">Claude Model</h4>
|
||||
<select id="model_claude_select">
|
||||
<optgroup label="Versions">
|
||||
<option value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
|
||||
<option value="claude-3-opus-20240229">claude-3-opus-20240229</option>
|
||||
<option value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
|
||||
<option value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
|
||||
@ -4476,14 +4476,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 +4491,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 +4842,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>
|
||||
</dialog>
|
||||
</template>
|
||||
<div id="shadow_popup">
|
||||
<div id="dialogue_popup">
|
||||
@ -4866,15 +4864,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 +5191,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 +5615,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>
|
||||
@ -6251,7 +6249,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 +6265,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 +6430,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)",
|
||||
|
@ -962,10 +962,11 @@
|
||||
"onboarding_import": "Импортируйте",
|
||||
"from supported sources or view": "из источника или посмотрите",
|
||||
"Sample characters": "Стандартных персонажей",
|
||||
"popup_text_save": "Сохранить",
|
||||
"popup_text_yes": "Да",
|
||||
"popup_text_no": "Нет",
|
||||
"popup_text_cancel": "Отмена",
|
||||
"popup-button-save": "Сохранить",
|
||||
"popup-button-yes": "Да",
|
||||
"popup-button-no": "Нет",
|
||||
"popup-button-cancel": "Отмена",
|
||||
"popup-button-import": "Импортировать",
|
||||
"Enter the URL of the content to import": "Введите URL-адрес импортируемого контента",
|
||||
"Supported sources:": "Поддерживаются следующие источники:",
|
||||
"char_import_example": "Пример:",
|
||||
@ -976,7 +977,6 @@
|
||||
"char_import_5": "Персонаж с AICharacterCard.com (прямая ссылка или ID)",
|
||||
"char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
|
||||
"char_import_7": ")",
|
||||
"popup_text_import": "Импортировать",
|
||||
"Grammar String": "Грамматика",
|
||||
"GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF или ENBF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
|
||||
"Account": "Аккаунт",
|
||||
|
@ -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)",
|
||||
|
@ -899,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)": "(用于聊天补全和指导模式)",
|
||||
|
@ -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)": "(用於聊天補充和指令模式)",
|
||||
|
101
public/script.js
101
public/script.js
@ -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';
|
||||
import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
|
||||
import { enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
|
||||
@ -265,6 +268,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) {
|
||||
@ -511,11 +527,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.
|
||||
@ -523,7 +543,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
|
||||
@ -887,6 +907,8 @@ async function firstLoadInit() {
|
||||
await getSystemMessages();
|
||||
sendSystemMessage(system_message_types.WELCOME);
|
||||
await getSettings();
|
||||
initKeyboard();
|
||||
initDynamicStyles();
|
||||
initTags();
|
||||
await getUserAvatars(true, user_avatar);
|
||||
await getCharacters();
|
||||
@ -927,6 +949,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) {
|
||||
@ -3421,7 +3445,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
* @returns {string[]} Examples array with block heading
|
||||
*/
|
||||
function parseMesExamples(examplesStr) {
|
||||
if (examplesStr.length === 0) {
|
||||
if (examplesStr.length === 0 || examplesStr === '<START>') {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -5488,7 +5512,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');
|
||||
@ -5736,7 +5760,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')), {
|
||||
@ -5780,7 +5804,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();
|
||||
}
|
||||
@ -5815,8 +5839,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');
|
||||
}
|
||||
@ -6853,7 +6877,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 || '');
|
||||
@ -6930,7 +6954,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);
|
||||
@ -7076,10 +7100,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';
|
||||
@ -7109,6 +7133,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);
|
||||
@ -7221,7 +7246,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;
|
||||
@ -7524,10 +7549,15 @@ function addAlternateGreeting(template, greeting, index, getArray) {
|
||||
template.find('.alternate_greetings_list').append(greetingBlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or edits a character based on the form data.
|
||||
* @param {Event} [e] Event that triggered the function call.
|
||||
*/
|
||||
async function createOrEditCharacter(e) {
|
||||
$('#rm_info_avatar').html('');
|
||||
const formData = new FormData($('#form_create').get(0));
|
||||
formData.set('fav', String(fav_ch_checked));
|
||||
const isNewChat = e instanceof CustomEvent && e.type === 'newChat';
|
||||
|
||||
const rawFile = formData.get('avatar');
|
||||
if (rawFile instanceof File) {
|
||||
@ -7598,7 +7628,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 = '';
|
||||
|
||||
@ -7670,6 +7700,7 @@ async function createOrEditCharacter(e) {
|
||||
// Recreate the chat if it hasn't been used at least once (i.e. with continue).
|
||||
const message = getFirstMessage();
|
||||
const shouldRegenerateMessage =
|
||||
!isNewChat &&
|
||||
message.mes &&
|
||||
!selected_group &&
|
||||
!chat_metadata['tainted'] &&
|
||||
@ -8958,7 +8989,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();
|
||||
}
|
||||
@ -9205,7 +9236,7 @@ jQuery(async function () {
|
||||
characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`;
|
||||
$('#selected_chat_pole').val(characters[this_chid].chat);
|
||||
await getChat();
|
||||
await createOrEditCharacter();
|
||||
await createOrEditCharacter(new CustomEvent('newChat'));
|
||||
if (isDelChatCheckbox) await delChat(chat_file_for_del + '.jsonl');
|
||||
}
|
||||
}
|
||||
@ -9467,7 +9498,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 () {
|
||||
@ -10367,7 +10398,7 @@ jQuery(async function () {
|
||||
'#character_cross',
|
||||
'#avatar-and-name-block',
|
||||
'#shadow_popup',
|
||||
'.shadow_popup',
|
||||
'.popup',
|
||||
'#world_popup',
|
||||
'.ui-widget',
|
||||
'.text_pole',
|
||||
@ -10672,7 +10703,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');
|
||||
@ -10727,32 +10758,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();
|
||||
|
@ -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,25 +992,7 @@ 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');
|
||||
});
|
||||
|
||||
$(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);
|
||||
const dragDropHandler = new DragAndDropHandler('.popup', async (files, event) => {
|
||||
let selectedTarget = ATTACHMENT_SOURCE.GLOBAL;
|
||||
const targets = getAvailableTargets();
|
||||
|
||||
@ -1027,13 +1010,6 @@ async function openAttachmentManager() {
|
||||
}
|
||||
renderAttachments();
|
||||
});
|
||||
}
|
||||
|
||||
function removeDragAndDrop() {
|
||||
$(document.body).off('dragover', '.shadow_popup');
|
||||
$(document.body).off('dragleave', '.shadow_popup');
|
||||
$(document.body).off('drop', '.shadow_popup');
|
||||
}
|
||||
|
||||
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
|
||||
let 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');
|
||||
|
@ -34,6 +34,7 @@
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
|
||||
<option data-type="openai" value="gpt-4o">gpt-4o</option>
|
||||
<option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
|
||||
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
|
||||
@ -44,9 +45,11 @@
|
||||
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3.5-sonnet">anthropic/claude-3.5-sonnet</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3.5-sonnet:beta">anthropic/claude-3.5-sonnet:beta</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-haiku:beta">anthropic/claude-3-haiku:beta</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
|
||||
|
@ -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';
|
||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
|
||||
const extensionName = 'gallery';
|
||||
@ -57,7 +58,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,
|
||||
@ -81,44 +83,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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -557,7 +557,7 @@ export class QuickReply {
|
||||
this.editorExecuteErrors.innerHTML = '';
|
||||
this.editorExecuteResult.innerHTML = '';
|
||||
if (this.editorExecuteHide.checked) {
|
||||
this.editorPopup.dom.classList.add('qr--hide');
|
||||
this.editorPopup.dlg.classList.add('qr--hide');
|
||||
}
|
||||
try {
|
||||
this.editorExecutePromise = this.execute({}, true);
|
||||
@ -588,7 +588,7 @@ export class QuickReply {
|
||||
}
|
||||
this.editorExecutePromise = null;
|
||||
this.editorExecuteBtn.classList.remove('qr--busy');
|
||||
this.editorPopup.dom.classList.remove('qr--hide');
|
||||
this.editorPopup.dlg.classList.remove('qr--hide');
|
||||
}
|
||||
|
||||
updateEditorProgress(done, total) {
|
||||
|
@ -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,10 @@
|
||||
background-color: var(--progFlashColor);
|
||||
}
|
||||
}
|
||||
.shadow_popup.qr--hide {
|
||||
|
||||
.popup.qr--hide {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.popup.qr--hide::backdrop {
|
||||
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;
|
||||
}
|
||||
|
@ -440,7 +440,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);
|
||||
@ -713,7 +713,7 @@ function onChatChanged() {
|
||||
adjustElementScrollHeight();
|
||||
}
|
||||
|
||||
function adjustElementScrollHeight(){
|
||||
function adjustElementScrollHeight() {
|
||||
if (!$('.sd_settings').is(':visible')) {
|
||||
return;
|
||||
}
|
||||
@ -823,7 +823,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();
|
||||
}
|
||||
|
||||
@ -834,13 +834,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();
|
||||
}
|
||||
|
||||
@ -903,13 +903,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();
|
||||
}
|
||||
|
||||
@ -949,7 +959,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();
|
||||
}
|
||||
|
||||
@ -1051,19 +1061,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();
|
||||
}
|
||||
|
||||
@ -3447,6 +3457,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,11 +128,14 @@
|
||||
<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">
|
||||
<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>
|
||||
@ -140,6 +143,7 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-sd-source="comfy">
|
||||
<label for="sd_comfy_url">ComfyUI URL</label>
|
||||
<div class="flex-container flexnowrap">
|
||||
@ -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>
|
||||
<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>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="flex1">
|
||||
<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>
|
||||
<div data-sd-source="comfy,auto">
|
||||
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
|
||||
<select id="sd_scheduler"></select>
|
||||
</div>
|
||||
<div data-sd-source="comfy">
|
||||
|
||||
<div class="flex1" data-sd-source="comfy">
|
||||
<label for="sd_vae">VAE</label>
|
||||
<select id="sd_vae"></select>
|
||||
</div>
|
||||
<div class="flex-container marginTopBot5">
|
||||
</div>
|
||||
|
||||
<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 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 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 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%);
|
||||
}
|
||||
|
@ -402,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);
|
||||
|
@ -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,6 +735,11 @@ 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' &&
|
||||
@ -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();
|
||||
return '';
|
||||
|
@ -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';
|
||||
|
||||
@ -1367,7 +1367,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);
|
||||
}
|
@ -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) {
|
||||
// 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();
|
||||
this.completeCancelled();
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const keyListenerBound = keyListener.bind(this);
|
||||
window.addEventListener('keydown', keyListenerBound);
|
||||
}
|
||||
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);
|
||||
|
||||
async show() {
|
||||
document.body.append(this.dom);
|
||||
this.dom.style.display = 'block';
|
||||
switch (this.type) {
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.input.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$(this.dom).transition({
|
||||
opacity: 1,
|
||||
duration: animation_duration,
|
||||
easing: animation_easing,
|
||||
});
|
||||
};
|
||||
const keyListenerBound = keyListener.bind(this);
|
||||
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.dlg);
|
||||
|
||||
// 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() {
|
||||
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.TEXT:
|
||||
case POPUP_TYPE.CONFIRM: {
|
||||
this.value = true;
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.value = this.input.value;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -2263,22 +2263,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);
|
||||
|
||||
@ -2287,7 +2288,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,
|
||||
@ -2325,33 +2365,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;
|
||||
}
|
||||
|
||||
@ -2652,7 +2679,7 @@ async function doDelMode(_, text) {
|
||||
|
||||
let oldestMesToDel = $('#chat').find(`.mes[mesid=${oldestMesIDToDel}]`);
|
||||
|
||||
if (!oldestMesIDToDel) {
|
||||
if (!oldestMesIDToDel && lastMesID > 0) {
|
||||
oldestMesToDel = await loadUntilMesId(oldestMesIDToDel);
|
||||
|
||||
if (!oldestMesToDel || !oldestMesToDel.length) {
|
||||
|
@ -185,7 +185,7 @@ class WebScraper {
|
||||
const files = [];
|
||||
|
||||
for (const link of links) {
|
||||
const result = await fetch('/api/serpapi/visit', {
|
||||
const result = await fetch('/api/search/visit', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ url: link }),
|
||||
@ -547,7 +547,7 @@ class YouTubeScraper {
|
||||
async getScript(videoUrl, lang) {
|
||||
const id = this.parseId(String(videoUrl).trim());
|
||||
|
||||
const result = await fetch('/api/serpapi/transcript', {
|
||||
const result = await fetch('/api/search/transcript', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ id, lang }),
|
||||
|
@ -62,6 +62,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
||||
import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
|
||||
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
|
||||
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
export {
|
||||
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
|
||||
@ -1465,6 +1466,7 @@ async function trimTokensCallback(arg, value) {
|
||||
|
||||
async function buttonsCallback(args, text) {
|
||||
try {
|
||||
/** @type {string[]} */
|
||||
const buttons = JSON.parse(resolveVariable(args?.labels));
|
||||
|
||||
if (!Array.isArray(buttons) || !buttons.length) {
|
||||
@ -1472,18 +1474,24 @@ async function buttonsCallback(args, text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel
|
||||
const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button]));
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
const safeValue = DOMPurify.sanitize(text || '');
|
||||
|
||||
/** @type {Popup} */
|
||||
let popup;
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p', 'm-t-1');
|
||||
|
||||
for (const button of buttons) {
|
||||
for (const [result, button] of resultToButtonMap) {
|
||||
const buttonElement = document.createElement('div');
|
||||
buttonElement.classList.add('menu_button', 'wide100p');
|
||||
buttonElement.classList.add('menu_button', 'result-control', 'wide100p');
|
||||
buttonElement.dataset.result = String(result);
|
||||
buttonElement.addEventListener('click', () => {
|
||||
resolve(button);
|
||||
$('#dialogue_popup_ok').trigger('click');
|
||||
popup?.complete(result);
|
||||
});
|
||||
buttonElement.innerText = button;
|
||||
buttonContainer.appendChild(buttonElement);
|
||||
@ -1492,8 +1500,10 @@ async function buttonsCallback(args, text) {
|
||||
const popupContainer = document.createElement('div');
|
||||
popupContainer.innerHTML = safeValue;
|
||||
popupContainer.appendChild(buttonContainer);
|
||||
callPopup(popupContainer, 'text', '', { okButton: 'Cancel' })
|
||||
.then(() => resolve(''))
|
||||
|
||||
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel' });
|
||||
popup.show()
|
||||
.then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '')))
|
||||
.catch(() => resolve(''));
|
||||
});
|
||||
} catch {
|
||||
@ -1509,7 +1519,7 @@ async function popupCallback(args, value) {
|
||||
okButton: args?.okButton !== undefined && typeof args?.okButton === 'string' ? args.okButton : 'Ok',
|
||||
};
|
||||
await delay(1);
|
||||
await callPopup(safeValue, 'text', '', popupOptions);
|
||||
await callGenericPopup(safeValue, POPUP_TYPE.TEXT, '', popupOptions);
|
||||
await delay(1);
|
||||
return value;
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import {
|
||||
this_chid,
|
||||
callPopup,
|
||||
menu_type,
|
||||
getCharacters,
|
||||
entitiesFilter,
|
||||
printCharactersDebounced,
|
||||
buildAvatarList,
|
||||
eventSource,
|
||||
event_types,
|
||||
DEFAULT_PRINT_TIMEOUT,
|
||||
substituteParams,
|
||||
printCharacters,
|
||||
} from '../script.js';
|
||||
@ -17,11 +17,15 @@ import {
|
||||
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';
|
||||
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
@ -49,6 +53,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');
|
||||
@ -332,7 +338,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;
|
||||
}
|
||||
|
||||
@ -470,7 +476,6 @@ export function getTagKeyForEntityElement(element) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the key for char/group by searching based on the name or avatar. If none can be found, a toastr will be shown and null returned.
|
||||
* This function is mostly used in slash commands.
|
||||
@ -493,29 +498,34 @@ export function searchCharByName(charName, { suppressLogging = false } = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@ -613,10 +623,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);
|
||||
@ -637,7 +647,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) {
|
||||
@ -651,7 +661,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;
|
||||
@ -660,70 +670,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) {
|
||||
let globalTagImportSetting = tagImportSettings.ASK; // Default setting
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const tagsToImport = tagNamesToImport.map(tag => getTag(tag, { createNew: true }));
|
||||
const added = addTagsToEntity(tagsToImport, character.avatar);
|
||||
|
||||
toastr.success(`Imported tags:<br />${tagsToImport.map(x => x.name).join(', ')}`, 'Importing Tags', { escapeHtml: 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Await the character list, which will automatically reprint it and all tag filters
|
||||
await getCharacters();
|
||||
|
||||
// need to return false to keep the input clear
|
||||
return false;
|
||||
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,
|
||||
@ -733,16 +873,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.
|
||||
*/
|
||||
@ -750,7 +888,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.
|
||||
@ -774,8 +912,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
|
||||
@ -859,7 +998,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;
|
||||
}
|
||||
@ -877,6 +1016,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);
|
||||
@ -892,19 +1038,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);
|
||||
@ -913,6 +1060,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));
|
||||
|
||||
@ -923,6 +1071,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1000,7 +1151,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');
|
||||
@ -1035,6 +1186,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);
|
||||
@ -1054,7 +1211,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);
|
||||
}
|
||||
@ -1072,7 +1229,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();
|
||||
@ -1081,11 +1238,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();
|
||||
@ -1094,7 +1251,7 @@ export function applyTagsOnGroupSelect() {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = selected_group;
|
||||
groupId = groupId ?? Number(selected_group);
|
||||
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
@ -1116,9 +1273,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(`
|
||||
@ -1159,13 +1314,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 });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1227,7 +1379,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.');
|
||||
}
|
||||
|
||||
@ -1354,7 +1506,7 @@ async function onTagRestoreFileSelect(e) {
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
onViewTagsListClick();
|
||||
await onViewTagsListClick();
|
||||
}
|
||||
|
||||
function onBackupRestoreClick() {
|
||||
@ -1376,14 +1528,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) {
|
||||
@ -1407,25 +1563,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() {
|
||||
@ -1463,20 +1641,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)) {
|
||||
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();
|
||||
}
|
||||
@ -1486,35 +1700,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');
|
||||
@ -1554,9 +1774,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);
|
||||
@ -1578,7 +1796,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);
|
||||
}
|
||||
@ -1598,7 +1816,7 @@ function registerTagsSlashCommands() {
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName, { allowCreate: true });
|
||||
if (!tag) return 'false';
|
||||
const result = addTagToEntity(tag, key);
|
||||
const result = addTagsToEntity(tag, key);
|
||||
printCharacters();
|
||||
return String(result);
|
||||
},
|
||||
@ -1778,22 +1996,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}"]`));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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)}`;
|
||||
}
|
||||
|
@ -2755,7 +2755,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?
|
||||
@ -2782,7 +2781,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;
|
||||
}
|
||||
|
||||
|
285
public/style.css
285
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;
|
||||
@ -809,7 +839,7 @@ body .panelControlBar {
|
||||
|
||||
#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,
|
||||
@ -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;
|
||||
@ -1186,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 {
|
||||
@ -2036,9 +2065,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;
|
||||
@ -2155,12 +2188,6 @@ h3 {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
@ -2181,12 +2208,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;
|
||||
@ -2202,13 +2223,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,
|
||||
@ -2217,6 +2239,7 @@ input[type="file"] {
|
||||
#WI_button_panel_pin_div {
|
||||
font-size: 24px;
|
||||
display: inline;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
#rm_button_panel_pin_div,
|
||||
@ -2227,8 +2250,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;
|
||||
}
|
||||
|
||||
@ -2276,6 +2302,7 @@ input[type="file"] {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
opacity: 0.5;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
#rm_button_selected_ch:hover {
|
||||
@ -2303,6 +2330,7 @@ input[type="file"] {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.bulk_select_checkbox {
|
||||
@ -2340,12 +2368,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%;
|
||||
}
|
||||
|
||||
@ -2755,19 +2784,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;
|
||||
}
|
||||
|
||||
@ -2792,24 +2825,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;
|
||||
}
|
||||
|
||||
@ -2818,15 +2850,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,
|
||||
@ -2855,6 +2887,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.avatar_div {
|
||||
@ -2944,6 +2977,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;
|
||||
@ -3043,7 +3077,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 {
|
||||
@ -3053,15 +3088,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;
|
||||
@ -3109,17 +3143,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%;
|
||||
@ -3127,15 +3160,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;
|
||||
@ -3143,16 +3175,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);
|
||||
}
|
||||
|
||||
@ -3160,15 +3190,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;
|
||||
}
|
||||
|
||||
@ -3223,13 +3251,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);
|
||||
@ -3241,15 +3267,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 {
|
||||
@ -3394,7 +3411,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;
|
||||
@ -3412,13 +3429,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;
|
||||
@ -3429,16 +3443,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;
|
||||
@ -3450,6 +3464,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 {
|
||||
@ -3567,6 +3582,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;
|
||||
@ -3766,35 +3791,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;
|
||||
}
|
||||
|
||||
@ -4101,17 +4120,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;
|
||||
@ -4265,24 +4273,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;
|
||||
@ -4596,6 +4586,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 {
|
||||
@ -5185,5 +5176,3 @@ body:not(.movingUI) .drawer-content.maximized {
|
||||
.regex-highlight {
|
||||
color: #FAF8F6;
|
||||
}
|
||||
|
||||
/* Pastel White */
|
||||
|
@ -404,6 +404,11 @@ redirect('/api/content/import', '/api/content/importURL');
|
||||
// Redirect deprecated moving UI endpoints
|
||||
redirect('/savemovingui', '/api/moving-ui/save');
|
||||
|
||||
// Redirect Serp endpoints
|
||||
redirect('/api/serpapi/search', '/api/search/serpapi');
|
||||
redirect('/api/serpapi/visit', '/api/search/visit');
|
||||
redirect('/api/serpapi/transcript', '/api/search/transcript');
|
||||
|
||||
// Moving UI
|
||||
app.use('/api/moving-ui', require('./src/endpoints/moving-ui').router);
|
||||
|
||||
@ -499,8 +504,8 @@ app.use('/api/extra/classify', require('./src/endpoints/classify').router);
|
||||
// Image captioning
|
||||
app.use('/api/extra/caption', require('./src/endpoints/caption').router);
|
||||
|
||||
// Web search extension
|
||||
app.use('/api/serpapi', require('./src/endpoints/serpapi').router);
|
||||
// Web search and scraping
|
||||
app.use('/api/search', require('./src/endpoints/search').router);
|
||||
|
||||
// The different text generation APIs
|
||||
|
||||
|
@ -420,7 +420,7 @@ async function downloadJannyCharacter(uuid) {
|
||||
// This endpoint is being guarded behind Bot Fight Mode of Cloudflare
|
||||
// So hosted ST on Azure/AWS/GCP/Collab might get blocked by IP
|
||||
// Should work normally on self-host PC/Android
|
||||
const result = await fetch('https://api.janitorai.me/api/v1/download', {
|
||||
const result = await fetch('https://api.jannyai.com/api/v1/download', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
@ -22,7 +22,7 @@ const visitHeaders = {
|
||||
'Sec-Fetch-User': '?1',
|
||||
};
|
||||
|
||||
router.post('/search', jsonParser, async (request, response) => {
|
||||
router.post('/serpapi', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
|
||||
|
||||
@ -134,6 +134,41 @@ router.post('/transcript', jsonParser, async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/searxng', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const { baseUrl, query } = request.body;
|
||||
|
||||
if (!baseUrl || !query) {
|
||||
console.log('Missing required parameters for /searxng');
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
const params = new URLSearchParams();
|
||||
params.append('q', query);
|
||||
params.append('format', 'html');
|
||||
url.pathname = '/search';
|
||||
url.search = params.toString();
|
||||
|
||||
const result = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: visitHeaders,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const text = await result.text();
|
||||
console.log('SearXNG request failed', result.statusText, text);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
const data = await result.text();
|
||||
return response.send(data);
|
||||
} catch (error) {
|
||||
console.log('SearXNG request failed', error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/visit', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const url = request.body.url;
|
Loading…
x
Reference in New Issue
Block a user