Merge pull request #2705 from SillyTavern/staging Staging

2428c3979fe6dee360426b72fe6aa88e46091dd8

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
134 files changed, +6095 -856Ignore whitespace
.eslintrc.js+1 -0
@@ -55,6 +55,7 @@ module.exports = {
55 isProbablyReaderable: 'readonly',55 isProbablyReaderable: 'readonly',
56 ePub: 'readonly',56 ePub: 'readonly',
57 diff_match_patch: 'readonly',57 diff_match_patch: 'readonly',
58 SillyTavern: 'readonly',
58 },59 },
59 },60 },
60 ],61 ],
default/config.yaml+39 -5
@@ -4,8 +4,22 @@ dataRoot: ./data
4# -- SERVER CONFIGURATION --4# -- SERVER CONFIGURATION --
5# Listen for incoming connections5# Listen for incoming connections
6listen: false6listen: false
7# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
8protocol:
9 ipv4: true
10 ipv6: false
11# Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6
12dnsPreferIPv6: false
13# The hostname that autorun opens.
14# - Use "auto" to let the server decide
15# - Use options like 'localhost', 'st.example.com'
16autorunHostname: "auto"
7# Server port17# Server port
8port: 800018port: 8000
19# Overrides the port for autorun in browser.
20# - Use -1 to use the server port.
21# - Specify a port to override the default.
22autorunPortOverride: -1
9# -- SECURITY CONFIGURATION --23# -- SECURITY CONFIGURATION --
10# Toggle whitelist mode24# Toggle whitelist mode
11whitelistMode: true25whitelistMode: true
@@ -13,6 +27,7 @@ whitelistMode: true
13enableForwardedWhitelist: true27enableForwardedWhitelist: true
14# Whitelist of allowed IP addresses28# Whitelist of allowed IP addresses
15whitelist:29whitelist:
30 - ::1
16 - 127.0.0.131 - 127.0.0.1
17# Toggle basic authentication for endpoints32# Toggle basic authentication for endpoints
18basicAuthMode: false33basicAuthMode: false
@@ -26,6 +41,11 @@ enableCorsProxy: false
26enableUserAccounts: false41enableUserAccounts: false
27# Enable discreet login mode: hides user list on the login screen42# Enable discreet login mode: hides user list on the login screen
28enableDiscreetLogin: false43enableDiscreetLogin: false
44# User session timeout *in seconds* (defaults to 24 hours).
45## Set to a positive number to expire session after a certain time of inactivity
46## Set to 0 to expire session when the browser is closed
47## Set to a negative number to disable session expiration
48sessionTimeout: 86400
29# Used to sign session cookies. Will be auto-generated if not set49# Used to sign session cookies. Will be auto-generated if not set
30cookieSecret: ''50cookieSecret: ''
31# Disable CSRF protection - NOT RECOMMENDED51# Disable CSRF protection - NOT RECOMMENDED
@@ -35,6 +55,9 @@ securityOverride: false
35# -- ADVANCED CONFIGURATION --55# -- ADVANCED CONFIGURATION --
36# Open the browser automatically56# Open the browser automatically
37autorun: true57autorun: true
58# Avoids using 'localhost' for autorun in auto mode.
59# use if you don't have 'localhost' in your hosts file
60avoidLocalhost: false
38# Disable thumbnail generation61# Disable thumbnail generation
39disableThumbnails: false62disableThumbnails: false
40# Thumbnail quality (0-100)63# Thumbnail quality (0-100)
@@ -98,10 +121,21 @@ mistral:
98 # Enables prefilling of the reply with the last assistant message in the prompt121 # Enables prefilling of the reply with the last assistant message in the prompt
99 # CAUTION: The prefix is echoed into the completion. You may want to use regex to trim it out.122 # CAUTION: The prefix is echoed into the completion. You may want to use regex to trim it out.
100 enablePrefix: false123 enablePrefix: false
124# -- OLLAMA API CONFIGURATION --
125ollama:
126 # Controls how long the model will stay loaded into memory following the request
127 # * -1: Keep the model loaded indefinitely
128 # * 0: Unload the model immediately after the request
129 # * N (any positive number): Keep the model loaded for N seconds after the request.
130 keepAlive: -1
131# -- ANTHROPIC CLAUDE API CONFIGURATION --
132claude:
133 # Enables caching of the system prompt (if supported).
134 # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
135 # -- IMPORTANT! --
136 # Use only when the prompt before the chat history is static and doesn't change between requests
137 # (e.g {{random}} macro or lorebooks not as in-chat injections).
138 # Otherwise, you'll just waste money on cache misses.
139 enableSystemPromptCache: false
101# -- SERVER PLUGIN CONFIGURATION --140# -- SERVER PLUGIN CONFIGURATION --
102enableServerPlugins: false141enableServerPlugins: false
103# User session timeout *in seconds* (defaults to 24 hours).
104## Set to a positive number to expire session after a certain time of inactivity
105## Set to 0 to expire session when the browser is closed
106## Set to a negative number to disable session expiration
107sessionTimeout: 86400
default/content/presets/openai/Default.json+4 -4
@@ -1,7 +1,7 @@
1{1{
2 "chat_completion_source": "openai",2 "chat_completion_source": "openai",
3 "openai_model": "gpt-3.5-turbo",3 "openai_model": "gpt-4-turbo",
4 "claude_model": "claude-instant-v1",4 "claude_model": "claude-3-5-sonnet-20240620",
5 "windowai_model": "",5 "windowai_model": "",
6 "openrouter_model": "OR_Website",6 "openrouter_model": "OR_Website",
7 "openrouter_use_fallback": false,7 "openrouter_use_fallback": false,
@@ -9,7 +9,7 @@
9 "openrouter_group_models": false,9 "openrouter_group_models": false,
10 "openrouter_sort_models": "alphabetically",10 "openrouter_sort_models": "alphabetically",
11 "ai21_model": "j2-ultra",11 "ai21_model": "j2-ultra",
12 "mistralai_model": "mistral-medium-latest",12 "mistralai_model": "mistral-large-latest",
13 "custom_model": "",13 "custom_model": "",
14 "custom_url": "",14 "custom_url": "",
15 "custom_include_body": "",15 "custom_include_body": "",
@@ -22,7 +22,7 @@
22 "count_penalty": 0,22 "count_penalty": 0,
23 "top_p": 1,23 "top_p": 1,
24 "top_k": 0,24 "top_k": 0,
25 "top_a": 1,25 "top_a": 0,
26 "min_p": 0,26 "min_p": 0,
27 "repetition_penalty": 1,27 "repetition_penalty": 1,
28 "openai_max_context": 4095,28 "openai_max_context": 4095,
default/content/settings.json+3 -3
@@ -610,9 +610,9 @@
610 }610 }
611 ]611 ]
612 },612 },
613 "wi_format": "[Details of the fictional world the RP is set in:\n{0}]\n",613 "wi_format": "{0}",
614 "openai_model": "gpt-3.5-turbo",614 "openai_model": "gpt-4-turbo",
615 "claude_model": "claude-instant-v1",615 "claude_model": "claude-3-5-sonnet-20240620",
616 "ai21_model": "j2-ultra",616 "ai21_model": "j2-ultra",
617 "windowai_model": "",617 "windowai_model": "",
618 "openrouter_model": "OR_Website",618 "openrouter_model": "OR_Website",
index.d.ts+5 -0
@@ -9,6 +9,11 @@ declare global {
9 };9 };
10 }10 }
11 }11 }
12
13 /**
14 * The root directory for user data.
15 */
16 var DATA_ROOT: string;
12}17}
1318
14declare module 'express-session' {19declare module 'express-session' {
package-lock.json+54 -27
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.12.4",3 "version": "1.12.5",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "sillytavern",8 "name": "sillytavern",
9 "version": "1.12.4",9 "version": "1.12.5",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -27,6 +27,7 @@
27 "google-translate-api-browser": "^3.0.1",27 "google-translate-api-browser": "^3.0.1",
28 "he": "^1.2.0",28 "he": "^1.2.0",
29 "helmet": "^7.1.0",29 "helmet": "^7.1.0",
30 "iconv-lite": "^0.6.3",
30 "ip-matching": "^2.1.2",31 "ip-matching": "^2.1.2",
31 "ipaddr.js": "^2.0.1",32 "ipaddr.js": "^2.0.1",
32 "jimp": "^0.22.10",33 "jimp": "^0.22.10",
@@ -42,7 +43,7 @@
42 "rate-limiter-flexible": "^5.0.0",43 "rate-limiter-flexible": "^5.0.0",
43 "response-time": "^2.3.2",44 "response-time": "^2.3.2",
44 "sanitize-filename": "^1.6.3",45 "sanitize-filename": "^1.6.3",
45 "sillytavern-transformers": "^2.14.6",46 "sillytavern-transformers": "2.14.6",
46 "simple-git": "^3.19.1",47 "simple-git": "^3.19.1",
47 "tiktoken": "^1.0.15",48 "tiktoken": "^1.0.15",
48 "vectra": "^0.2.2",49 "vectra": "^0.2.2",
@@ -58,7 +59,7 @@
58 },59 },
59 "devDependencies": {60 "devDependencies": {
60 "@types/jquery": "^3.5.29",61 "@types/jquery": "^3.5.29",
61 "eslint": "^8.55.0",62 "eslint": "^8.57.0",
62 "jquery": "^3.6.4"63 "jquery": "^3.6.4"
63 },64 },
64 "engines": {65 "engines": {
@@ -166,9 +167,9 @@
166 "license": "MIT"167 "license": "MIT"
167 },168 },
168 "node_modules/@eslint/js": {169 "node_modules/@eslint/js": {
169 "version": "8.55.0",170 "version": "8.57.0",
170 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",171 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
171 "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",172 "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
172 "dev": true,173 "dev": true,
173 "license": "MIT",174 "license": "MIT",
174 "engines": {175 "engines": {
@@ -185,14 +186,15 @@
185 }186 }
186 },187 },
187 "node_modules/@humanwhocodes/config-array": {188 "node_modules/@humanwhocodes/config-array": {
188 "version": "0.11.13",189 "version": "0.11.14",
189 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",190 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
190 "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",191 "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
192 "deprecated": "Use @eslint/config-array instead",
191 "dev": true,193 "dev": true,
192 "license": "Apache-2.0",194 "license": "Apache-2.0",
193 "dependencies": {195 "dependencies": {
194 "@humanwhocodes/object-schema": "^2.0.1",196 "@humanwhocodes/object-schema": "^2.0.2",
195 "debug": "^4.1.1",197 "debug": "^4.3.1",
196 "minimatch": "^3.0.5"198 "minimatch": "^3.0.5"
197 },199 },
198 "engines": {200 "engines": {
@@ -200,9 +202,9 @@
200 }202 }
201 },203 },
202 "node_modules/@humanwhocodes/config-array/node_modules/debug": {204 "node_modules/@humanwhocodes/config-array/node_modules/debug": {
203 "version": "4.3.4",205 "version": "4.3.6",
204 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",206 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
205 "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",207 "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
206 "dev": true,208 "dev": true,
207 "license": "MIT",209 "license": "MIT",
208 "dependencies": {210 "dependencies": {
@@ -239,9 +241,10 @@
239 }241 }
240 },242 },
241 "node_modules/@humanwhocodes/object-schema": {243 "node_modules/@humanwhocodes/object-schema": {
242 "version": "2.0.1",244 "version": "2.0.3",
243 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",245 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
244 "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",246 "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
247 "deprecated": "Use @eslint/object-schema instead",
245 "dev": true,248 "dev": true,
246 "license": "BSD-3-Clause"249 "license": "BSD-3-Clause"
247 },250 },
@@ -1490,6 +1493,18 @@
1490 "node": ">= 0.8"1493 "node": ">= 0.8"
1491 }1494 }
1492 },1495 },
1496 "node_modules/body-parser/node_modules/iconv-lite": {
1497 "version": "0.4.24",
1498 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1499 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1500 "license": "MIT",
1501 "dependencies": {
1502 "safer-buffer": ">= 2.1.2 < 3"
1503 },
1504 "engines": {
1505 "node": ">=0.10.0"
1506 }
1507 },
1493 "node_modules/boolbase": {1508 "node_modules/boolbase": {
1494 "version": "1.0.0",1509 "version": "1.0.0",
1495 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",1510 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -2455,17 +2470,17 @@
2455 }2470 }
2456 },2471 },
2457 "node_modules/eslint": {2472 "node_modules/eslint": {
2458 "version": "8.55.0",2473 "version": "8.57.0",
2459 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",2474 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
2460 "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",2475 "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
2461 "dev": true,2476 "dev": true,
2462 "license": "MIT",2477 "license": "MIT",
2463 "dependencies": {2478 "dependencies": {
2464 "@eslint-community/eslint-utils": "^4.2.0",2479 "@eslint-community/eslint-utils": "^4.2.0",
2465 "@eslint-community/regexpp": "^4.6.1",2480 "@eslint-community/regexpp": "^4.6.1",
2466 "@eslint/eslintrc": "^2.1.4",2481 "@eslint/eslintrc": "^2.1.4",
2467 "@eslint/js": "8.55.0",2482 "@eslint/js": "8.57.0",
2468 "@humanwhocodes/config-array": "^0.11.13",2483 "@humanwhocodes/config-array": "^0.11.14",
2469 "@humanwhocodes/module-importer": "^1.0.1",2484 "@humanwhocodes/module-importer": "^1.0.1",
2470 "@nodelib/fs.walk": "^1.2.8",2485 "@nodelib/fs.walk": "^1.2.8",
2471 "@ungap/structured-clone": "^1.2.0",2486 "@ungap/structured-clone": "^1.2.0",
@@ -3280,12 +3295,12 @@
3280 }3295 }
3281 },3296 },
3282 "node_modules/iconv-lite": {3297 "node_modules/iconv-lite": {
3283 "version": "0.4.24",3298 "version": "0.6.3",
3284 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",3299 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
3285 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",3300 "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
3286 "license": "MIT",3301 "license": "MIT",
3287 "dependencies": {3302 "dependencies": {
3288 "safer-buffer": ">= 2.1.2 < 3"3303 "safer-buffer": ">= 2.1.2 < 3.0.0"
3289 },3304 },
3290 "engines": {3305 "engines": {
3291 "node": ">=0.10.0"3306 "node": ">=0.10.0"
@@ -4616,6 +4631,18 @@
4616 "node": ">= 0.8"4631 "node": ">= 0.8"
4617 }4632 }
4618 },4633 },
4634 "node_modules/raw-body/node_modules/iconv-lite": {
4635 "version": "0.4.24",
4636 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
4637 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
4638 "license": "MIT",
4639 "dependencies": {
4640 "safer-buffer": ">= 2.1.2 < 3"
4641 },
4642 "engines": {
4643 "node": ">=0.10.0"
4644 }
4645 },
4619 "node_modules/readable-stream": {4646 "node_modules/readable-stream": {
4620 "version": "2.3.8",4647 "version": "2.3.8",
4621 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",4648 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
package.json+4 -3
@@ -17,6 +17,7 @@
17 "google-translate-api-browser": "^3.0.1",17 "google-translate-api-browser": "^3.0.1",
18 "he": "^1.2.0",18 "he": "^1.2.0",
19 "helmet": "^7.1.0",19 "helmet": "^7.1.0",
20 "iconv-lite": "^0.6.3",
20 "ip-matching": "^2.1.2",21 "ip-matching": "^2.1.2",
21 "ipaddr.js": "^2.0.1",22 "ipaddr.js": "^2.0.1",
22 "jimp": "^0.22.10",23 "jimp": "^0.22.10",
@@ -32,7 +33,7 @@
32 "rate-limiter-flexible": "^5.0.0",33 "rate-limiter-flexible": "^5.0.0",
33 "response-time": "^2.3.2",34 "response-time": "^2.3.2",
34 "sanitize-filename": "^1.6.3",35 "sanitize-filename": "^1.6.3",
35 "sillytavern-transformers": "^2.14.6",36 "sillytavern-transformers": "2.14.6",
36 "simple-git": "^3.19.1",37 "simple-git": "^3.19.1",
37 "tiktoken": "^1.0.15",38 "tiktoken": "^1.0.15",
38 "vectra": "^0.2.2",39 "vectra": "^0.2.2",
@@ -70,7 +71,7 @@
70 "type": "git",71 "type": "git",
71 "url": "https://github.com/SillyTavern/SillyTavern.git"72 "url": "https://github.com/SillyTavern/SillyTavern.git"
72 },73 },
73 "version": "1.12.4",74 "version": "1.12.5",
74 "scripts": {75 "scripts": {
75 "start": "node server.js",76 "start": "node server.js",
76 "start:no-csrf": "node server.js --disableCsrf",77 "start:no-csrf": "node server.js --disableCsrf",
@@ -90,7 +91,7 @@
90 "main": "server.js",91 "main": "server.js",
91 "devDependencies": {92 "devDependencies": {
92 "@types/jquery": "^3.5.29",93 "@types/jquery": "^3.5.29",
93 "eslint": "^8.55.0",94 "eslint": "^8.57.0",
94 "jquery": "^3.6.4"95 "jquery": "^3.6.4"
95 }96 }
96}97}
public/css/character-group-overlay.css+1 -1
@@ -99,6 +99,6 @@
99}99}
100100
101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
102 width: 100px;102 width: unset;
103 padding: 0.25em;103 padding: 0.25em;
104}104}
public/css/world-info.css+12 -0
@@ -120,6 +120,14 @@
120 flex-wrap: wrap;120 flex-wrap: wrap;
121}121}
122122
123.world_entry .inline-drawer-header {
124 cursor: initial;
125}
126
127.world_entry .killSwitch {
128 cursor: pointer;
129}
130
123.world_entry_form_control input[type=button] {131.world_entry_form_control input[type=button] {
124 cursor: pointer;132 cursor: pointer;
125}133}
@@ -173,6 +181,10 @@
173 width: 7em;181 width: 7em;
174}182}
175183
184.world_entry .killSwitch.fa-toggle-on {
185 color: var(--SmartThemeQuoteColor);
186}
187
176.wi-card-entry {188.wi-card-entry {
177 border: 1px solid;189 border: 1px solid;
178 border-color: var(--SmartThemeBorderColor);190 border-color: var(--SmartThemeBorderColor);
public/global.d.ts+5 -0
@@ -14,6 +14,11 @@ declare var isProbablyReaderable;
14declare var ePub;14declare var ePub;
15declare var ai;15declare var ai;
1616
17declare var SillyTavern: {
18 getContext(): any;
19 llm: any;
20};
21
17// Jquery plugins22// Jquery plugins
18interface JQuery {23interface JQuery {
19 nanogallery2(options?: any): JQuery;24 nanogallery2(options?: any): JQuery;
public/img/blockentropy.svg+3 -0
@@ -0,0 +1,3 @@
1<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236.38 282.41">
2 <path d="M126.55,0v54.44l-79.87,33.76v93.95l27.53-12.94,43.08,31.09.04-.05v.09l55.21-31.44.13-.08v-80.06l-55.34,24.92v80.2l-42.55-30.7h-.02s0-81.16,0-81.16l57.02-24.11V9.23l93.54,56.12v22.51l-24.34,11.53,1.84,90.56-88.45,51.47-.13.08v34.46L5.23,198.97v-65.56H0v66.92c0,.85.41,1.64,1.11,2.14l113.13,79.91v.05l.04-.02h0s0,0,0,0l121.97-73.54.13-.08v-126.13l-5.84,2.76v-22.94h-.3l.11-.18L126.55,0Z" />
3</svg>
public/img/step-into.svg+149 -0
@@ -0,0 +1,149 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-into.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.313708"
29 inkscape:cx="58.910834"
30 inkscape:cy="25.323262"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="g2714" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:none"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:inline"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:none"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-out.svg+149 -0
@@ -0,0 +1,149 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-out.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.313708"
29 inkscape:cx="58.910834"
30 inkscape:cy="25.323262"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="g2714" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:none"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:none"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:inline"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-over.svg+149 -0
@@ -0,0 +1,149 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-over.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.313708"
29 inkscape:cx="58.910834"
30 inkscape:cy="25.323262"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="g2714" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:inline"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:none"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:none"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-resume.svg+218 -0
@@ -0,0 +1,218 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-resume.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.315"
29 inkscape:cx="4.7282369"
30 inkscape:cy="24.259832"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="layer8" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /><image
91 width="460"
92 height="71"
93 preserveAspectRatio="none"
94 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcwAAABHCAYAAACK23cpAAAABHNCSVQICAgIfAhkiAAACuZJREFU
95eJzt3T9o3FgeB/Cv77aZBHInYZPD+JLF4iDxDl44ZNiAvVfNhC2vCEqxWQJpJn3MwlRXTeP0drNF
96NtfIKa5cYnVxkQMLDoztbU6GgBkwDiPOEHuOK/aK7FM0Gs3M01gzkp6+HzBknPFEkSV93z/9NLO6
97uvoLiIiIaKjfZL0BRERERcDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIi
98ksDAJCIiksDAJCIiksDAJCIiksDAJCIikvBZ1htAlIZmswlN0wAAvu+j1WplvEX5YVkWOp0OHMfJ
99elMoBYuLi8Gfj4+PM9yS8mFgEinMsiyYphm8ZmgSjY9DskSKioZlvV5HrVbLcIuoyEzTDEZxyoqB
100SaSgaFgKDE0aR6PRCI6pMocmA5NIMYPCUmBoUhKNRgOGYQD4eOyUOTQZmEQKGRWWAkOTZPm+3/O6
101zKHJwCRShGxYCgxNkmHbNlzX7fleWUNzqqtk5xaWMbuwHLx+d+Tg4vw0eH3txs2e19RPXBA1TQuW
102lHc6nb5WYFj4ouj7ft/BT8UXF5ae50HX9Z7bbQD0XOTq9ToArp6l4WzbBoC+RWQA4Lru0OuPSqYa
103mLMLy1i69yh4/f5kvycgv3nyI47evuwL0jKzLCuYPxjWmvM8D47jxIanOLDF+xiYahkUlltbW2g2
104mz3fb7VaPfesAgxNksPQzOGQ7NK9R/jLgw3MhXqiZWSaJjY2NoJhj1FDH4ZhBCvZyjZMUmbDwnKQ
105VqsV26ji8CyNUvbh2VwWLrh24ya+frCBs5N9uK+fl6q3aRgGarVa0Ksc5+ebzSZ2dnZK0+ors06n
1060/N6VFgKcT1NVY8Vy7Lgui48z8t6Uwpr1HVpUE9T0zQ8ffp0rDAVo2F5GhHLXQ8zbG5hGd88+RG3
107l2q4duNm1pszcaKXGHdQioPHtm3Yto2tra2hoViv15XsbY77f9I0DY1GQ7n94TgOdnZ2AMiHpRDu
108acb1HFTQaDRgmmbP1AYlI86dUfsvrqfp+z42NzfHaowZhgHLsnI18pHLHmaUef+Z8r1NcVBGibnJ
109uNZx+Hvi4AofrOJ7qhAnrWEYiU5CTdOCuTzLsmDbtlK9Kcdxxl7M1Wq1YJqmkmEZnf8Xv3v2NJMJ
110r4GQfW+4IS9Cc9yeZp7m2H9769atv03rH5tbWMbcH78MXkcX94QXBEVdv3ETf/rzXwEAF+en+N9/
111P0xuQ6csfEEXfN/HixcvgovhKL7vY3d3F7quY35+Pvi+rut97yvixdGyLFSrVQBApVJBtVrFwcEB
112ut0uAGBtbQ2VSgUA0O12sbu7C6B/34r9U8R9MEy73Y79/qD9IvOzRddut1GtVoP/f6VSgWEYaLfb
113V2owaZqGSqUSHHvTNu1h9MePH/e89jwPvu/3fIWvM4Zh4PLyEr7vB/uo2+3i4OAA8/PzfT876Cv6
114mb7vZ36s5raHeXF+ig/np32Lf5buPcLnX9Thvn6Os5P9jLYuXdFe4FWetmHbNjqdTqJWYRG4rgvD
115MIKLhZgbGdbTjGuIAPloqdLkxfVsxu1pip8LD0v6vo+9vb1SHU+Dhv3j6hYD/T3NJFMG0Tl2y7KC
116sM5KbucwP5yf4s2r9dhhWLEo6OsHG4Wf24ybSBfLt8cVntdShed5fUOpwxYUDArLra0tDsmVSNwc
117Wlz4DVOr1dBsNvver2ka6vV634W9jCaxejauMTzusG5acjske3F+indHDv5zdox//+sfuP67P+D3
118c70HrBimFUO0RRymffjwYTBkBCS/oNdqteDADH/put43HAsUd0gWQDAkYxhGzzBbtVrtOYkqlQrW
1191tb6fr6MYSkzJKs6MRw4zvCszDoAcQxOa99Oe0g2PFo17PpxeHjYNyUUNzwra9DvLTodM0257WFG
120ua+f482r9dhFP+b9ZzDvPytcbzN6f6Xv+4kv6GIRTNyXigb1NEcpY1jSJ+P2NOMW4sXRNC1RWUJV
121pd3THPZ7y6Knmds5zDhnJ/v46YfvcPerb/H5F/WegBS3oBSpUlD0BNvb28toS4pFhKbsScOwVEfc
122MHsS0eNl2Jxm0kanqquNk0q7IpCYK15ZWQl+f4ZhBPt7mnOahelhhv38z78PXPRTpEpBKysrwZ99
1233y/V4oGriutpxmFYqkWMyoz7Negz43qaSXswcVMgZZV2T9NxnL4Ohfi8aSpUDzPsQwF6kKOMuxI2
124LMmqM9WM6mkyLEnWoIVjV/2MMptETzNq2o2UQgbm3a++HXjP5rsjB+7r51PeIsrKoNBkWKrpqsNv
125g0It7ngRTwOSxeHYfmmFZtziK3HuT1OhAnNuYRl37z2KHW49O9nHz29fKnNv5iDiwPN9P6gjqlLV
126mnFEQ5Nhqa6rjMokvdVIPPlHtucYretLH101NEXJ0LCkZSDTUpjANO8/w+2l+JqC7uvneHek9vyf
127uH0kSnYuT3Xh1ibD8pNOp8OGFca7L9f3fdi2LbVSVpSwpHjjhmZcydCkBRDSlPvAvL1Ug3n/Wezf
128qV5fVhhW+Fg8nUTV4tlJMCj7lXmOW7hKEQvP87CzszO0chYX7MlJGppiMVaYaMRkJbeBef3Xaj5x
129w68X56dKlcYbRvZRX/V6PfOyUUR5k0bFJ8dx4LouS+OlIGloRhf1ZF08P7eBee3GzdhCBGVa1CNK
130b8m+1zRNnrxEv0qzPKIYBhTzmWVqmIbncXVdly7mkITMU07ysDYht4EZVZZFPVdhGEZpA1MUZtc0
131DcfHx8GCjbIr836JK2l31YtuWfZdWKfT6SleP6nbZ4aFpmEYmYclUJDALMOinjhJb8pVtRzeMHEr
1326IBPcx15OMmywP2CYOW0OC/y0EMpItu2p1b0fFBo5mV9Rm6LrwMfe5VvXq3j/QR6lbOzs7i4uEj9
133c9Ok63rwDEgZ4pmYZVGr1QYWxq5UKkGDI+n9dEXH/fJRt9uF53mYn5/H9va2MmE57eLr3W4X7XYb
134mqZhZmam52ERk3CVgu2Tlsse5qQX9czOzuL777/H+vr6RD4/LUlP8DLdByY7v1uv16debzJL3C+9
135srwFQSWe5020wSHzPM08yF0t2XdHDn764buJhuWTJ08m8tlpE08el1Wm+ctRj1wa971Fx/1CRTSJ
13652lOwlR7mO9P9nH09mXwOloP9s2r9Yku6hFhOTs7O7F/I23RJ8YPMukWYN4kma8VC1/y1FKdFO4X
137Kqq0a89OwlQD8+xkf2ggMiz7iUUawx5llVWZqKzkqcWZJ9wvVHR5D81czmGmrahhKXieh1arFRQx
1380HUdmqbBdV24rluqnuW4sj7R8or7hfImz6GpfGAWPSzDHMcp1TzlIGJuV7ZHVZZQ4H4hVeQ1NHO3
1396CdNKoUl9Yo+TDat9xYd9wupIo8LgZQNTIal2hzHkWpllq0wNvcLqWRQaGZVpEXJwGRYlsPm5ubQ
140cMjiAbN5wP1CKomGpud5mVX+mWqln2lIEpZsYRdbt9vF7u4ufN9HpVIJqpC4rouDgwNsb2+Xcp6O
141+0Vt0670kweHh4fQdR2Xl5eZ3hEws7q6+ktm/3rKkvYs817ph4iI8kOZVbKi3B0REdEkKNXDJCIi
142mhQlF/0QERGljYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQk
1434bPFxcWst4GIiCj32MMkIiKSMHPnzh3WkiUiIhqBPUwiIiIJDEwiIiIJDEwiIiIJ/weH8+Ed/xCz
144fAAAAABJRU5ErkJggg==
145"
146 id="image2845"
147 x="-15.489594"
148 y="-8.2945251" /></g><g
149 inkscape:groupmode="layer"
150 id="layer5"
151 inkscape:label="dot" /><g
152 inkscape:label="over"
153 inkscape:groupmode="layer"
154 id="layer1"
155 style="display:none"><ellipse
156 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
157 id="path2637"
158 cx="24"
159 cy="33"
160 rx="4.3600898"
161 ry="4.3600893" /><path
162 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
163 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
164 id="path2142"
165 inkscape:path-effect="#path-effect2144"
166 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
167 sodipodi:nodetypes="csc" /><path
168 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
169 d="M 26,22 H 39 V 9"
170 id="path2626"
171 sodipodi:nodetypes="ccc" /></g><g
172 inkscape:groupmode="layer"
173 id="layer6"
174 inkscape:label="into"
175 style="display:none"><ellipse
176 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
177 id="path2637-3"
178 cx="24"
179 cy="38.5"
180 rx="4.3600898"
181 ry="4.3600893" /><path
182 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
183 d="M 24,2 V 24"
184 id="path2668"
185 sodipodi:nodetypes="cc" /><path
186 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
187 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
188 id="path2626-8"
189 sodipodi:nodetypes="ccc" /></g><g
190 inkscape:groupmode="layer"
191 id="g2714"
192 inkscape:label="out"
193 style="display:none"><ellipse
194 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
195 id="ellipse2708"
196 cx="24"
197 cy="38.5"
198 rx="4.3600898"
199 ry="4.3600893" /><path
200 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
201 d="M 24,29.722858 V 7.7228579"
202 id="path2710"
203 sodipodi:nodetypes="cc" /><path
204 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
205 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
206 id="path2712"
207 sodipodi:nodetypes="ccc" /></g><g
208 inkscape:groupmode="layer"
209 id="layer8"
210 inkscape:label="resume"><path
211 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
212 d="M 13,12 V 38"
213 id="path2850"
214 sodipodi:nodetypes="cc" /><path
215 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
216 d="M 21,16 V 34 L 36.5,25 Z"
217 id="path2852"
218 sodipodi:nodetypes="cccc" /></g></svg>
public/index.html+139 -52
@@ -383,7 +383,7 @@
383 Max Response Length (tokens)383 Max Response Length (tokens)
384 </div>384 </div>
385 <div class="wide100p">385 <div class="wide100p">
386 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="50" max="8000">386 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="16384">
387 </div>387 </div>
388 </div>388 </div>
389 <div class="range-block" data-source="openai,custom">389 <div class="range-block" data-source="openai,custom">
@@ -1273,7 +1273,7 @@
1273 <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />1273 <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />
1274 <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="max_tokens_second_textgenerationwebui" id="max_tokens_second_counter_textgenerationwebui">1274 <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="max_tokens_second_textgenerationwebui" id="max_tokens_second_counter_textgenerationwebui">
1275 </div>1275 </div>
1276 <div data-newbie-hidden data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" name="smoothingBlock" class="wide100p">1276 <div data-newbie-hidden data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" id="smoothingBlock" name="smoothingBlock" class="wide100p">
1277 <h4 class="wide100p textAlignCenter">1277 <h4 class="wide100p textAlignCenter">
1278 <label data-i18n="Smooth Sampling">Smooth Sampling</label>1278 <label data-i18n="Smooth Sampling">Smooth Sampling</label>
1279 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Smooth_Sampling_desc" title="Allows you to use quadratic/cubic transformations to adjust the distribution. Lower Smoothing Factor values will be more creative, usually between 0.2-0.3 is the sweetspot (assuming the curve = 1). Higher Smoothing Curve values will make the curve steeper, which will punish low probability choices more aggressively. 1.0 curve is equivalent to only using Smoothing Factor."></div>1279 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Smooth_Sampling_desc" title="Allows you to use quadratic/cubic transformations to adjust the distribution. Lower Smoothing Factor values will be more creative, usually between 0.2-0.3 is the sweetspot (assuming the curve = 1). Higher Smoothing Curve values will make the curve steeper, which will punish low probability choices more aggressively. 1.0 curve is equivalent to only using Smoothing Factor."></div>
@@ -1358,7 +1358,7 @@
1358 </div>1358 </div>
1359 </div>1359 </div>
1360 </div>1360 </div>
1361 <div data-newbie-hidden id="mirostat_block_ooba" class="wide100p">1361 <div data-newbie-hidden data-tg-type="ooba,aphrodite,infermaticai,koboldcpp,llamacpp,mancer,ollama,tabby" id="mirostat_block_ooba" class="wide100p">
1362 <h4 class="wide100p textAlignCenter">1362 <h4 class="wide100p textAlignCenter">
1363 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>1363 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>
1364 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat_desc" title="Mirostat is a thermostat for output perplexity.&#13;Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap&#13;(where, as the autoregressive inference produces text, the perplexity of the output tends toward zero)&#13;and the confusion trap (where the perplexity diverges).&#13;For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).&#13;Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>1364 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat_desc" title="Mirostat is a thermostat for output perplexity.&#13;Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap&#13;(where, as the autoregressive inference produces text, the perplexity of the output tends toward zero)&#13;and the confusion trap (where the perplexity diverges).&#13;For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).&#13;Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>
@@ -1387,7 +1387,7 @@
1387 </div>1387 </div>
1388 </div>1388 </div>
1389 </div>1389 </div>
1390 <div data-newbie-hidden data-tg-type="ooba, vllm" name="beamSearchBlock" class="wide100p">1390 <div data-newbie-hidden data-tg-type="ooba, vllm" id="beamSearchBlock" name="beamSearchBlock" class="wide100p">
1391 <h4 class="wide100p textAlignCenter">1391 <h4 class="wide100p textAlignCenter">
1392 <label>1392 <label>
1393 <span data-i18n="Beam search">Beam Search</span>1393 <span data-i18n="Beam search">Beam Search</span>
@@ -1413,7 +1413,7 @@
1413 </div>1413 </div>
1414 </div>1414 </div>
1415 </div>1415 </div>
1416 <div data-tg-type="ooba" data-newbie-hidden name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">1416 <div data-tg-type="ooba" data-newbie-hidden id="contrastiveSearchBlock" name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
1417 <h4 class="textAlignCenter" data-i18n="Contrastive search">Contrastive Search1417 <h4 class="textAlignCenter" data-i18n="Contrastive search">Contrastive Search
1418 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="Contrastive_search_txt" title="A sampler that encourages diversity while maintaining coherence, by exploiting the isotropicity of the representation space of most LLMs. For details, see the paper A Contrastive Framework for Neural Text Generation by Su et al. (2022)."></div>1418 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="Contrastive_search_txt" title="A sampler that encourages diversity while maintaining coherence, by exploiting the isotropicity of the representation space of most LLMs. For details, see the paper A Contrastive Framework for Neural Text Generation by Su et al. (2022)."></div>
1419 </h4>1419 </h4>
@@ -1544,7 +1544,7 @@
1544 <h4 class="wide100p textAlignCenter">1544 <h4 class="wide100p textAlignCenter">
1545 <label>1545 <label>
1546 <span data-i18n="Grammar String">Grammar String</span>1546 <span data-i18n="Grammar String">Grammar String</span>
1547 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GNBF or ENBF, depends on the backend in use. If you're using this you should know which." title="GNBF or ENBF, depends on the backend in use. If you're using this you should know which."></div>1547 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GBNF or EBNF, depends on the backend in use. If you're using this you should know which." title="GBNF or EBNF, depends on the backend in use. If you're using this you should know which."></div>
1548 <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">1548 <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">
1549 <small>1549 <small>
1550 <div class="fa-solid fa-up-right-from-square note-link-span"></div>1550 <div class="fa-solid fa-up-right-from-square note-link-span"></div>
@@ -1554,13 +1554,13 @@
1554 </h4>1554 </h4>
1555 <textarea id="grammar_string_textgenerationwebui" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea>1555 <textarea id="grammar_string_textgenerationwebui" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea>
1556 </div>1556 </div>
1557 <div id="sampler_order_block" data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">1557 <div id="sampler_order_block_kcpp" data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">
1558 <hr class="wide100p">1558 <hr class="wide100p">
1559 <div class="range-block-title">1559 <div class="range-block-title">
1560 <span data-i18n="Samplers Order">Samplers Order</span>1560 <span data-i18n="Samplers Order">Samplers Order</span>
1561 </div>1561 </div>
1562 <div class="toggle-description widthUnset" data-i18n="Samplers will be applied in a top-down order. Use with caution.">1562 <div class="toggle-description widthUnset" data-i18n="Samplers will be applied in a top-down order. Use with caution.">
1563 Samplers will be applied in a top-down order.1563 kcpp only. Samplers will be applied in a top-down order.
1564 Use with caution.1564 Use with caution.
1565 </div>1565 </div>
1566 <div id="koboldcpp_order" class="prompt_order">1566 <div id="koboldcpp_order" class="prompt_order">
@@ -1597,10 +1597,10 @@
1597 <span data-i18n="Load default order">Load default order</span>1597 <span data-i18n="Load default order">Load default order</span>
1598 </div>1598 </div>
1599 </div>1599 </div>
1600 <div data-newbie-hidden data-tg-type="llamacpp" class="range-block flexFlowColumn wide100p">1600 <div id="sampler_order_block_lcpp" data-newbie-hidden data-tg-type="llamacpp" class="range-block flexFlowColumn wide100p">
1601 <hr class="wide100p">1601 <hr class="wide100p">
1602 <h4 class="range-block-title justifyCenter">1602 <h4 class="range-block-title justifyCenter">
1603 <span data-i18n="Samplers Order">Samplers Order</span>1603 <span data-i18n="Sampler Order">Sampler Order</span>
1604 <div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored." title="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored."></div>1604 <div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored." title="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored."></div>
1605 </h4>1605 </h4>
1606 <div class="toggle-description widthUnset" data-i18n="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.">1606 <div class="toggle-description widthUnset" data-i18n="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.">
@@ -1618,7 +1618,7 @@
1618 <span data-i18n="Load default order">Load default order</span>1618 <span data-i18n="Load default order">Load default order</span>
1619 </div>1619 </div>
1620 </div>1620 </div>
1621 <div data-newbie-hidden data-tg-type="ooba" class="range-block flexFlowColumn wide100p">1621 <div id="sampler_priority_block_ooba" data-newbie-hidden data-tg-type="ooba" class="range-block flexFlowColumn wide100p">
1622 <hr class="wide100p">1622 <hr class="wide100p">
1623 <h4 class="range-block-title justifyCenter">1623 <h4 class="range-block-title justifyCenter">
1624 <span data-i18n="Sampler Priority">Sampler Priority</span>1624 <span data-i18n="Sampler Priority">Sampler Priority</span>
@@ -1770,13 +1770,13 @@
1770 </div>1770 </div>
1771 </label>1771 </label>
1772 </div>1772 </div>
1773 <div class="range-block" data-source="openai,openrouter,makersuite,claude,custom">1773 <div class="range-block" data-source="openai,openrouter,makersuite,claude,custom,01ai">
1774 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">1774 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
1775 <input id="openai_image_inlining" type="checkbox" />1775 <input id="openai_image_inlining" type="checkbox" />
1776 <span data-i18n="Send inline images">Send inline images</span>1776 <span data-i18n="Send inline images">Send inline images</span>
1777 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">1777 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
1778 <span data-i18n="image_inlining_hint_1">Sends images in prompts if the model supports it (e.g. GPT-4V, Claude 3 or Llava 13B).1778 <span data-i18n="image_inlining_hint_1">Sends images in prompts if the model supports it (e.g. GPT-4V, Claude 3 or Llava 13B).
1779 Use the</span> <code><i class="fa-solid fa-paperclip"></i></code> <span data-i18n="image_inlining_hint_2">action on any message or the</span>1779 Use the</span> <code><i class="fa-solid fa-paperclip"></i></code> <span data-i18n="image_inlining_hint_2">action on any message or the</span>
1780 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code> <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>1780 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code> <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
1781 </div>1781 </div>
1782 </label>1782 </label>
@@ -1823,10 +1823,16 @@
1823 </div>1823 </div>
1824 <div data-newbie-hidden class="range-block" data-source="claude">1824 <div data-newbie-hidden class="range-block" data-source="claude">
1825 <div class="wide100p">1825 <div class="wide100p">
1826 <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>1826 <div class="flex-container alignItemsCenter">
1827 <textarea id="claude_assistant_prefill" class="text_pole textarea_compact autoSetHeight" name="assistant_prefill" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>1827 <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
1828 <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>1828 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_prefill" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
1829 <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact autoSetHeight" name="assistant_impersonation" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>1829 </div>
1830 <textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1831 <div class="flex-container alignItemsCenter">
1832 <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
1833 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_impersonation" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
1834 </div>
1835 <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1830 </div>1836 </div>
1831 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">1837 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
1832 <input id="claude_use_sysprompt" type="checkbox" />1838 <input id="claude_use_sysprompt" type="checkbox" />
@@ -2427,10 +2433,11 @@
2427 <optgroup>2433 <optgroup>
2428 <option value="01ai">01.AI (Yi)</option>2434 <option value="01ai">01.AI (Yi)</option>
2429 <option value="ai21">AI21</option>2435 <option value="ai21">AI21</option>
2436 <option value="blockentropy">Block Entropy</option>
2430 <option value="claude">Claude</option>2437 <option value="claude">Claude</option>
2431 <option value="cohere">Cohere</option>2438 <option value="cohere">Cohere</option>
2432 <option value="groq">Groq</option>2439 <option value="groq">Groq</option>
2433 <option value="makersuite">Google MakerSuite</option>2440 <option value="makersuite">Google AI Studio</option>
2434 <option value="mistralai">MistralAI</option>2441 <option value="mistralai">MistralAI</option>
2435 <option value="openrouter">OpenRouter</option>2442 <option value="openrouter">OpenRouter</option>
2436 <option value="perplexity">Perplexity</option>2443 <option value="perplexity">Perplexity</option>
@@ -2570,7 +2577,9 @@
2570 </optgroup>2577 </optgroup>
2571 <optgroup label="GPT-4o">2578 <optgroup label="GPT-4o">
2572 <option value="gpt-4o">gpt-4o</option>2579 <option value="gpt-4o">gpt-4o</option>
2580 <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
2573 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>2581 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2582 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2574 </optgroup>2583 </optgroup>
2575 <optgroup label="gpt-4o-mini">2584 <optgroup label="gpt-4o-mini">
2576 <option value="gpt-4o-mini">gpt-4o-mini</option>2585 <option value="gpt-4o-mini">gpt-4o-mini</option>
@@ -2791,7 +2800,7 @@
2791 </div>2800 </div>
2792 </form>2801 </form>
2793 <form id="makersuite_form" data-source="makersuite" action="javascript:void(null);" method="post" enctype="multipart/form-data">2802 <form id="makersuite_form" data-source="makersuite" action="javascript:void(null);" method="post" enctype="multipart/form-data">
2794 <h4 data-i18n="MakerSuite API Key">MakerSuite API Key</h4>2803 <h4 data-i18n="Google AI Studio API Key">Google AI Studio API Key</h4>
2795 <div class="flex-container">2804 <div class="flex-container">
2796 <input id="api_key_makersuite" name="api_key_makersuite" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">2805 <input id="api_key_makersuite" name="api_key_makersuite" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
2797 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_makersuite"></div>2806 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_makersuite"></div>
@@ -2802,21 +2811,26 @@
2802 <div>2811 <div>
2803 <h4 data-i18n="Google Model">Google Model</h4>2812 <h4 data-i18n="Google Model">Google Model</h4>
2804 <select id="model_google_select">2813 <select id="model_google_select">
2805 <optgroup label="Latest">2814 <optgroup label="Primary">
2806 <!-- Doesn't work without "latest". Maybe my key is scuffed? -->2815 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
2807 <option value="gemini-1.5-flash-latest">Gemini 1.5 Flash</option>2816 <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
2808 <!-- Points to 1.0, no default 1.5 endpoint -->2817 <option value="gemini-1.0-pro">Gemini 1.0 Pro</option>
2809 <option value="gemini-pro">Gemini Pro</option>2818 <option value="gemini-pro">Gemini Pro (1.0)</option>
2810 <option value="gemini-pro-vision">Gemini Pro Vision</option>2819 <option value="gemini-pro-vision">Gemini Pro Vision (1.0)</option>
2811 <option value="gemini-ultra">Gemini Ultra</option>2820 <option value="gemini-ultra">Gemini Ultra (1.0)</option>
2812 <option value="text-bison-001">Bison Text</option>
2813 <option value="chat-bison-001">Bison Chat</option>
2814 </optgroup>
2815 <optgroup label="Sub-versions">
2816 <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro</option>
2817 <option value="gemini-1.0-pro-latest">Gemini 1.0 Pro</option>
2818 <option value="gemini-1.0-pro-vision-latest">Gemini 1.0 Pro Vision</option>
2819 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>2821 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
2822 <option value="text-bison-001">PaLM 2 (Legacy)</option>
2823 <option value="chat-bison-001">PaLM 2 Chat (Legacy)</option>
2824 </optgroup>
2825 <optgroup label="Subversions">
2826 <option value="gemini-1.5-pro-exp-0801">Gemini 1.5 Pro Experiment 2024-08-01</option>
2827 <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>
2828 <option value="gemini-1.5-pro-001">Gemini 1.5 Pro [001]</option>
2829 <option value="gemini-1.5-flash-latest">Gemini 1.5 Flash [latest]</option>
2830 <option value="gemini-1.5-flash-001">Gemini 1.5 Flash [001]</option>
2831 <option value="gemini-1.0-pro-latest">Gemini 1.0 Pro [latest]</option>
2832 <option value="gemini-1.0-pro-001">Gemini 1.0 Pro (Tuning) [001]</option>
2833 <option value="gemini-1.0-pro-vision-latest">Gemini 1.0 Pro Vision [latest]</option>
2820 </optgroup>2834 </optgroup>
2821 </select>2835 </select>
2822 </div>2836 </div>
@@ -2894,7 +2908,20 @@
2894 </div>2908 </div>
2895 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>2909 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
2896 <select id="model_perplexity_select">2910 <select id="model_perplexity_select">
2897 <optgroup label="Perplexity Models">2911 <optgroup label="Perplexity Sonar Models">
2912 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
2913 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
2914 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
2915 </optgroup>
2916 <optgroup label="Perplexity Chat Models">
2917 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
2918 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
2919 </optgroup>
2920 <optgroup label="Open-Source Models">
2921 <option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option>
2922 <option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option>
2923 </optgroup>
2924 <optgroup label="Deprecated Models">
2898 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>2925 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
2899 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>2926 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
2900 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>2927 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
@@ -2903,8 +2930,6 @@
2903 <option value="sonar-small-online">sonar-small-online</option>2930 <option value="sonar-small-online">sonar-small-online</option>
2904 <option value="sonar-medium-chat">sonar-medium-chat</option>2931 <option value="sonar-medium-chat">sonar-medium-chat</option>
2905 <option value="sonar-medium-online">sonar-medium-online</option>2932 <option value="sonar-medium-online">sonar-medium-online</option>
2906 </optgroup>
2907 <optgroup label="Open-Source Models">
2908 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>2933 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
2909 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>2934 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
2910 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>2935 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
@@ -2938,6 +2963,20 @@
2938 </select>2963 </select>
2939 </div>2964 </div>
2940 </form>2965 </form>
2966 <form id="blockentropy_form" data-source="blockentropy">
2967 <h4 data-i18n="Block Entropy API Key">Block Entropy API Key</h4>
2968 <div class="flex-container">
2969 <input id="api_key_blockentropy" name="api_key_blockentropy" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
2970 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_blockentropy"></div>
2971 </div>
2972 <div data-for="api_key_blockentropy" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
2973 For privacy reasons, your API key will be hidden after you reload the page.
2974 </div>
2975 <h4 data-i18n="Select a Model">Select a Model</h4>
2976 <div class="flex-container">
2977 <select id="model_blockentropy_select" class="text_pole"></select>
2978 </div>
2979 </form>
2941 <form id="custom_form" data-source="custom">2980 <form id="custom_form" data-source="custom">
2942 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>2981 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
2943 <div class="flex-container">2982 <div class="flex-container">
@@ -3069,7 +3108,11 @@
3069 </div>3108 </div>
3070 <label class="checkbox_label" title="Add Chat Start and Example Separator to a list of stopping strings." data-i18n="[title]Add Chat Start and Example Separator to a list of stopping strings.">3109 <label class="checkbox_label" title="Add Chat Start and Example Separator to a list of stopping strings." data-i18n="[title]Add Chat Start and Example Separator to a list of stopping strings.">
3071 <input id="context_use_stop_strings" type="checkbox" />3110 <input id="context_use_stop_strings" type="checkbox" />
3072 <small data-i18n="Use as Stop Strings">Use as Stop Strings</small>3111 <small data-i18n="Separators as Stop Strings">Separators as Stop Strings</small>
3112 </label>
3113 <label class="checkbox_label" title="Add Character and User names to a list of stopping strings." data-i18n="[title]Add Character and User names to a list of stopping strings.">
3114 <input id="context_names_as_stop_strings" type="checkbox" />
3115 <small data-i18n="Names as Stop Strings">Names as Stop Strings</small>
3073 </label>3116 </label>
3074 <label class="checkbox_label" title="Includes Post-History Instructions at the end of the prompt, if defined in the character card AND ''Prefer Char. Instructions'' is enabled.&#10;THIS IS NOT RECOMMENDED FOR TEXT COMPLETION MODELS, CAN LEAD TO BAD OUTPUT." data-i18n="[title]context_allow_post_history_instructions">3117 <label class="checkbox_label" title="Includes Post-History Instructions at the end of the prompt, if defined in the character card AND ''Prefer Char. Instructions'' is enabled.&#10;THIS IS NOT RECOMMENDED FOR TEXT COMPLETION MODELS, CAN LEAD TO BAD OUTPUT." data-i18n="[title]context_allow_post_history_instructions">
3075 <input id="context_allow_jailbreak" type="checkbox" />3118 <input id="context_allow_jailbreak" type="checkbox" />
@@ -3406,6 +3449,7 @@
3406 <!-- Option #2 was a legacy GPT-2/3 tokenizer -->3449 <!-- Option #2 was a legacy GPT-2/3 tokenizer -->
3407 <option value="3">Llama 1/2</option>3450 <option value="3">Llama 1/2</option>
3408 <option value="12">Llama 3</option>3451 <option value="12">Llama 3</option>
3452 <option value="13">Gemma / Gemini</option>
3409 <option value="4">NerdStash (NovelAI Clio)</option>3453 <option value="4">NerdStash (NovelAI Clio)</option>
3410 <option value="5">NerdStash v2 (NovelAI Kayra)</option>3454 <option value="5">NerdStash v2 (NovelAI Kayra)</option>
3411 <option value="7">Mistral</option>3455 <option value="7">Mistral</option>
@@ -3578,6 +3622,7 @@
3578 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Scan chronologically until reached min entries or token budget." data-i18n="[title]Scan chronologically until reached min entries or token budget.">3622 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Scan chronologically until reached min entries or token budget." data-i18n="[title]Scan chronologically until reached min entries or token budget.">
3579 <small>3623 <small>
3580 <span data-i18n="Min Activations">Min Activations</span>3624 <span data-i18n="Min Activations">Min Activations</span>
3625 <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title](disabled when max recursion steps are used)" title="(disabled when max recursion steps are used)"></div>
3581 </small>3626 </small>
3582 <input class="neo-range-slider" type="range" id="world_info_min_activations" name="world_info_min_activations" min="0" max="100" step="1">3627 <input class="neo-range-slider" type="range" id="world_info_min_activations" name="world_info_min_activations" min="0" max="100" step="1">
3583 <input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations" id="world_info_min_activations_counter">3628 <input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations" id="world_info_min_activations_counter">
@@ -3591,6 +3636,14 @@
3591 <input class="neo-range-slider" type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">3636 <input class="neo-range-slider" type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">
3592 <input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations_depth_max" id="world_info_min_activations_depth_max_counter">3637 <input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations_depth_max" id="world_info_min_activations_depth_max_counter">
3593 </div>3638 </div>
3639 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">
3640 <small>
3641 <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
3642 <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>
3643 </small>
3644 <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">
3645 <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">
3646 </div>
35943647
3595 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink flexBasis48p">3648 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink flexBasis48p">
3596 <small data-i18n="Insertion Strategy">3649 <small data-i18n="Insertion Strategy">
@@ -4054,25 +4107,31 @@
4054 <input id="world_import_dialog" type="checkbox" />4107 <input id="world_import_dialog" type="checkbox" />
4055 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>4108 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>
4056 </label>4109 </label>
4110 <label data-newbie-hidden class="checkbox_label" for="enable_auto_select_input" title="Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields." data-i18n="[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.">
4111 <input id="enable_auto_select_input" type="checkbox" />
4112 <small data-i18n="Auto-select Input Text">Auto-select Input Text</small>
4113 </label>
4057 <label class="checkbox_label" for="restore_user_input" title="Restore unsaved user input on page refresh." data-i18n="[title]Restore unsaved user input on page refresh">4114 <label class="checkbox_label" for="restore_user_input" title="Restore unsaved user input on page refresh." data-i18n="[title]Restore unsaved user input on page refresh">
4058 <input id="restore_user_input" type="checkbox" />4115 <input id="restore_user_input" type="checkbox" />
4059 <small data-i18n="Restore User Input">Restore User Input</small>4116 <small data-i18n="Restore User Input">Restore User Input</small>
4060 </label>4117 </label>
4061 <label data-newbie-hidden id="movingUIModeCheckBlock" for="movingUImode" class="checkbox_label" title="Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile." data-i18n="[title]Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile">4118 <div class="flex-container alignItemsCenter">
4062 <input id="movingUImode" type="checkbox" />4119 <label data-newbie-hidden id="movingUIModeCheckBlock" for="movingUImode" class="checkbox_label" title="Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile." data-i18n="[title]Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile">
4063 <small data-i18n="Movable UI Panels">MovingUI&nbsp;<i class="fa-solid fa-desktop"></i></small>4120 <input id="movingUImode" type="checkbox" />
4064 </label>4121 <small data-i18n="Movable UI Panels">MovingUI&nbsp;<i class="fa-solid fa-desktop"></i></small>
4122 </label>
4123 <div data-newbie-hidden id="movingUIreset" title="Reset MovingUI panel sizes/locations." class="menu_button margin0" data-i18n="[title]Reset MovingUI panel sizes/locations."><i class=" fa-solid fa-recycle margin-r5"></i> Reset</div>
4124 </div>
4065 <div data-newbie-hidden id="MovingUI-presets-block" class="flex-container alignitemscenter">4125 <div data-newbie-hidden id="MovingUI-presets-block" class="flex-container alignitemscenter">
4066 <div class="flex-container alignItemsFlexEnd">4126 <div class="flex-container alignItemsFlexEnd">
4067 <label for="movingUIPresets" title="MovingUI preset. Predefined/saved draggable positions." data-i18n="[title]MovingUI preset. Predefined/saved draggable positions">4127 <label for="movingUIPresets" title="MovingUI preset. Predefined/saved draggable positions." data-i18n="[title]MovingUI preset. Predefined/saved draggable positions">
4068 <small data-i18n="MUI Preset">MUI Preset:</small>4128 <small data-i18n="MUI Preset">MovingUI Preset:</small>
4069 <div class="flex-container flexnowrap">4129 <div class="flex-container flexnowrap">
4070 <select id="movingUIPresets" class="widthNatural flex1 margin0">4130 <select id="movingUIPresets" class="widthNatural flex1 margin0">
4071 </select>4131 </select>
4072 </div>4132 </div>
4073 </label>4133 </label>
4074 <div id="movingui-preset-save-button" title="Save changes to a new MovingUI preset file." data-i18n="[title]Save movingUI changes to a new file" class="menu_button margin0 fa-solid fa-save"></div>4134 <div id="movingui-preset-save-button" title="Save changes to a new MovingUI preset file." data-i18n="[title]Save movingUI changes to a new file" class="menu_button margin0 fa-solid fa-save"></div>
4075 <div data-newbie-hidden id="movingUIreset" title="Reset MovingUI panel sizes/locations." class="menu_button fa-solid fa-recycle margin0" data-i18n="[title]Reset MovingUI panel sizes/locations."></div>
4076 </div>4135 </div>
4077 </div>4136 </div>
4078 <div data-newbie-hidden id="CustomCSS-block" class="flex-container flexFlowColumn">4137 <div data-newbie-hidden id="CustomCSS-block" class="flex-container flexFlowColumn">
@@ -4144,6 +4203,12 @@
4144 Quick "Continue" button4203 Quick "Continue" button
4145 </small>4204 </small>
4146 </label>4205 </label>
4206 <label class="checkbox_label" for="quick_impersonate" title="Show a button in the input area to ask the AI to impersonate your character for a single message." data-i18n="[title]Show a button in the input area to ask the AI to impersonate your character for a single message">
4207 <input id="quick_impersonate" type="checkbox" />
4208 <small data-i18n="Quick 'Impersonate' button">
4209 Quick "Impersonate" button
4210 </small>
4211 </label>
4147 <div class="checkbox-container flex-container">4212 <div class="checkbox-container flex-container">
4148 <label data-newbie-hidden class="checkbox_label" for="swipes-checkbox" title="Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile." data-i18n="[title]Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile">4213 <label data-newbie-hidden class="checkbox_label" for="swipes-checkbox" title="Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile." data-i18n="[title]Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile">
4149 <input id="swipes-checkbox" type="checkbox" />4214 <input id="swipes-checkbox" type="checkbox" />
@@ -4264,6 +4329,16 @@
4264 </div>4329 </div>
4265 </div>4330 </div>
4266 </div>4331 </div>
4332 <div title="Determines which keys select an item from the AutoComplete suggestions">
4333 <label data-i18n="Keyboard">
4334 <small>Keyboard:</small>
4335 </label>
4336 <select id="stscript_autocomplete_select">
4337 <option value="3" data-i18n="Select with Tab or Enter">Select with Tab or Enter</option>
4338 <option value="1" data-i18n="Select with Tab">Select with Tab</option>
4339 <option value="2" data-i18n="Select with Enter">Select with Enter</option>
4340 </select>
4341 </div>
4267 <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">4342 <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">
4268 <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>4343 <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>
4269 <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">4344 <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">
@@ -4605,7 +4680,7 @@
4605 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>4680 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>
4606 <input type="hidden" id="fav_checkbox" name="fav" />4681 <input type="hidden" id="fav_checkbox" name="fav" />
4607 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>4682 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
4608 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore" data-i18n="[title]Character Lore"></div>4683 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div>
4609 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>4684 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
4610 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>4685 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
4611 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->4686 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
@@ -4923,7 +4998,7 @@
4923 <div class="popup-crop-wrap">4998 <div class="popup-crop-wrap">
4924 <img class="popup-crop-image" src="">4999 <img class="popup-crop-image" src="">
4925 </div>5000 </div>
4926 <textarea class="popup-input text_pole result-control" rows="1" data-result="1" data-result-event="submit"></textarea>5001 <textarea class="popup-input text_pole result-control auto-select" rows="1" data-result="1" data-result-event="submit"></textarea>
4927 <div class="popup-inputs"></div>5002 <div class="popup-inputs"></div>
4928 <div class="popup-controls">5003 <div class="popup-controls">
4929 <div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>5004 <div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>
@@ -5262,21 +5337,22 @@
5262 <div class="world_entry">5337 <div class="world_entry">
5263 <form class="world_entry_form wi-card-entry">5338 <form class="world_entry_form wi-card-entry">
5264 <div class="inline-drawer wide100p">5339 <div class="inline-drawer wide100p">
5265 <div class="inline-drawer-toggle inline-drawer-header gap5px padding0">5340 <div class="inline-drawer-header gap5px padding0">
5266 <span class="drag-handle">&#9776;</span>5341 <span class="drag-handle">&#9776;</span>
5267 <div class="gap5px world_entry_thin_controls wide100p alignitemscenter">5342 <div class="gap5px world_entry_thin_controls wide100p alignitemscenter">
5268 <div class="fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>5343 <div class="inline-drawer-toggle fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
5344 <div class="fa-solid fa-toggle-on killSwitch" name="entryKillSwitch" title="Toggle entry's active state."></div>
5269 <div class="flex-container alignitemscenter wide100p">5345 <div class="flex-container alignitemscenter wide100p">
5346
5270 <div class="WIEntryTitleAndStatus flex-container flex1 alignitemscenter">5347 <div class="WIEntryTitleAndStatus flex-container flex1 alignitemscenter">
5348
5271 <div class="flex-container flex1">5349 <div class="flex-container flex1">
5272 <textarea class="text_pole" rows="1" name="comment" maxlength="5000" data-i18n="[placeholder]Entry Title/Memo" placeholder="Entry Title/Memo"></textarea>5350 <textarea class="text_pole" rows="1" name="comment" maxlength="5000" data-i18n="[placeholder]Entry Title/Memo" placeholder="Entry Title/Memo"></textarea>
5273 </div>5351 </div>
5274 <!-- <span class="world_entry_form_position_value"></span> -->
5275 <select data-i18n="[title]WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized❌ Disabled" title="WI Entry Status:&#13;🔵 Constant&#13;🟢 Normal&#13;🔗 Vectorized&#13;❌ Disabled" name="entryStateSelector" class="text_pole widthNatural margin0">5352 <select data-i18n="[title]WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized❌ Disabled" title="WI Entry Status:&#13;🔵 Constant&#13;🟢 Normal&#13;🔗 Vectorized&#13;❌ Disabled" name="entryStateSelector" class="text_pole widthNatural margin0">
5276 <option value="constant" title="Constant" data-i18n="[title]WI_Entry_Status_Constant">🔵</option>5353 <option value="constant" title="Constant" data-i18n="[title]WI_Entry_Status_Constant">🔵</option>
5277 <option value="normal" title="Normal" data-i18n="[title]WI_Entry_Status_Normal">🟢</option>5354 <option value="normal" title="Normal" data-i18n="[title]WI_Entry_Status_Normal">🟢</option>
5278 <option value="vectorized" title="Vectorized" data-i18n="[title]WI_Entry_Status_Vectorized">🔗</option>5355 <option value="vectorized" title="Vectorized" data-i18n="[title]WI_Entry_Status_Vectorized">🔗</option>
5279 <option value="disabled" title="Disabled" data-i18n="[title]WI_Entry_Status_Disabled">❌</option>
5280 </select>5356 </select>
5281 </div>5357 </div>
5282 <div class="WIEnteryHeaderControls flex-container">5358 <div class="WIEnteryHeaderControls flex-container">
@@ -5525,13 +5601,16 @@
5525 <div class="flex-container wide100p flexGap10">5601 <div class="flex-container wide100p flexGap10">
5526 <div class="flex4 flex-container flexFlowColumn flexNoGap">5602 <div class="flex4 flex-container flexFlowColumn flexNoGap">
5527 <div class="flex-container justifySpaceBetween">5603 <div class="flex-container justifySpaceBetween">
5528 <small for="characterFilter" data-i18n="Filter to Character(s)">5604 <small for="characterFilter" data-i18n="Filter to Characters or Tags">
5529 Filter to Character(s)5605 Filter to Characters or Tags
5530 </small>5606 </small>
5531 <label class="checkbox_label flexNoGap margin-r5" for="character_exclusion">5607 <label class="checkbox_label flexNoGap margin-r5" for="character_exclusion">
5532 <input type="checkbox" name="character_exclusion" />5608 <input type="checkbox" name="character_exclusion" />
5533 <span>5609 <span>
5534 <small data-i18n="Character Exclusion">Character Exclusion</small>5610 <small title="Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry" data-i18n="[title]Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry">
5611 <span data-i18n="Exclude">Exclude</span>
5612 <div class="fa-solid fa-circle-info opacity50p"></div>
5613 </small>
5535 </span>5614 </span>
5536 </label>5615 </label>
5537 </div>5616 </div>
@@ -5749,6 +5828,11 @@
5749 <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div>5828 <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div>
5750 <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div>5829 <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div>
5751 </div>5830 </div>
5831 <div class="mes_img_swipes">
5832 <div title="Swipe left" class="right_menu_button fa-lg fa-solid fa-chevron-left mes_img_swipe_left" data-i18n="[title]Swipe left"></div>
5833 <div class="mes_img_swipe_counter">1/1</div>
5834 <div title="Swipe right" class="right_menu_button fa-lg fa-solid fa-chevron-right mes_img_swipe_right" data-i18n="[title]Swipe right"></div>
5835 </div>
5752 <img class="mes_img" src="" />5836 <img class="mes_img" src="" />
5753 </div>5837 </div>
5754 <div class="mes_bias"></div>5838 <div class="mes_bias"></div>
@@ -5811,6 +5895,7 @@
5811 Enable simple UI mode5895 Enable simple UI mode
5812 </span>5896 </span>
5813 </label>5897 </label>
5898 <div class="expander"></div>
5814 <div class="textAlignCenter">5899 <div class="textAlignCenter">
5815 <h3 data-i18n="Looking for AI characters?">5900 <h3 data-i18n="Looking for AI characters?">
5816 Looking for AI characters?5901 Looking for AI characters?
@@ -5829,6 +5914,7 @@
5829 </span>5914 </span>
5830 </span>5915 </span>
5831 </div>5916 </div>
5917 <div class="expander"></div>
5832 <h3 data-i18n="Your Persona">5918 <h3 data-i18n="Your Persona">
5833 Your Persona5919 Your Persona
5834 </h3>5920 </h3>
@@ -6342,6 +6428,7 @@
6342 <div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">6428 <div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
6343 <i class="fa-solid fa-circle-stop"></i>6429 <i class="fa-solid fa-circle-stop"></i>
6344 </div>6430 </div>
6431 <div id="mes_impersonate" class="fa-solid fa-user-secret interactable displayNone" title="Ask AI to write your message for you" data-i18n="[title]Ask AI to write your message for you" tabindex="0"></div>
6345 <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>6432 <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>
6346 <div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>6433 <div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
6347 </div>6434 </div>
public/locales/ar-sa.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "مخطط جيسون",207 "JSON Schema": "مخطط جيسون",
208 "Type in the desired JSON schema": "اكتب مخطط JSON المطلوب",208 "Type in the desired JSON schema": "اكتب مخطط JSON المطلوب",
209 "Grammar String": "سلسلة القواعد",209 "Grammar String": "سلسلة القواعد",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "يعتمد GNBF أو ENBF على الواجهة الخلفية المستخدمة. إذا كنت تستخدم هذا يجب أن تعرف أي.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "يعتمد GBNF أو EBNF على الواجهة الخلفية المستخدمة. إذا كنت تستخدم هذا يجب أن تعرف أي.",
211 "Top P & Min P": "أعلى ع وأدنى ص",211 "Top P & Min P": "أعلى ع وأدنى ص",
212 "Load default order": "تحميل الترتيب الافتراضي",212 "Load default order": "تحميل الترتيب الافتراضي",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp فقط. تحديد ترتيب أخذ العينات. إذا لم يكن وضع Mirostat 0، فسيتم تجاهل ترتيب أخذ العينات.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp فقط. تحديد ترتيب أخذ العينات. إذا لم يكن وضع Mirostat 0، فسيتم تجاهل ترتيب أخذ العينات.",
@@ -390,7 +390,7 @@
390 "Alt Method": "طريقة بديلة",390 "Alt Method": "طريقة بديلة",
391 "AI21 API Key": "مفتاح API لـ AI21",391 "AI21 API Key": "مفتاح API لـ AI21",
392 "AI21 Model": "نموذج AI21",392 "AI21 Model": "نموذج AI21",
393 "MakerSuite API Key": "مفتاح واجهة برمجة تطبيقات MakerSuite",393 "Google AI Studio API Key": "مفتاح واجهة برمجة تطبيقات Google AI Studio",
394 "Google Model": "نموذج جوجل",394 "Google Model": "نموذج جوجل",
395 "MistralAI API Key": "مفتاح واجهة برمجة التطبيقات MistralAI",395 "MistralAI API Key": "مفتاح واجهة برمجة التطبيقات MistralAI",
396 "MistralAI Model": "نموذج ميسترال آي آي",396 "MistralAI Model": "نموذج ميسترال آي آي",
public/locales/de-de.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "JSON-Schema",207 "JSON Schema": "JSON-Schema",
208 "Type in the desired JSON schema": "Geben Sie das gewünschte JSON-Schema ein",208 "Type in the desired JSON schema": "Geben Sie das gewünschte JSON-Schema ein",
209 "Grammar String": "Grammatikzeichenfolge",209 "Grammar String": "Grammatikzeichenfolge",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF oder ENBF, hängt vom verwendeten Backend ab. Wenn Sie dieses verwenden, sollten Sie wissen, welches.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF oder EBNF, hängt vom verwendeten Backend ab. Wenn Sie dieses verwenden, sollten Sie wissen, welches.",
211 "Top P & Min P": "Top P und Min P",211 "Top P & Min P": "Top P und Min P",
212 "Load default order": "Standardreihenfolge laden",212 "Load default order": "Standardreihenfolge laden",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Nur llama.cpp. Bestimmt die Reihenfolge der Sampler. Wenn der Mirostat-Modus nicht 0 ist, wird die Sampler-Reihenfolge ignoriert.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Nur llama.cpp. Bestimmt die Reihenfolge der Sampler. Wenn der Mirostat-Modus nicht 0 ist, wird die Sampler-Reihenfolge ignoriert.",
@@ -390,7 +390,7 @@
390 "Alt Method": "Alternative Methode",390 "Alt Method": "Alternative Methode",
391 "AI21 API Key": "AI21 API-Schlüssel",391 "AI21 API Key": "AI21 API-Schlüssel",
392 "AI21 Model": "AI21-Modell",392 "AI21 Model": "AI21-Modell",
393 "MakerSuite API Key": "MakerSuite API-Schlüssel",393 "Google AI Studio API Key": "Google AI Studio API-Schlüssel",
394 "Google Model": "Google-Modell",394 "Google Model": "Google-Modell",
395 "MistralAI API Key": "MistralAI API-Schlüssel",395 "MistralAI API Key": "MistralAI API-Schlüssel",
396 "MistralAI Model": "MistralAI-Modell",396 "MistralAI Model": "MistralAI-Modell",
public/locales/es-es.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "Esquema JSON",207 "JSON Schema": "Esquema JSON",
208 "Type in the desired JSON schema": "Escriba el esquema JSON deseado",208 "Type in the desired JSON schema": "Escriba el esquema JSON deseado",
209 "Grammar String": "Cadena de gramática",209 "Grammar String": "Cadena de gramática",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF o ENBF, depende del backend en uso. Si estás usando esto, debes saber cuál.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF o EBNF, depende del backend en uso. Si estás usando esto, debes saber cuál.",
211 "Top P & Min P": "P superior y P mínima",211 "Top P & Min P": "P superior y P mínima",
212 "Load default order": "Cargar orden predeterminado",212 "Load default order": "Cargar orden predeterminado",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp únicamente. Determina el orden de los muestreadores. Si el modo Mirostat no es 0, se ignora el orden de las muestras.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp únicamente. Determina el orden de los muestreadores. Si el modo Mirostat no es 0, se ignora el orden de las muestras.",
@@ -390,7 +390,7 @@
390 "Alt Method": "Método alternativo",390 "Alt Method": "Método alternativo",
391 "AI21 API Key": "Clave API de AI21",391 "AI21 API Key": "Clave API de AI21",
392 "AI21 Model": "Modelo de AI21",392 "AI21 Model": "Modelo de AI21",
393 "MakerSuite API Key": "Clave API de MakerSuite",393 "Google AI Studio API Key": "Clave API de Google AI Studio",
394 "Google Model": "Modelo de Google",394 "Google Model": "Modelo de Google",
395 "MistralAI API Key": "Clave API de MistralAI",395 "MistralAI API Key": "Clave API de MistralAI",
396 "MistralAI Model": "Modelo MistralAI",396 "MistralAI Model": "Modelo MistralAI",
public/locales/fr-fr.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "Schéma JSON",207 "JSON Schema": "Schéma JSON",
208 "Type in the desired JSON schema": "Tapez le schéma JSON souhaité",208 "Type in the desired JSON schema": "Tapez le schéma JSON souhaité",
209 "Grammar String": "Chaîne de grammaire",209 "Grammar String": "Chaîne de grammaire",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF ou ENBF dépend du backend utilisé. Si vous l'utilisez, vous devez savoir lequel.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF ou EBNF dépend du backend utilisé. Si vous l'utilisez, vous devez savoir lequel.",
211 "Top P & Min P": "P supérieur et P minimal",211 "Top P & Min P": "P supérieur et P minimal",
212 "Load default order": "Charger l'ordre par défaut",212 "Load default order": "Charger l'ordre par défaut",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "lama.cpp uniquement. Détermine l’ordre des échantillonneurs. Si le mode Mirostat n'est pas 0, l'ordre de l'échantillonneur est ignoré.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "lama.cpp uniquement. Détermine l’ordre des échantillonneurs. Si le mode Mirostat n'est pas 0, l'ordre de l'échantillonneur est ignoré.",
@@ -390,7 +390,7 @@
390 "Alt Method": "Méthode alternative",390 "Alt Method": "Méthode alternative",
391 "AI21 API Key": "Clé API AI21",391 "AI21 API Key": "Clé API AI21",
392 "AI21 Model": "Modèle AI21",392 "AI21 Model": "Modèle AI21",
393 "MakerSuite API Key": "Clé API MakerSuite",393 "Google AI Studio API Key": "Clé API Google AI Studio",
394 "Google Model": "Modèle Google",394 "Google Model": "Modèle Google",
395 "MistralAI API Key": "Clé API MistralAI",395 "MistralAI API Key": "Clé API MistralAI",
396 "MistralAI Model": "Modèle MistralAI",396 "MistralAI Model": "Modèle MistralAI",
public/locales/is-is.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "JSON kerfi",207 "JSON Schema": "JSON kerfi",
208 "Type in the desired JSON schema": "Sláðu inn æskilegt JSON skema",208 "Type in the desired JSON schema": "Sláðu inn æskilegt JSON skema",
209 "Grammar String": "Málfræðistrengur",209 "Grammar String": "Málfræðistrengur",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF eða ENBF, fer eftir bakendanum sem er í notkun. Ef þú ert að nota þetta ættir þú að vita hvaða.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF eða EBNF, fer eftir bakendanum sem er í notkun. Ef þú ert að nota þetta ættir þú að vita hvaða.",
211 "Top P & Min P": "Efstu P & Min P",211 "Top P & Min P": "Efstu P & Min P",
212 "Load default order": "Hlaða sjálfgefna röð",212 "Load default order": "Hlaða sjálfgefna röð",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp eingöngu. Ákveður röð sýnataka. Ef Mirostat hamur er ekki 0, er röð sýnatöku hunsuð.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp eingöngu. Ákveður röð sýnataka. Ef Mirostat hamur er ekki 0, er röð sýnatöku hunsuð.",
@@ -390,7 +390,7 @@
390 "Alt Method": "Aðferð Bakmenn",390 "Alt Method": "Aðferð Bakmenn",
391 "AI21 API Key": "Lykill API fyrir AI21",391 "AI21 API Key": "Lykill API fyrir AI21",
392 "AI21 Model": "AI21 Módel",392 "AI21 Model": "AI21 Módel",
393 "MakerSuite API Key": "MakerSuite API lykill",393 "Google AI Studio API Key": "Google AI Studio API lykill",
394 "Google Model": "Google líkan",394 "Google Model": "Google líkan",
395 "MistralAI API Key": "MistralAI API lykill",395 "MistralAI API Key": "MistralAI API lykill",
396 "MistralAI Model": "MistralAI líkan",396 "MistralAI Model": "MistralAI líkan",
public/locales/it-it.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "Schema JSON",207 "JSON Schema": "Schema JSON",
208 "Type in the desired JSON schema": "Digita lo schema JSON desiderato",208 "Type in the desired JSON schema": "Digita lo schema JSON desiderato",
209 "Grammar String": "Stringa grammaticale",209 "Grammar String": "Stringa grammaticale",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF o ENBF, dipende dal backend in uso. Se stai usando questo dovresti sapere quale.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF o EBNF, dipende dal backend in uso. Se stai usando questo dovresti sapere quale.",
211 "Top P & Min P": "P massimo e P minimo",211 "Top P & Min P": "P massimo e P minimo",
212 "Load default order": "Carica ordine predefinito",212 "Load default order": "Carica ordine predefinito",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Solo lama.cpp. Determina l'ordine dei campionatori. Se la modalità Mirostat non è 0, l'ordine del campionatore viene ignorato.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Solo lama.cpp. Determina l'ordine dei campionatori. Se la modalità Mirostat non è 0, l'ordine del campionatore viene ignorato.",
@@ -390,7 +390,7 @@
390 "Alt Method": "Metodo alternativo",390 "Alt Method": "Metodo alternativo",
391 "AI21 API Key": "Chiave API di AI21",391 "AI21 API Key": "Chiave API di AI21",
392 "AI21 Model": "Modello AI21",392 "AI21 Model": "Modello AI21",
393 "MakerSuite API Key": "Chiave API MakerSuite",393 "Google AI Studio API Key": "Chiave API Google AI Studio",
394 "Google Model": "Modello Google",394 "Google Model": "Modello Google",
395 "MistralAI API Key": "Chiave API MistralAI",395 "MistralAI API Key": "Chiave API MistralAI",
396 "MistralAI Model": "Modello MistralAI",396 "MistralAI Model": "Modello MistralAI",
public/locales/ja-jp.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "JSONスキーマ",207 "JSON Schema": "JSONスキーマ",
208 "Type in the desired JSON schema": "希望するJSONスキーマを入力します",208 "Type in the desired JSON schema": "希望するJSONスキーマを入力します",
209 "Grammar String": "文法文字列",209 "Grammar String": "文法文字列",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF または ENBF は、使用するバックエンドによって異なります。これを使用する場合は、どちらであるかを知っておく必要があります。",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF または EBNF は、使用するバックエンドによって異なります。これを使用する場合は、どちらであるかを知っておく必要があります。",
211 "Top P & Min P": "トップPと最小P",211 "Top P & Min P": "トップPと最小P",
212 "Load default order": "デフォルトの順序を読み込む",212 "Load default order": "デフォルトの順序を読み込む",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp のみ。サンプラーの順序を決定します。Mirostat モードが 0 でない場合、サンプラーの順序は無視されます。",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp のみ。サンプラーの順序を決定します。Mirostat モードが 0 でない場合、サンプラーの順序は無視されます。",
@@ -390,7 +390,7 @@
390 "Alt Method": "代替手法",390 "Alt Method": "代替手法",
391 "AI21 API Key": "AI21のAPIキー",391 "AI21 API Key": "AI21のAPIキー",
392 "AI21 Model": "AI21モデル",392 "AI21 Model": "AI21モデル",
393 "MakerSuite API Key": "MakerSuite APIキー",393 "Google AI Studio API Key": "Google AI Studio APIキー",
394 "Google Model": "Google モデル",394 "Google Model": "Google モデル",
395 "MistralAI API Key": "MistralAI API キー",395 "MistralAI API Key": "MistralAI API キー",
396 "MistralAI Model": "MistralAI モデル",396 "MistralAI Model": "MistralAI モデル",
public/locales/ko-kr.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "JSON 스키마",207 "JSON Schema": "JSON 스키마",
208 "Type in the desired JSON schema": "원하는 JSON 스키마를 입력하세요.",208 "Type in the desired JSON schema": "원하는 JSON 스키마를 입력하세요.",
209 "Grammar String": "문법 문자열",209 "Grammar String": "문법 문자열",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 또는 ENBF는 사용 중인 백엔드에 따라 다릅니다. 이것을 사용한다면 어느 것이 무엇인지 알아야합니다.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 또는 EBNF는 사용 중인 백엔드에 따라 다릅니다. 이것을 사용한다면 어느 것이 무엇인지 알아야합니다.",
211 "Top P & Min P": "상위 P 및 최소 P",211 "Top P & Min P": "상위 P 및 최소 P",
212 "Load default order": "기본 순서로 로드",212 "Load default order": "기본 순서로 로드",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp만 가능합니다. 샘플러의 순서를 결정합니다. Mirostat 모드가 0이 아닌 경우 샘플러 순서는 무시됩니다.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp만 가능합니다. 샘플러의 순서를 결정합니다. Mirostat 모드가 0이 아닌 경우 샘플러 순서는 무시됩니다.",
@@ -390,7 +390,7 @@
390 "Alt Method": "대체 방법",390 "Alt Method": "대체 방법",
391 "AI21 API Key": "AI21 API 키",391 "AI21 API Key": "AI21 API 키",
392 "AI21 Model": "AI21 모델",392 "AI21 Model": "AI21 모델",
393 "MakerSuite API Key": "MakerSuite API 키",393 "Google AI Studio API Key": "Google AI Studio API 키",
394 "Google Model": "구글 모델",394 "Google Model": "구글 모델",
395 "MistralAI API Key": "MistralAI API 키",395 "MistralAI API Key": "MistralAI API 키",
396 "MistralAI Model": "MistralAI 모델",396 "MistralAI Model": "MistralAI 모델",
public/locales/nl-nl.json+1 -1
@@ -207,7 +207,7 @@
207 "JSON Schema": "JSON-schema",207 "JSON Schema": "JSON-schema",
208 "Type in the desired JSON schema": "Typ het gewenste JSON-schema",208 "Type in the desired JSON schema": "Typ het gewenste JSON-schema",
209 "Grammar String": "Grammaticareeks",209 "Grammar String": "Grammaticareeks",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF of ENBF, hangt af van de gebruikte backend. Als u dit gebruikt, moet u weten welke.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF of EBNF, hangt af van de gebruikte backend. Als u dit gebruikt, moet u weten welke.",
211 "Top P & Min P": "Top P & Min P",211 "Top P & Min P": "Top P & Min P",
212 "Load default order": "Standaardvolgorde laden",212 "Load default order": "Standaardvolgorde laden",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "alleen lama.cpp. Bepaalt de volgorde van de samplers. Als de Mirostat-modus niet 0 is, wordt de samplervolgorde genegeerd.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "alleen lama.cpp. Bepaalt de volgorde van de samplers. Als de Mirostat-modus niet 0 is, wordt de samplervolgorde genegeerd.",
public/locales/pt-pt.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "Esquema JSON",207 "JSON Schema": "Esquema JSON",
208 "Type in the desired JSON schema": "Digite o esquema JSON desejado",208 "Type in the desired JSON schema": "Digite o esquema JSON desejado",
209 "Grammar String": "Cadeia de Gramática",209 "Grammar String": "Cadeia de Gramática",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF ou ENBF, depende do backend em uso. Se você estiver usando isso, você deve saber qual.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF ou EBNF, depende do backend em uso. Se você estiver usando isso, você deve saber qual.",
211 "Top P & Min P": "P superior e P mínimo",211 "Top P & Min P": "P superior e P mínimo",
212 "Load default order": "Carregar ordem padrão",212 "Load default order": "Carregar ordem padrão",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "apenas lhama.cpp. Determina a ordem dos amostradores. Se o modo Mirostat não for 0, a ordem do amostrador será ignorada.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "apenas lhama.cpp. Determina a ordem dos amostradores. Se o modo Mirostat não for 0, a ordem do amostrador será ignorada.",
@@ -390,7 +390,7 @@
390 "Alt Method": "Método Alternativo",390 "Alt Method": "Método Alternativo",
391 "AI21 API Key": "Chave da API AI21",391 "AI21 API Key": "Chave da API AI21",
392 "AI21 Model": "Modelo AI21",392 "AI21 Model": "Modelo AI21",
393 "MakerSuite API Key": "Chave API MakerSuite",393 "Google AI Studio API Key": "Chave API Google AI Studio",
394 "Google Model": "Modelo Google",394 "Google Model": "Modelo Google",
395 "MistralAI API Key": "Chave de API MistralAI",395 "MistralAI API Key": "Chave de API MistralAI",
396 "MistralAI Model": "Modelo MistralAI",396 "MistralAI Model": "Modelo MistralAI",
public/locales/ru-ru.json+2 -2
@@ -722,7 +722,7 @@
722 "Proxy Server URL": "Адрес прокси-сервера",722 "Proxy Server URL": "Адрес прокси-сервера",
723 "MistralAI Model": "Модель MistralAI",723 "MistralAI Model": "Модель MistralAI",
724 "MistralAI API Key": "Ключ от API MistralAI",724 "MistralAI API Key": "Ключ от API MistralAI",
725 "MakerSuite API Key": "Ключ от API MakerSuite",725 "Google AI Studio API Key": "Ключ от API Google AI Studio",
726 "Google Model": "Модель Google",726 "Google Model": "Модель Google",
727 "Cohere API Key": "Ключ от API Cohere",727 "Cohere API Key": "Ключ от API Cohere",
728 "Cohere Model": "Модель Cohere",728 "Cohere Model": "Модель Cohere",
@@ -978,7 +978,7 @@
978 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",978 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
979 "char_import_7": ")",979 "char_import_7": ")",
980 "Grammar String": "Грамматика",980 "Grammar String": "Грамматика",
981 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF или ENBF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",981 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
982 "Account": "Аккаунт",982 "Account": "Аккаунт",
983 "Hi,": "Привет,",983 "Hi,": "Привет,",
984 "To enable multi-account features, restart the SillyTavern server with": "Чтобы активировать систему аккаунтов, перезапустите SillyTavern, выставив",984 "To enable multi-account features, restart the SillyTavern server with": "Чтобы активировать систему аккаунтов, перезапустите SillyTavern, выставив",
public/locales/uk-ua.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "Схема JSON",207 "JSON Schema": "Схема JSON",
208 "Type in the desired JSON schema": "Введіть потрібну схему JSON",208 "Type in the desired JSON schema": "Введіть потрібну схему JSON",
209 "Grammar String": "Граматичний рядок",209 "Grammar String": "Граматичний рядок",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF або ENBF, залежить від серверної частини, яка використовується. Якщо ви використовуєте це, ви повинні знати, який.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF або EBNF, залежить від серверної частини, яка використовується. Якщо ви використовуєте це, ви повинні знати, який.",
211 "Top P & Min P": "Верхній P & Min P",211 "Top P & Min P": "Верхній P & Min P",
212 "Load default order": "Завантажити типовий порядок",212 "Load default order": "Завантажити типовий порядок",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "лише llama.cpp. Визначає порядок пробовідбірників. Якщо режим Mirostat не 0, порядок вибірки ігнорується.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "лише llama.cpp. Визначає порядок пробовідбірників. Якщо режим Mirostat не 0, порядок вибірки ігнорується.",
@@ -390,7 +390,7 @@
390 "Alt Method": "Альтернативний метод",390 "Alt Method": "Альтернативний метод",
391 "AI21 API Key": "Ключ API для AI21",391 "AI21 API Key": "Ключ API для AI21",
392 "AI21 Model": "Модель AI21",392 "AI21 Model": "Модель AI21",
393 "MakerSuite API Key": "Ключ API MakerSuite",393 "Google AI Studio API Key": "Ключ API Google AI Studio",
394 "Google Model": "Модель Google",394 "Google Model": "Модель Google",
395 "MistralAI API Key": "Ключ API MistralAI",395 "MistralAI API Key": "Ключ API MistralAI",
396 "MistralAI Model": "Модель MistralAI",396 "MistralAI Model": "Модель MistralAI",
public/locales/vi-vn.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "Lược đồ JSON",207 "JSON Schema": "Lược đồ JSON",
208 "Type in the desired JSON schema": "Nhập lược đồ JSON mong muốn",208 "Type in the desired JSON schema": "Nhập lược đồ JSON mong muốn",
209 "Grammar String": "Chuỗi ngữ pháp",209 "Grammar String": "Chuỗi ngữ pháp",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF hoặc ENBF, tùy thuộc vào backend đang sử dụng. Nếu bạn đang sử dụng cái này, bạn nên biết cái nào.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF hoặc EBNF, tùy thuộc vào backend đang sử dụng. Nếu bạn đang sử dụng cái này, bạn nên biết cái nào.",
211 "Top P & Min P": "P & P tối thiểu hàng đầu",211 "Top P & Min P": "P & P tối thiểu hàng đầu",
212 "Load default order": "Tải thứ tự mặc định",212 "Load default order": "Tải thứ tự mặc định",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "chỉ llama.cpp. Xác định thứ tự lấy mẫu. Nếu chế độ Mirostat khác 0, thứ tự lấy mẫu sẽ bị bỏ qua.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "chỉ llama.cpp. Xác định thứ tự lấy mẫu. Nếu chế độ Mirostat khác 0, thứ tự lấy mẫu sẽ bị bỏ qua.",
@@ -390,7 +390,7 @@
390 "Alt Method": "Phương pháp thay thế",390 "Alt Method": "Phương pháp thay thế",
391 "AI21 API Key": "Khóa API của AI21",391 "AI21 API Key": "Khóa API của AI21",
392 "AI21 Model": "Mô hình AI21",392 "AI21 Model": "Mô hình AI21",
393 "MakerSuite API Key": "Khóa API MakerSuite",393 "Google AI Studio API Key": "Khóa API Google AI Studio",
394 "Google Model": "Mô hình Google",394 "Google Model": "Mô hình Google",
395 "MistralAI API Key": "Khóa API MistralAI",395 "MistralAI API Key": "Khóa API MistralAI",
396 "MistralAI Model": "Mô hình MistralAI",396 "MistralAI Model": "Mô hình MistralAI",
public/locales/zh-cn.json+22 -5
@@ -208,9 +208,10 @@
208 "JSON Schema": "JSON 结构",208 "JSON Schema": "JSON 结构",
209 "Type in the desired JSON schema": "输入所需的 JSON 结构",209 "Type in the desired JSON schema": "输入所需的 JSON 结构",
210 "Grammar String": "语法字符串",210 "Grammar String": "语法字符串",
211 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 或 ENBF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",211 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
212 "Top P & Min P": "Top P 和 Min P",212 "Top P & Min P": "Top P 和 Min P",
213 "Load default order": "加载默认顺序",213 "Load default order": "加载默认顺序",
214 "Sampler Order": "取样器顺序",
214 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。",215 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。",
215 "Sampler Priority": "采样器优先级",216 "Sampler Priority": "采样器优先级",
216 "Ooba only. Determines the order of samplers.": "确定采样器的顺序(仅适用于Ooba)",217 "Ooba only. Determines the order of samplers.": "确定采样器的顺序(仅适用于Ooba)",
@@ -405,7 +406,7 @@
405 "Alt Method": "备用方法",406 "Alt Method": "备用方法",
406 "AI21 API Key": "AI21 API 密钥",407 "AI21 API Key": "AI21 API 密钥",
407 "AI21 Model": "AI21 模型",408 "AI21 Model": "AI21 模型",
408 "MakerSuite API Key": "MakerSuite API 密钥",409 "Google AI Studio API Key": "Google AI Studio API 密钥",
409 "Google Model": "Google 模型",410 "Google Model": "Google 模型",
410 "MistralAI API Key": "MistralAI API 密钥",411 "MistralAI API Key": "MistralAI API 密钥",
411 "MistralAI Model": "MistralAI 模型",412 "MistralAI Model": "MistralAI 模型",
@@ -443,7 +444,9 @@
443 "Example Separator": "示例分隔符",444 "Example Separator": "示例分隔符",
444 "Chat Start": "聊天开始",445 "Chat Start": "聊天开始",
445 "Add Chat Start and Example Separator to a list of stopping strings.": "将聊天开始和示例分隔符添加到停止字符串列表中。",446 "Add Chat Start and Example Separator to a list of stopping strings.": "将聊天开始和示例分隔符添加到停止字符串列表中。",
446 "Use as Stop Strings": "用作停止字符串",447 "Separators as Stop Strings": "分隔符作为终止字符串",
448 "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",
449 "Names as Stop Strings": "名称作为终止字符串",
447 "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。",450 "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。",
448 "Allow Post-History Instructions": "允许后历史说明",451 "Allow Post-History Instructions": "允许后历史说明",
449 "Context Order": "上下文顺序",452 "Context Order": "上下文顺序",
@@ -704,10 +707,10 @@
704 "Restore User Input": "恢复用户输入",707 "Restore User Input": "恢复用户输入",
705 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",708 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",
706 "Movable UI Panels": "可移动 UI 面板",709 "Movable UI Panels": "可移动 UI 面板",
710 "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
707 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",711 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",
708 "MUI Preset": "可移动 UI 预设",712 "MUI Preset": "可移动 UI 预设",
709 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",713 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",
710 "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
711 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",714 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",
712 "Custom CSS": "自定义 CSS",715 "Custom CSS": "自定义 CSS",
713 "Expand the editor": "展开编辑器",716 "Expand the editor": "展开编辑器",
@@ -727,6 +730,8 @@
727 "Press Send to continue": "按发送键以继续",730 "Press Send to continue": "按发送键以继续",
728 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",731 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",
729 "Quick 'Continue' button": "快速“继续”按钮",732 "Quick 'Continue' button": "快速“继续”按钮",
733 "Show a button in the input area to ask the AI to impersonate your character for a single message": "在输入区域中显示一个按钮,让 AI 模仿你的角色发送一条消息。",
734 "Quick 'Impersonate' button": "快速“模仿”按钮",
730 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",735 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",
731 "Swipes": "刷新回复按钮",736 "Swipes": "刷新回复按钮",
732 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",737 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",
@@ -772,6 +777,10 @@
772 "Autocomplete Style": "风格",777 "Autocomplete Style": "风格",
773 "Follow Theme": "关注主题",778 "Follow Theme": "关注主题",
774 "Dark": "黑暗的",779 "Dark": "黑暗的",
780 "Keyboard": "键盘:",
781 "Select with Tab or Enter": "使用 Tab 或 Enter 选择",
782 "Select with Tab": "使用 Tab 选择",
783 "Select with Enter": "按 Enter 键选择",
775 "Sets the font size of the autocomplete.": "设置自动完成的字体大小。",784 "Sets the font size of the autocomplete.": "设置自动完成的字体大小。",
776 "Sets the width of the autocomplete.": "设置自动完成的宽度。",785 "Sets the width of the autocomplete.": "设置自动完成的宽度。",
777 "Autocomplete Width": "宽度",786 "Autocomplete Width": "宽度",
@@ -1176,6 +1185,7 @@
1176 "Pause script execution": "暂停执行脚本",1185 "Pause script execution": "暂停执行脚本",
1177 "Abort script execution": "中止执行脚本",1186 "Abort script execution": "中止执行脚本",
1178 "Abort request": "中止请求",1187 "Abort request": "中止请求",
1188 "Ask AI to write your message for you": "让AI为您撰写消息",
1179 "Continue the last message": "继续上一条消息",1189 "Continue the last message": "继续上一条消息",
1180 "Send a message": "发送消息",1190 "Send a message": "发送消息",
1181 "Close chat": "关闭聊天",1191 "Close chat": "关闭聊天",
@@ -1187,7 +1197,6 @@
1187 "Manage chat files": "管理聊天文件",1197 "Manage chat files": "管理聊天文件",
1188 "Delete messages": "删除消息",1198 "Delete messages": "删除消息",
1189 "Regenerate": "重新生成",1199 "Regenerate": "重新生成",
1190 "Ask AI to write your message for you": "请求AI为您撰写消息",
1191 "Impersonate": "AI 帮答",1200 "Impersonate": "AI 帮答",
1192 "Continue": "继续",1201 "Continue": "继续",
1193 "Bind user name to that avatar": "将用户名称绑定到该头像",1202 "Bind user name to that avatar": "将用户名称绑定到该头像",
@@ -1341,6 +1350,7 @@
1341 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",1350 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",
1342 "Labels and Message": "标签和信息",1351 "Labels and Message": "标签和信息",
1343 "Label": "标签",1352 "Label": "标签",
1353 "(label of the button, if no icon is chosen) ": "(如果没有选择图标,则为按钮的标签)",
1344 "Title": "标题",1354 "Title": "标题",
1345 "(tooltip, leave empty to show message or /command)": "(工具提示,留空以显示消息或/命令)",1355 "(tooltip, leave empty to show message or /command)": "(工具提示,留空以显示消息或/命令)",
1346 "Message / Command:": "消息/命令:",1356 "Message / Command:": "消息/命令:",
@@ -1371,6 +1381,8 @@
1371 "Inject user input automatically": "自动注入用户输入",1381 "Inject user input automatically": "自动注入用户输入",
1372 "(if disabled, use ": "(如果禁用,使用",1382 "(if disabled, use ": "(如果禁用,使用",
1373 "macro for manual injection)": "宏用于手动注入)",1383 "macro for manual injection)": "宏用于手动注入)",
1384 "Color": "颜色",
1385 "Only apply color as accent": "仅应用颜色作为强调",
1374 "ext_regex_title": "正则",1386 "ext_regex_title": "正则",
1375 "ext_regex_new_global_script_desc": "新的全局正则表达式脚本",1387 "ext_regex_new_global_script_desc": "新的全局正则表达式脚本",
1376 "ext_regex_new_global_script": "新建全局正则",1388 "ext_regex_new_global_script": "新建全局正则",
@@ -1419,6 +1431,7 @@
1419 "ext_regex_export_script": "导出脚本",1431 "ext_regex_export_script": "导出脚本",
1420 "ext_regex_delete_script": "删除脚本",1432 "ext_regex_delete_script": "删除脚本",
1421 "Trigger Stable Diffusion": "触发Stable Diffusion",1433 "Trigger Stable Diffusion": "触发Stable Diffusion",
1434 "Abort current image generation task": "中止当前图像生成",
1422 "sd_Yourself": "你自己",1435 "sd_Yourself": "你自己",
1423 "sd_Your_Face": "你的脸",1436 "sd_Your_Face": "你的脸",
1424 "sd_Me": "我",1437 "sd_Me": "我",
@@ -1572,6 +1585,10 @@
1572 "Only used when Main API is selected.": "仅在选择主 API 时使用。",1585 "Only used when Main API is selected.": "仅在选择主 API 时使用。",
1573 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",1586 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",
1574 "View Stats": "查看统计数据",1587 "View Stats": "查看统计数据",
1588 "Title/Memo": "标题/备忘录",
1589 "Status": "状态",
1590 "Position": "位置",
1591 "Trigger %": "触发率 %",
1575 "Manager Users": "管理用户",1592 "Manager Users": "管理用户",
1576 "New User": "新用户",1593 "New User": "新用户",
1577 "Status:": "地位:",1594 "Status:": "地位:",
public/locales/zh-tw.json+2 -2
@@ -208,7 +208,7 @@
208 "JSON Schema": "JSON 結構",208 "JSON Schema": "JSON 結構",
209 "Type in the desired JSON schema": "輸入所需的 JSON 結構",209 "Type in the desired JSON schema": "輸入所需的 JSON 結構",
210 "Grammar String": "語法字串",210 "Grammar String": "語法字串",
211 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 或 ENBF,取決於所使用的後端。如果您使用此功能,應該知道是哪一種",211 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取決於所使用的後端。如果您使用此功能,應該知道是哪一種",
212 "Top P & Min P": "Top P 和 Min P",212 "Top P & Min P": "Top P 和 Min P",
213 "Load default order": "載入預設順序",213 "Load default order": "載入預設順序",
214 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。",214 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。",
@@ -391,7 +391,7 @@
391 "Alt Method": "替代方法",391 "Alt Method": "替代方法",
392 "AI21 API Key": "AI21 API 金鑰",392 "AI21 API Key": "AI21 API 金鑰",
393 "AI21 Model": "AI21 模型",393 "AI21 Model": "AI21 模型",
394 "MakerSuite API Key": "MakerSuite API 金鑰",394 "Google AI Studio API Key": "Google AI Studio API 金鑰",
395 "Google Model": "Google 模型",395 "Google Model": "Google 模型",
396 "MistralAI API Key": "MistralAI API 金鑰",396 "MistralAI API Key": "MistralAI API 金鑰",
397 "MistralAI Model": "MistralAI 模型",397 "MistralAI Model": "MistralAI 模型",
public/script.js+202 -103
@@ -156,6 +156,7 @@ import {
156 ensureImageFormatSupported,156 ensureImageFormatSupported,
157 flashHighlight,157 flashHighlight,
158 isTrueBoolean,158 isTrueBoolean,
159 toggleDrawer,
159} from './scripts/utils.js';160} from './scripts/utils.js';
160import { debounce_timeout } from './scripts/constants.js';161import { debounce_timeout } from './scripts/constants.js';
161162
@@ -224,10 +225,10 @@ import {
224import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';225import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
225import { hideLoader, showLoader } from './scripts/loader.js';226import { hideLoader, showLoader } from './scripts/loader.js';
226import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';227import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
227import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js';228import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels } from './scripts/textgen-models.js';
228import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';229import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
229import { initPresetManager } from './scripts/preset-manager.js';230import { initPresetManager } from './scripts/preset-manager.js';
230import { MacrosParser, evaluateMacros } from './scripts/macros.js';231import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros.js';
231import { currentUser, setUserControls } from './scripts/user.js';232import { currentUser, setUserControls } from './scripts/user.js';
232import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';233import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
233import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';234import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
@@ -241,7 +242,7 @@ import { DragAndDropHandler } from './scripts/dragdrop.js';
241import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';242import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
242import { initDynamicStyles } from './scripts/dynamic-styles.js';243import { initDynamicStyles } from './scripts/dynamic-styles.js';
243import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';244import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
244import { enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';245import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
245246
246//exporting functions and vars for mods247//exporting functions and vars for mods
247export {248export {
@@ -424,6 +425,8 @@ export const event_types = {
424 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',425 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
425 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',426 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
426 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',427 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',
428 OAI_PRESET_EXPORT_READY: 'oai_preset_export_ready',
429 OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready',
427 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',430 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
428 WORLDINFO_UPDATED: 'worldinfo_updated',431 WORLDINFO_UPDATED: 'worldinfo_updated',
429 CHARACTER_EDITED: 'character_edited',432 CHARACTER_EDITED: 'character_edited',
@@ -439,6 +442,7 @@ export const event_types = {
439 GROUP_CHAT_CREATED: 'group_chat_created',442 GROUP_CHAT_CREATED: 'group_chat_created',
440 GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',443 GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
441 GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',444 GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
445 GENERATE_AFTER_DATA: 'generate_after_data',
442 GROUP_MEMBER_DRAFTED: 'group_member_drafted',446 GROUP_MEMBER_DRAFTED: 'group_member_drafted',
443 WORLD_INFO_ACTIVATED: 'world_info_activated',447 WORLD_INFO_ACTIVATED: 'world_info_activated',
444 TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',448 TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
@@ -455,6 +459,7 @@ export const event_types = {
455 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',459 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
456 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',460 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
457 ONLINE_STATUS_CHANGED: 'online_status_changed',461 ONLINE_STATUS_CHANGED: 'online_status_changed',
462 IMAGE_SWIPED: 'image_swiped',
458};463};
459464
460export const eventSource = new EventEmitter();465export const eventSource = new EventEmitter();
@@ -910,6 +915,7 @@ async function firstLoadInit() {
910 await readSecretState();915 await readSecretState();
911 initLocales();916 initLocales();
912 initDefaultSlashCommands();917 initDefaultSlashCommands();
918 initTextGenModels();
913 await getSystemMessages();919 await getSystemMessages();
914 sendSystemMessage(system_message_types.WELCOME);920 sendSystemMessage(system_message_types.WELCOME);
915 await getSettings();921 await getSettings();
@@ -1720,16 +1726,24 @@ export async function replaceCurrentChat() {
1720}1726}
17211727
1722export function showMoreMessages() {1728export function showMoreMessages() {
1723 let messageId = Number($('#chat').children('.mes').first().attr('mesid'));1729 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
1730 let messageId = Number(firstDisplayedMesId);
1724 let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER;1731 let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
17251732
1733 // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id,
1734 // so the first "new" message being shown will be the last available message
1735 if (isNaN(messageId)) {
1736 messageId = getLastMessageId() + 1;
1737 }
1738
1726 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);1739 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
1727 const prevHeight = $('#chat').prop('scrollHeight');1740 const prevHeight = $('#chat').prop('scrollHeight');
17281741
1729 while (messageId > 0 && count > 0) {1742 while (messageId > 0 && count > 0) {
1743 let newMessageId = messageId - 1;
1744 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId });
1730 count--;1745 count--;
1731 messageId--;1746 messageId--;
1732 addOneMessage(chat[messageId], { insertBefore: messageId + 1, scroll: false, forceId: messageId });
1733 }1747 }
17341748
1735 if (messageId == 0) {1749 if (messageId == 0) {
@@ -1865,7 +1879,12 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
1865 }1879 }
18661880
1867 if (Number(messageId) === 0 && !isSystem && !isUser) {1881 if (Number(messageId) === 0 && !isSystem && !isUser) {
1882 const mesBeforeReplace = mes;
1883 const chatMessage = chat[messageId];
1868 mes = substituteParams(mes, undefined, ch_name);1884 mes = substituteParams(mes, undefined, ch_name);
1885 if (chatMessage && chatMessage.mes === mesBeforeReplace && chatMessage.extra?.display_text !== mesBeforeReplace) {
1886 chatMessage.mes = mes;
1887 }
1869 }1888 }
18701889
1871 mesForShowdownParse = mes;1890 mesForShowdownParse = mes;
@@ -2099,6 +2118,7 @@ export function updateMessageBlock(messageId, message) {
2099export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {2118export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
2100 // Add image to message2119 // Add image to message
2101 if (mes.extra?.image) {2120 if (mes.extra?.image) {
2121 const container = messageElement.find('.mes_img_container');
2102 const chatHeight = $('#chat').prop('scrollHeight');2122 const chatHeight = $('#chat').prop('scrollHeight');
2103 const image = messageElement.find('.mes_img');2123 const image = messageElement.find('.mes_img');
2104 const text = messageElement.find('.mes_text');2124 const text = messageElement.find('.mes_text');
@@ -2114,9 +2134,27 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
2114 });2134 });
2115 image.attr('src', mes.extra?.image);2135 image.attr('src', mes.extra?.image);
2116 image.attr('title', mes.extra?.title || mes.title || '');2136 image.attr('title', mes.extra?.title || mes.title || '');
2117 messageElement.find('.mes_img_container').addClass('img_extra');2137 container.addClass('img_extra');
2118 image.toggleClass('img_inline', isInline);2138 image.toggleClass('img_inline', isInline);
2119 text.toggleClass('displayNone', !isInline);2139 text.toggleClass('displayNone', !isInline);
2140
2141 const imageSwipes = mes.extra.image_swipes;
2142 if (Array.isArray(imageSwipes) && imageSwipes.length > 0) {
2143 container.addClass('img_swipes');
2144 const counter = container.find('.mes_img_swipe_counter');
2145 const currentImage = imageSwipes.indexOf(mes.extra.image) + 1;
2146 counter.text(`${currentImage}/${imageSwipes.length}`);
2147
2148 const swipeLeft = container.find('.mes_img_swipe_left');
2149 swipeLeft.off('click').on('click', function () {
2150 eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'left' });
2151 });
2152
2153 const swipeRight = container.find('.mes_img_swipe_right');
2154 swipeRight.off('click').on('click', function () {
2155 eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' });
2156 });
2157 }
2120 }2158 }
21212159
2122 // Add file to message2160 // Add file to message
@@ -2470,26 +2508,30 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
2470 * @returns {string[]} Array of stopping strings2508 * @returns {string[]} Array of stopping strings
2471 */2509 */
2472export function getStoppingStrings(isImpersonate, isContinue) {2510export function getStoppingStrings(isImpersonate, isContinue) {
2473 const charString = `\n${name2}:`;2511 const result = [];
2474 const userString = `\n${name1}:`;
2475 const result = isImpersonate ? [charString] : [userString];
24762512
2477 result.push(userString);2513 if (power_user.context.names_as_stop_strings) {
2514 const charString = `\n${name2}:`;
2515 const userString = `\n${name1}:`;
2516 result.push(isImpersonate ? charString : userString);
24782517
2479 if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) {2518 result.push(userString);
2480 result.push(charString);
2481 }
24822519
2483 // Add other group members as the stopping strings2520 if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) {
2484 if (selected_group) {2521 result.push(charString);
2485 const group = groups.find(x => x.id === selected_group);2522 }
24862523
2487 if (group && Array.isArray(group.members)) {2524 // Add group members as stopping strings if generating for a specific group member or user. (Allow slash commands to work around name stopping string restrictions)
2488 const names = group.members2525 if (selected_group && (name2 || isImpersonate)) {
2489 .map(x => characters.find(y => y.avatar == x))2526 const group = groups.find(x => x.id === selected_group);
2490 .filter(x => x && x.name && x.name !== name2)2527
2491 .map(x => `\n${x.name}:`);2528 if (group && Array.isArray(group.members)) {
2492 result.push(...names);2529 const names = group.members
2530 .map(x => characters.find(y => y.avatar == x))
2531 .filter(x => x && x.name && x.name !== name2)
2532 .map(x => `\n${x.name}:`);
2533 result.push(...names);
2534 }
2493 }2535 }
2494 }2536 }
24952537
@@ -2802,7 +2844,14 @@ function hideStopButton() {
2802}2844}
28032845
2804class StreamingProcessor {2846class StreamingProcessor {
2805 constructor(type, force_name2, timeStarted, messageAlreadyGenerated) {2847 /**
2848 * Creates a new streaming processor.
2849 * @param {string} type Generation type
2850 * @param {boolean} forceName2 If true, force the use of name2
2851 * @param {Date} timeStarted Date when generation was started
2852 * @param {string} continueMessage Previous message if the type is 'continue'
2853 */
2854 constructor(type, forceName2, timeStarted, continueMessage) {
2806 this.result = '';2855 this.result = '';
2807 this.messageId = -1;2856 this.messageId = -1;
2808 this.messageDom = null;2857 this.messageDom = null;
@@ -2812,14 +2861,14 @@ class StreamingProcessor {
2812 /** @type {HTMLTextAreaElement} */2861 /** @type {HTMLTextAreaElement} */
2813 this.sendTextarea = document.querySelector('#send_textarea');2862 this.sendTextarea = document.querySelector('#send_textarea');
2814 this.type = type;2863 this.type = type;
2815 this.force_name2 = force_name2;2864 this.force_name2 = forceName2;
2816 this.isStopped = false;2865 this.isStopped = false;
2817 this.isFinished = false;2866 this.isFinished = false;
2818 this.generator = this.nullStreamingGeneration;2867 this.generator = this.nullStreamingGeneration;
2819 this.abortController = new AbortController();2868 this.abortController = new AbortController();
2820 this.firstMessageText = '...';2869 this.firstMessageText = '...';
2821 this.timeStarted = timeStarted;2870 this.timeStarted = timeStarted;
2822 this.messageAlreadyGenerated = messageAlreadyGenerated;2871 this.continueMessage = type === 'continue' ? continueMessage : '';
2823 this.swipes = [];2872 this.swipes = [];
2824 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */2873 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
2825 this.messageLogprobs = [];2874 this.messageLogprobs = [];
@@ -2972,8 +3021,7 @@ class StreamingProcessor {
2972 await eventSource.emit(event_types.IMPERSONATE_READY, text);3021 await eventSource.emit(event_types.IMPERSONATE_READY, text);
2973 }3022 }
29743023
2975 const continueMsg = this.type === 'continue' ? this.messageAlreadyGenerated : undefined;3024 saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
2976 saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), continueMsg);
2977 await saveChatConditional();3025 await saveChatConditional();
2978 unblockGeneration();3026 unblockGeneration();
2979 generatedPromptCache = '';3027 generatedPromptCache = '';
@@ -3069,7 +3117,7 @@ class StreamingProcessor {
3069 if (logprobs) {3117 if (logprobs) {
3070 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3118 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3071 }3119 }
3072 await sw.tick(() => this.onProgressStreaming(this.messageId, this.messageAlreadyGenerated + text));3120 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
3073 }3121 }
3074 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;3122 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
3075 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);3123 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
@@ -3262,8 +3310,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3262 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';3310 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
3263 const isImpersonate = type == 'impersonate';3311 const isImpersonate = type == 'impersonate';
32643312
3265 let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
3266
3267 if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {3313 if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
3268 const interruptedByCommand = await processCommands(String($('#send_textarea').val()));3314 const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
32693315
@@ -3702,7 +3748,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3702 let oaiMessageExamples = [];3748 let oaiMessageExamples = [];
37033749
3704 if (main_api === 'openai') {3750 if (main_api === 'openai') {
3705 message_already_generated = '';
3706 oaiMessages = setOpenAIMessages(coreChat);3751 oaiMessages = setOpenAIMessages(coreChat);
3707 oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray);3752 oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray);
3708 }3753 }
@@ -3845,7 +3890,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3845 cyclePrompt += oai_settings.continue_postfix;3890 cyclePrompt += oai_settings.continue_postfix;
3846 continue_mag += oai_settings.continue_postfix;3891 continue_mag += oai_settings.continue_postfix;
3847 }3892 }
3848 message_already_generated = continue_mag;
3849 }3893 }
38503894
3851 const originalType = type;3895 const originalType = type;
@@ -3930,7 +3974,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
39303974
3931 // Get instruct mode line3975 // Get instruct mode line
3932 if (isInstruct && !isContinue) {3976 if (isInstruct && !isContinue) {
3933 const name = (quiet_prompt && !quietToLoud) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);3977 const name = (quiet_prompt && !quietToLoud && !isImpersonate) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
3934 const isQuiet = quiet_prompt && type == 'quiet';3978 const isQuiet = quiet_prompt && type == 'quiet';
3935 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);3979 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);
3936 }3980 }
@@ -4211,6 +4255,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4211 }4255 }
4212 }4256 }
42134257
4258 await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data);
4259
4214 if (dryRun) {4260 if (dryRun) {
4215 generatedPromptCache = '';4261 generatedPromptCache = '';
4216 return Promise.resolve();4262 return Promise.resolve();
@@ -4270,7 +4316,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4270 console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);4316 console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);
42714317
4272 if (isStreamingEnabled() && type !== 'quiet') {4318 if (isStreamingEnabled() && type !== 'quiet') {
4273 streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, message_already_generated);4319 streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, continue_mag);
4274 if (isContinue) {4320 if (isContinue) {
4275 // Save reply does add cycle text to the prompt, so it's not needed here4321 // Save reply does add cycle text to the prompt, so it's not needed here
4276 streamingProcessor.firstMessageText = '';4322 streamingProcessor.firstMessageText = '';
@@ -5074,7 +5120,7 @@ function setInContextMessages(lastmsg, type) {
5074 * @param {object} data Generation data5120 * @param {object} data Generation data
5075 * @returns {Promise<object>} Response data from the API5121 * @returns {Promise<object>} Response data from the API
5076 */5122 */
5077async function sendGenerationRequest(type, data) {5123export async function sendGenerationRequest(type, data) {
5078 if (main_api === 'openai') {5124 if (main_api === 'openai') {
5079 return await sendOpenAIRequest(type, data.prompt, abortController.signal);5125 return await sendOpenAIRequest(type, data.prompt, abortController.signal);
5080 }5126 }
@@ -5106,7 +5152,7 @@ async function sendGenerationRequest(type, data) {
5106 * @param {object} data Generation data5152 * @param {object} data Generation data
5107 * @returns {Promise<any>} Streaming generator5153 * @returns {Promise<any>} Streaming generator
5108 */5154 */
5109async function sendStreamingRequest(type, data) {5155export async function sendStreamingRequest(type, data) {
5110 if (abortController?.signal?.aborted) {5156 if (abortController?.signal?.aborted) {
5111 throw new Error('Generation was aborted.');5157 throw new Error('Generation was aborted.');
5112 }5158 }
@@ -5387,9 +5433,11 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5387 getMessage = fixMarkdown(getMessage, false);5433 getMessage = fixMarkdown(getMessage, false);
5388 }5434 }
53895435
5390 const nameToTrim2 = isImpersonate ? name1 : name2;5436 const nameToTrim2 = isImpersonate
5437 ? (!power_user.allow_name1_display ? name1 : '')
5438 : (!power_user.allow_name2_display ? name2 : '');
53915439
5392 if (getMessage.startsWith(nameToTrim2 + ':')) {5440 if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) {
5393 getMessage = getMessage.replace(nameToTrim2 + ':', '');5441 getMessage = getMessage.replace(nameToTrim2 + ':', '');
5394 getMessage = getMessage.trimStart();5442 getMessage = getMessage.trimStart();
5395 }5443 }
@@ -5610,6 +5658,7 @@ export function activateSendButtons() {
5610 is_send_press = false;5658 is_send_press = false;
5611 $('#send_but').removeClass('displayNone');5659 $('#send_but').removeClass('displayNone');
5612 $('#mes_continue').removeClass('displayNone');5660 $('#mes_continue').removeClass('displayNone');
5661 $('#mes_impersonate').removeClass('displayNone');
5613 $('.mes_buttons:last').show();5662 $('.mes_buttons:last').show();
5614 hideStopButton();5663 hideStopButton();
5615}5664}
@@ -5617,6 +5666,7 @@ export function activateSendButtons() {
5617export function deactivateSendButtons() {5666export function deactivateSendButtons() {
5618 $('#send_but').addClass('displayNone');5667 $('#send_but').addClass('displayNone');
5619 $('#mes_continue').addClass('displayNone');5668 $('#mes_continue').addClass('displayNone');
5669 $('#mes_impersonate').addClass('displayNone');
5620 showStopButton();5670 showStopButton();
5621}5671}
56225672
@@ -6407,7 +6457,7 @@ export async function getSettings() {
6407 loadHordeSettings(settings);6457 loadHordeSettings(settings);
64086458
6409 // Load power user settings6459 // Load power user settings
6410 loadPowerUserSettings(settings, data);6460 await loadPowerUserSettings(settings, data);
64116461
6412 // Load character tags6462 // Load character tags
6413 loadTagsSettings(settings);6463 loadTagsSettings(settings);
@@ -7917,6 +7967,8 @@ window['SillyTavern'].getContext = function () {
7917 eventTypes: event_types,7967 eventTypes: event_types,
7918 addOneMessage: addOneMessage,7968 addOneMessage: addOneMessage,
7919 generate: Generate,7969 generate: Generate,
7970 sendStreamingRequest: sendStreamingRequest,
7971 sendGenerationRequest: sendGenerationRequest,
7920 stopGeneration: stopGeneration,7972 stopGeneration: stopGeneration,
7921 getTokenCount: getTokenCount,7973 getTokenCount: getTokenCount,
7922 extensionPrompts: extension_prompts,7974 extensionPrompts: extension_prompts,
@@ -8334,6 +8386,12 @@ const CONNECT_API_MAP = {
8334 button: '#api_button_openai',8386 button: '#api_button_openai',
8335 source: chat_completion_sources.OPENAI,8387 source: chat_completion_sources.OPENAI,
8336 },8388 },
8389 // Google alias
8390 'google': {
8391 selected: 'openai',
8392 button: '#api_button_openai',
8393 source: chat_completion_sources.MAKERSUITE,
8394 },
8337 // OpenRouter special naming, to differentiate between chat comp and text comp8395 // OpenRouter special naming, to differentiate between chat comp and text comp
8338 'openrouter': {8396 'openrouter': {
8339 selected: 'openai',8397 selected: 'openai',
@@ -8347,6 +8405,9 @@ const CONNECT_API_MAP = {
8347 },8405 },
8348};8406};
83498407
8408// Collect all unique API names in an array
8409export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8410
8350// Fill connections map from textgen_types and chat_completion_sources8411// Fill connections map from textgen_types and chat_completion_sources
8351for (const textGenType of Object.values(textgen_types)) {8412for (const textGenType of Object.values(textgen_types)) {
8352 if (CONNECT_API_MAP[textGenType]) continue;8413 if (CONNECT_API_MAP[textGenType]) continue;
@@ -8416,7 +8477,7 @@ async function disableInstructCallback() {
8416/**8477/**
8417 * @param {string} text API name8478 * @param {string} text API name
8418 */8479 */
8419async function connectAPISlash(_, text) {8480async function connectAPISlash(args, text) {
8420 if (!text.trim()) {8481 if (!text.trim()) {
8421 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {8482 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
8422 if (config.selected !== main_api) continue;8483 if (config.selected !== main_api) continue;
@@ -8439,12 +8500,15 @@ async function connectAPISlash(_, text) {
84398500
8440 return key;8501 return key;
8441 }8502 }
8503
8504 console.error('FIXME: The current API is not in the API map');
8505 return '';
8442 }8506 }
84438507
8444 const apiConfig = CONNECT_API_MAP[text.toLowerCase()];8508 const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
8445 if (!apiConfig) {8509 if (!apiConfig) {
8446 toastr.error(`Error: ${text} is not a valid API`);8510 toastr.error(`Error: ${text} is not a valid API`);
8447 return;8511 return '';
8448 }8512 }
84498513
8450 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);8514 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
@@ -8464,14 +8528,18 @@ async function connectAPISlash(_, text) {
8464 $(apiConfig.button).trigger('click');8528 $(apiConfig.button).trigger('click');
8465 }8529 }
84668530
8467 toastr.info(`API set to ${text}, trying to connect..`);8531 const quiet = isTrueBoolean(args?.quiet);
8532 const toast = quiet ? jQuery() : toastr.info(`API set to ${text}, trying to connect..`);
84688533
8469 try {8534 try {
8470 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);8535 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
8471 console.log('Connection successful');8536 console.log('Connection successful');
8472 } catch {8537 } catch {
8473 console.log('Could not connect after 5 seconds, skipping.');8538 console.log('Could not connect after 10 seconds, skipping.');
8474 }8539 }
8540
8541 toastr.clear(toast);
8542 return text;
8475}8543}
84768544
8477/**8545/**
@@ -8496,7 +8564,7 @@ export async function processDroppedFiles(files, data = new Map()) {
84968564
8497 for (const file of files) {8565 for (const file of files) {
8498 const extension = file.name.split('.').pop().toLowerCase();8566 const extension = file.name.split('.').pop().toLowerCase();
8499 if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) {8567 if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) {
8500 const preservedName = data instanceof Map && data.get(file);8568 const preservedName = data instanceof Map && data.get(file);
8501 await importCharacter(file, preservedName);8569 await importCharacter(file, preservedName);
8502 } else {8570 } else {
@@ -8921,9 +8989,6 @@ jQuery(async function () {
8921 return '';8989 return '';
8922 }8990 }
89238991
8924 // Collect all unique API names in an array
8925 const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8926
8927 SlashCommandParser.addCommandObject(SlashCommand.fromProps({8992 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
8928 name: 'dupe',8993 name: 'dupe',
8929 callback: duplicateCharacter,8994 callback: duplicateCharacter,
@@ -8932,13 +8997,22 @@ jQuery(async function () {
8932 SlashCommandParser.addCommandObject(SlashCommand.fromProps({8997 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
8933 name: 'api',8998 name: 'api',
8934 callback: connectAPISlash,8999 callback: connectAPISlash,
9000 returns: 'the current API',
9001 namedArgumentList: [
9002 SlashCommandNamedArgument.fromProps({
9003 name: 'quiet',
9004 description: 'Suppress the toast message on connection',
9005 typeList: [ARGUMENT_TYPE.BOOLEAN],
9006 defaultValue: 'false',
9007 enumList: commonEnumProviders.boolean('trueFalse')(),
9008 }),
9009 ],
8935 unnamedArgumentList: [9010 unnamedArgumentList: [
8936 SlashCommandArgument.fromProps({9011 SlashCommandArgument.fromProps({
8937 description: 'API to connect to',9012 description: 'API to connect to',
8938 typeList: [ARGUMENT_TYPE.STRING],9013 typeList: [ARGUMENT_TYPE.STRING],
8939 isRequired: false,
8940 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>9014 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
8941 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIs.findIndex(x => x === selected)),9015 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
8942 selected[0].toUpperCase() ?? enumIcons.default)),9016 selected[0].toUpperCase() ?? enumIcons.default)),
8943 }),9017 }),
8944 ],9018 ],
@@ -9095,14 +9169,14 @@ jQuery(async function () {
9095 $('#send_textarea').on('focusin focus click', () => {9169 $('#send_textarea').on('focusin focus click', () => {
9096 S_TAPreviouslyFocused = true;9170 S_TAPreviouslyFocused = true;
9097 });9171 });
9098 $('#send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => {9172 $('#send_but, #option_regenerate, #option_continue, #mes_continue, #mes_impersonate').on('click', () => {
9099 if (S_TAPreviouslyFocused) {9173 if (S_TAPreviouslyFocused) {
9100 $('#send_textarea').focus();9174 $('#send_textarea').focus();
9101 }9175 }
9102 });9176 });
9103 $(document).click(event => {9177 $(document).click(event => {
9104 if ($(':focus').attr('id') !== 'send_textarea') {9178 if ($(':focus').attr('id') !== 'send_textarea') {
9105 var validIDs = ['options_button', 'send_but', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];9179 var validIDs = ['options_button', 'send_but', 'mes_impersonate', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];
9106 if (!validIDs.includes($(event.target).attr('id'))) {9180 if (!validIDs.includes($(event.target).attr('id'))) {
9107 S_TAPreviouslyFocused = false;9181 S_TAPreviouslyFocused = false;
9108 }9182 }
@@ -9138,6 +9212,9 @@ jQuery(async function () {
9138 debouncedCharacterSearch(searchQuery);9212 debouncedCharacterSearch(searchQuery);
9139 });9213 });
91409214
9215 $('#mes_impersonate').on('click', function () {
9216 $('#option_impersonate').trigger('click');
9217 });
91419218
9142 $('#mes_continue').on('click', function () {9219 $('#mes_continue').on('click', function () {
9143 $('#option_continue').trigger('click');9220 $('#option_continue').trigger('click');
@@ -10435,8 +10512,9 @@ jQuery(async function () {
10435 }10512 }
1043610513
10437 // Set the height of "autoSetHeight" textareas within the drawer to their scroll height10514 // Set the height of "autoSetHeight" textareas within the drawer to their scroll height
10438 $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(function () {10515 $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {
10439 resetScrollHeight($(this));10516 await resetScrollHeight($(this));
10517 return;
10440 });10518 });
1044110519
10442 } else if (drawerWasOpenAlready) { //to close manually10520 } else if (drawerWasOpenAlready) { //to close manually
@@ -10509,8 +10587,9 @@ jQuery(async function () {
10509 $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle();10587 $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle();
1051010588
10511 // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height10589 // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height
10512 $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(function () {10590 $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(async function () {
10513 resetScrollHeight($(this));10591 await resetScrollHeight($(this));
10592 return;
10514 });10593 });
10515 });10594 });
1051610595
@@ -10591,15 +10670,31 @@ jQuery(async function () {
10591 }10670 }
10592 });10671 });
1059310672
10594 $(document).on('click', '#OpenAllWIEntries', function () {10673 document.addEventListener('click', function (e) {
10595 $('#world_popup_entries_list').children().find('.down').click();10674 if (!(e.target instanceof HTMLElement)) return;
10596 });10675 if (e.target.matches('#OpenAllWIEntries')) {
10597 $(document).on('click', '#CloseAllWIEntries', function () {10676 document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10598 $('#world_popup_entries_list').children().find('.up').click();10677 toggleDrawer(drawer, true);
10678 });
10679 } else if (e.target.matches('#CloseAllWIEntries')) {
10680 document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10681 toggleDrawer(drawer, false);
10682 });
10683 }
10599 });10684 });
10685
10600 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);10686 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
10601 /* $('#set_character_world').on('click', openCharacterWorldPopup); */10687 /* $('#set_character_world').on('click', openCharacterWorldPopup); */
1060210688
10689 $(document).on('focus', 'input.auto-select, textarea.auto-select', function () {
10690 if (!power_user.enable_auto_select_input) return;
10691 const control = $(this)[0];
10692 if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {
10693 control.select();
10694 console.debug('Auto-selecting content of input control', control);
10695 }
10696 });
10697
10603 $(document).keyup(function (e) {10698 $(document).keyup(function (e) {
10604 if (e.key === 'Escape') {10699 if (e.key === 'Escape') {
10605 const isEditVisible = $('#curEditTextarea').is(':visible');10700 const isEditVisible = $('#curEditTextarea').is(':visible');
@@ -10683,7 +10778,7 @@ jQuery(async function () {
10683 }10778 }
10684 } break;10779 } break;
10685 case 'import_tags': {10780 case 'import_tags': {
10686 await importTags(characters[this_chid], { forceShow: true });10781 await importTags(characters[this_chid], { importSetting: tag_import_setting.ASK });
10687 } break;10782 } break;
10688 /*case 'delete_button':10783 /*case 'delete_button':
10689 popup_type = "del_ch";10784 popup_type = "del_ch";
@@ -10712,62 +10807,66 @@ jQuery(async function () {
10712 var isManualInput = false;10807 var isManualInput = false;
10713 var valueBeforeManualInput;10808 var valueBeforeManualInput;
1071410809
10715 $('.range-block-counter input, .neo-range-input').on('click', function () {10810 $(document).on('input', '.range-block-counter input, .neo-range-input', function () {
10716 valueBeforeManualInput = $(this).val();10811 valueBeforeManualInput = $(this).val();
10717 console.log(valueBeforeManualInput);10812 console.log(valueBeforeManualInput);
10718 })10813 });
10719 .on('change', function (e) {10814
10720 e.target.focus();10815 $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {
10721 e.target.dispatchEvent(new Event('keyup'));10816 e.target.focus();
10722 })10817 e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
10723 .on('keydown', function (e) {10818 });
10724 const masterSelector = '#' + $(this).data('for');10819
10725 const masterElement = $(masterSelector);10820 $(document).on('keydown', '.range-block-counter input, .neo-range-input', function (e) {
10726 if (e.key === 'Enter') {10821 const masterSelector = '#' + $(this).data('for');
10727 let manualInput = Number($(this).val());10822 const masterElement = $(masterSelector);
10728 if (isManualInput) {10823 if (e.key === 'Enter') {
10729 //disallow manual inputs outside acceptable range
10730 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10731 //if value is ok, assign to slider and update handle text and position
10732 //newSlider.val(manualInput)
10733 //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
10734 valueBeforeManualInput = manualInput;
10735 $(masterElement).val($(this).val()).trigger('input', { forced: true });
10736 } else {
10737 //if value not ok, warn and reset to last known valid value
10738 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10739 console.log(valueBeforeManualInput);
10740 //newSlider.val(valueBeforeManualInput)
10741 $(this).val(valueBeforeManualInput);
10742 }
10743 }
10744 }
10745 })
10746 .on('keyup', function () {
10747 valueBeforeManualInput = $(this).val();
10748 console.log(valueBeforeManualInput);
10749 isManualInput = true;
10750 })
10751 //trigger slider changes when user clicks away
10752 .on('mouseup blur', function () {
10753 const masterSelector = '#' + $(this).data('for');
10754 const masterElement = $(masterSelector);
10755 let manualInput = Number($(this).val());10824 let manualInput = Number($(this).val());
10756 if (isManualInput) {10825 if (isManualInput) {
10757 //if value is between correct range for the slider10826 //disallow manual inputs outside acceptable range
10758 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {10827 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10828 //if value is ok, assign to slider and update handle text and position
10829 //newSlider.val(manualInput)
10830 //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
10759 valueBeforeManualInput = manualInput;10831 valueBeforeManualInput = manualInput;
10760 //set the slider value to input value
10761 $(masterElement).val($(this).val()).trigger('input', { forced: true });10832 $(masterElement).val($(this).val()).trigger('input', { forced: true });
10762 } else {10833 } else {
10763 //if value not ok, warn and reset to last known valid value10834 //if value not ok, warn and reset to last known valid value
10764 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);10835 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10765 console.log(valueBeforeManualInput);10836 console.log(valueBeforeManualInput);
10837 //newSlider.val(valueBeforeManualInput)
10766 $(this).val(valueBeforeManualInput);10838 $(this).val(valueBeforeManualInput);
10767 }10839 }
10768 }10840 }
10769 isManualInput = false;10841 }
10770 });10842 });
10843
10844 $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {
10845 valueBeforeManualInput = $(this).val();
10846 console.log(valueBeforeManualInput);
10847 isManualInput = true;
10848 });
10849
10850 //trigger slider changes when user clicks away
10851 $(document).on('mouseup blur', '.range-block-counter input, .neo-range-input', function () {
10852 const masterSelector = '#' + $(this).data('for');
10853 const masterElement = $(masterSelector);
10854 let manualInput = Number($(this).val());
10855 if (isManualInput) {
10856 //if value is between correct range for the slider
10857 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10858 valueBeforeManualInput = manualInput;
10859 //set the slider value to input value
10860 $(masterElement).val($(this).val()).trigger('input', { forced: true });
10861 } else {
10862 //if value not ok, warn and reset to last known valid value
10863 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10864 console.log(valueBeforeManualInput);
10865 $(this).val(valueBeforeManualInput);
10866 }
10867 }
10868 isManualInput = false;
10869 });
1077110870
10772 $('.user_stats_button').on('click', function () {10871 $('.user_stats_button').on('click', function () {
10773 userStatsHandler();10872 userStatsHandler();
public/scripts/BulkEditOverlay.js+34 -4
@@ -18,7 +18,7 @@ import {
18import { favsToHotswap } from './RossAscends-mods.js';18import { favsToHotswap } from './RossAscends-mods.js';
19import { hideLoader, showLoader } from './loader.js';19import { hideLoader, showLoader } from './loader.js';
20import { convertCharacterToPersona } from './personas.js';20import { convertCharacterToPersona } from './personas.js';
21import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap } from './tags.js';21import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
2222
23/**23/**
24 * Static object representing the actions of the24 * Static object representing the actions of the
@@ -197,10 +197,10 @@ class BulkTagPopupHandler {
197 #getHtml = () => {197 #getHtml = () => {
198 const characterData = JSON.stringify({ characterIds: this.characterIds });198 const characterData = JSON.stringify({ characterIds: this.characterIds });
199 return `<div id="bulk_tag_shadow_popup">199 return `<div id="bulk_tag_shadow_popup">
200 <div id="bulk_tag_popup">200 <div id="bulk_tag_popup" class="wider_dialogue_popup">
201 <div id="bulk_tag_popup_holder">201 <div id="bulk_tag_popup_holder">
202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters.</small>203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters. Import all or existing tags for all selected characters.</small>
204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
205 <br>205 <br>
206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
@@ -219,6 +219,12 @@ class BulkTagPopupHandler {
219 <i class="fa-solid fa-trash-can margin-right-10px"></i>219 <i class="fa-solid fa-trash-can margin-right-10px"></i>
220 Mutual220 Mutual
221 </div>221 </div>
222 <div id="bulk_tag_popup_import_all_tags" class="menu_button" title="Import all tags from selected characters" data-i18n="[title]Import all tags from selected characters">
223 Import All
224 </div>
225 <div id="bulk_tag_popup_import_existing_tags" class="menu_button" title="Import existing tags from selected characters" data-i18n="[title]Import existing tags from selected characters">
226 Import Existing
227 </div>
222 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>228 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
223 </div>229 </div>
224 </div>230 </div>
@@ -254,6 +260,30 @@ class BulkTagPopupHandler {
254 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));260 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
255 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));261 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
256 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));262 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
263 document.querySelector('#bulk_tag_popup_import_all_tags').addEventListener('click', this.importAllTags.bind(this));
264 document.querySelector('#bulk_tag_popup_import_existing_tags').addEventListener('click', this.importExistingTags.bind(this));
265 }
266
267 /**
268 * Import existing tags for all selected characters
269 */
270 async importExistingTags() {
271 for (const characterId of this.characterIds) {
272 await importTags(characters[characterId], { importSetting: tag_import_setting.ONLY_EXISTING });
273 }
274
275 $('#bulkTagList').empty();
276 }
277
278 /**
279 * Import all tags for all selected characters
280 */
281 async importAllTags() {
282 for (const characterId of this.characterIds) {
283 await importTags(characters[characterId], { importSetting: tag_import_setting.ALL });
284 }
285
286 $('#bulkTagList').empty();
257 }287 }
258288
259 /**289 /**
@@ -570,7 +600,7 @@ class BulkEditOverlay {
570 this.container.removeEventListener('mouseup', cancelHold);600 this.container.removeEventListener('mouseup', cancelHold);
571 this.container.removeEventListener('touchend', cancelHold);601 this.container.removeEventListener('touchend', cancelHold);
572 },602 },
573 BulkEditOverlay.longPressDelay);603 BulkEditOverlay.longPressDelay);
574 };604 };
575605
576 handleLongPressEnd = (event) => {606 handleLongPressEnd = (event) => {
public/scripts/RossAscends-mods.js+8 -4
@@ -311,6 +311,7 @@ function RA_checkOnlineStatus() {
311 $('#send_form').addClass('no-connection'); //entire input form area is red when not connected311 $('#send_form').addClass('no-connection'); //entire input form area is red when not connected
312 $('#send_but').addClass('displayNone'); //send button is hidden when not connected;312 $('#send_but').addClass('displayNone'); //send button is hidden when not connected;
313 $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;313 $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
314 $('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected;
314 $('#API-status-top').removeClass('fa-plug');315 $('#API-status-top').removeClass('fa-plug');
315 $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow');316 $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow');
316 connection_made = false;317 connection_made = false;
@@ -327,6 +328,7 @@ function RA_checkOnlineStatus() {
327 if (!is_send_press && !(selected_group && is_group_generating)) {328 if (!is_send_press && !(selected_group && is_group_generating)) {
328 $('#send_but').removeClass('displayNone'); //on connect, send button shows329 $('#send_but').removeClass('displayNone'); //on connect, send button shows
329 $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected330 $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected
331 $('#mes_impersonate').removeClass('displayNone'); //continue button is shown when connected
330 }332 }
331 }333 }
332 }334 }
@@ -378,6 +380,7 @@ function RA_autoconnect(PrevApi) {
378 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)380 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
379 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)381 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
380 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)382 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
383 || (secret_state[SECRET_KEYS.BLOCKENTROPY] && oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY)
381 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)384 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
382 ) {385 ) {
383 $('#api_button_openai').trigger('click');386 $('#api_button_openai').trigger('click');
@@ -951,6 +954,11 @@ export function initRossMods() {
951 * @param {KeyboardEvent} event954 * @param {KeyboardEvent} event
952 */955 */
953 async function processHotkeys(event) {956 async function processHotkeys(event) {
957 // Default hotkeys and shortcuts shouldn't work if any popup is currently open
958 if (Popup.util.isPopupOpen()) {
959 return;
960 }
961
954 //Enter to send when send_textarea in focus962 //Enter to send when send_textarea in focus
955 if (document.activeElement == hotkeyTargets['send_textarea']) {963 if (document.activeElement == hotkeyTargets['send_textarea']) {
956 const sendOnEnter = shouldSendOnEnter();964 const sendOnEnter = shouldSendOnEnter();
@@ -1104,10 +1112,6 @@ export function initRossMods() {
1104 }1112 }
11051113
1106 if (event.key == 'Escape') { //closes various panels1114 if (event.key == 'Escape') { //closes various panels
1107 // Do not close panels if we are currently inside a popup
1108 if (Popup.util.isPopupOpen())
1109 return;
1110
1111 //dont override Escape hotkey functions from script.js1115 //dont override Escape hotkey functions from script.js
1112 //"close edit box" and "cancel stream generation".1116 //"close edit box" and "cancel stream generation".
1113 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {1117 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {
public/scripts/autocomplete/AutoComplete.js+71 -48
@@ -16,8 +16,15 @@ export const AUTOCOMPLETE_WIDTH = {
16 'FULL': 2,16 'FULL': 2,
17};17};
1818
19/**@readonly*/
20/**@enum {Number}*/
21export const AUTOCOMPLETE_SELECT_KEY = {
22 'TAB': 1, // 2^0
23 'ENTER': 2, // 2^1
24};
25
19export class AutoComplete {26export class AutoComplete {
20 /**@type {HTMLTextAreaElement}*/ textarea;27 /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
21 /**@type {boolean}*/ isFloating = false;28 /**@type {boolean}*/ isFloating = false;
22 /**@type {()=>boolean}*/ checkIfActivate;29 /**@type {()=>boolean}*/ checkIfActivate;
23 /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;30 /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
@@ -56,6 +63,8 @@ export class AutoComplete {
56 /**@type {function}*/ updateDetailsPositionDebounced;63 /**@type {function}*/ updateDetailsPositionDebounced;
57 /**@type {function}*/ updateFloatingPositionDebounced;64 /**@type {function}*/ updateFloatingPositionDebounced;
5865
66 /**@type {(item:AutoCompleteOption)=>any}*/ onSelect;
67
59 get matchType() {68 get matchType() {
60 return power_user.stscript.matching ?? 'fuzzy';69 return power_user.stscript.matching ?? 'fuzzy';
61 }70 }
@@ -68,7 +77,7 @@ export class AutoComplete {
6877
6978
70 /**79 /**
71 * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete.80 * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete.
72 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')81 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
73 * @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.82 * @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.
74 * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.83 * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
@@ -102,10 +111,15 @@ export class AutoComplete {
102 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);111 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
103 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);112 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
104113
105 textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced));114 textarea.addEventListener('input', ()=>{
115 this.selectionStart = this.textarea.selectionStart;
116 if (this.text != this.textarea.value) this.show(true, this.wasForced);
117 });
106 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));118 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
107 textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);119 textarea.addEventListener('click', ()=>{
108 textarea.addEventListener('selectionchange', ()=>this.show());120 this.selectionStart = this.textarea.selectionStart;
121 if (this.isActive) this.show();
122 });
109 textarea.addEventListener('blur', ()=>this.hide());123 textarea.addEventListener('blur', ()=>this.hide());
110 if (isFloating) {124 if (isFloating) {
111 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());125 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
@@ -189,6 +203,11 @@ export class AutoComplete {
189 * @returns The option.203 * @returns The option.
190 */204 */
191 fuzzyScore(option) {205 fuzzyScore(option) {
206 // might have been matched by the options matchProvider function instead
207 if (!this.fuzzyRegex.test(option.name)) {
208 option.score = new AutoCompleteFuzzyScore(Number.MAX_SAFE_INTEGER, -1);
209 return option;
210 }
192 const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);211 const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
193 let start = null;212 let start = null;
194 let consecutive = [];213 let consecutive = [];
@@ -339,7 +358,7 @@ export class AutoComplete {
339358
340 this.result = this.effectiveParserResult.optionList359 this.result = this.effectiveParserResult.optionList
341 // filter the list of options by the partial name according to the matching type360 // filter the list of options by the partial name according to the matching type
342 .filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)361 .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)
343 // remove aliases362 // remove aliases
344 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);363 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
345364
@@ -357,10 +376,11 @@ export class AutoComplete {
357 // build element376 // build element
358 option.dom = this.makeItem(option);377 option.dom = this.makeItem(option);
359 // update replacer and add quotes if necessary378 // update replacer and add quotes if necessary
379 const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name;
360 if (this.effectiveParserResult.canBeQuoted) {380 if (this.effectiveParserResult.canBeQuoted) {
361 option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;381 option.replacer = optionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionName.replace(/"/g, '\\"')}"` : `${optionName}`;
362 } else {382 } else {
363 option.replacer = option.name;383 option.replacer = optionName;
364 }384 }
365 // calculate fuzzy score if matching is fuzzy385 // calculate fuzzy score if matching is fuzzy
366 if (this.matchType == 'fuzzy') this.fuzzyScore(option);386 if (this.matchType == 'fuzzy') this.fuzzyScore(option);
@@ -399,7 +419,7 @@ export class AutoComplete {
399 ,419 ,
400 );420 );
401 this.result.push(option);421 this.result.push(option);
402 } else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) {422 } else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) {
403 // only one result that is exactly the current value? just show hint, no autocomplete423 // only one result that is exactly the current value? just show hint, no autocomplete
404 this.isReplaceable = false;424 this.isReplaceable = false;
405 this.isShowingDetails = false;425 this.isShowingDetails = false;
@@ -439,11 +459,14 @@ export class AutoComplete {
439 } else {459 } else {
440 item.dom.classList.remove('selected');460 item.dom.classList.remove('selected');
441 }461 }
462 if (!item.isSelectable) {
463 item.dom.classList.add('not-selectable');
464 }
442 frag.append(item.dom);465 frag.append(item.dom);
443 }466 }
444 this.dom.append(frag);467 this.dom.append(frag);
445 this.updatePosition();468 this.updatePosition();
446 getTopmostModalLayer().append(this.domWrap);469 this.getLayer().append(this.domWrap);
447 } else {470 } else {
448 this.domWrap.remove();471 this.domWrap.remove();
449 }472 }
@@ -458,10 +481,17 @@ export class AutoComplete {
458 if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();481 if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
459 this.detailsDom.innerHTML = '';482 this.detailsDom.innerHTML = '';
460 this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');483 this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
461 getTopmostModalLayer().append(this.detailsWrap);484 this.getLayer().append(this.detailsWrap);
462 this.updateDetailsPositionDebounced();485 this.updateDetailsPositionDebounced();
463 }486 }
464487
488 /**
489 * @returns {HTMLElement} closest ancestor dialog or body
490 */
491 getLayer() {
492 return this.textarea.closest('dialog, body');
493 }
494
465495
466496
467 /**497 /**
@@ -474,7 +504,7 @@ export class AutoComplete {
474 const rect = {};504 const rect = {};
475 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();505 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
476 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();506 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
477 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();507 rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect();
478 this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);508 this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
479 this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);509 this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
480 this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;510 this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
@@ -501,7 +531,7 @@ export class AutoComplete {
501 const rect = {};531 const rect = {};
502 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();532 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
503 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();533 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
504 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();534 rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect();
505 if (this.isReplaceable) {535 if (this.isReplaceable) {
506 this.detailsWrap.classList.remove('full');536 this.detailsWrap.classList.remove('full');
507 const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();537 const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
@@ -527,32 +557,34 @@ export class AutoComplete {
527 updateFloatingPosition() {557 updateFloatingPosition() {
528 const location = this.getCursorPosition();558 const location = this.getCursorPosition();
529 const rect = this.textarea.getBoundingClientRect();559 const rect = this.textarea.getBoundingClientRect();
560 const layerRect = this.getLayer().getBoundingClientRect();
530 // cursor is out of view -> hide561 // cursor is out of view -> hide
531 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {562 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
532 return this.hide();563 return this.hide();
533 }564 }
534 const left = Math.max(rect.left, location.left);565 const left = Math.max(rect.left, location.left) - layerRect.left;
535 this.domWrap.style.setProperty('--targetOffset', `${left}`);566 this.domWrap.style.setProperty('--targetOffset', `${left}`);
536 if (location.top <= window.innerHeight / 2) {567 if (location.top <= window.innerHeight / 2) {
537 // if cursor is in lower half of window, show list above line568 // if cursor is in lower half of window, show list above line
538 this.domWrap.style.top = `${location.bottom}px`;569 this.domWrap.style.top = `${location.bottom - layerRect.top}px`;
539 this.domWrap.style.bottom = 'auto';570 this.domWrap.style.bottom = 'auto';
540 this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;571 this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
541 } else {572 } else {
542 // if cursor is in upper half of window, show list below line573 // if cursor is in upper half of window, show list below line
543 this.domWrap.style.top = 'auto';574 this.domWrap.style.top = 'auto';
544 this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`;575 this.domWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`;
545 this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;576 this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
546 }577 }
547 }578 }
548579
549 updateFloatingDetailsPosition(location = null) {580 updateFloatingDetailsPosition(location = null) {
550 if (!location) location = this.getCursorPosition();581 if (!location) location = this.getCursorPosition();
551 const rect = this.textarea.getBoundingClientRect();582 const rect = this.textarea.getBoundingClientRect();
583 const layerRect = this.getLayer().getBoundingClientRect();
552 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {584 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
553 return this.hide();585 return this.hide();
554 }586 }
555 const left = Math.max(rect.left, location.left);587 const left = Math.max(rect.left, location.left) - layerRect.left;
556 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);588 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
557 if (this.isReplaceable) {589 if (this.isReplaceable) {
558 this.detailsWrap.classList.remove('full');590 this.detailsWrap.classList.remove('full');
@@ -572,14 +604,14 @@ export class AutoComplete {
572 }604 }
573 if (location.top <= window.innerHeight / 2) {605 if (location.top <= window.innerHeight / 2) {
574 // if cursor is in lower half of window, show list above line606 // if cursor is in lower half of window, show list above line
575 this.detailsWrap.style.top = `${location.bottom}px`;607 this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`;
576 this.detailsWrap.style.bottom = 'auto';608 this.detailsWrap.style.bottom = 'auto';
577 this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;609 this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
578 } else {610 } else {
579 // if cursor is in upper half of window, show list below line611 // if cursor is in upper half of window, show list below line
580 this.detailsWrap.style.top = 'auto';612 this.detailsWrap.style.top = 'auto';
581 this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`;613 this.detailsWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`;
582 this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;614 this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
583 }615 }
584 }616 }
585617
@@ -597,7 +629,7 @@ export class AutoComplete {
597 }629 }
598 this.clone.style.position = 'fixed';630 this.clone.style.position = 'fixed';
599 this.clone.style.visibility = 'hidden';631 this.clone.style.visibility = 'hidden';
600 getTopmostModalLayer().append(this.clone);632 document.body.append(this.clone);
601 const mo = new MutationObserver(muts=>{633 const mo = new MutationObserver(muts=>{
602 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {634 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
603 this.clone.remove();635 this.clone.remove();
@@ -656,6 +688,7 @@ export class AutoComplete {
656 }688 }
657 this.wasForced = false;689 this.wasForced = false;
658 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));690 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
691 this.onSelect?.(this.selectedItem);
659 }692 }
660693
661694
@@ -708,8 +741,10 @@ export class AutoComplete {
708 }741 }
709 case 'Enter': {742 case 'Enter': {
710 // pick the selected item to autocomplete743 // pick the selected item to autocomplete
744 if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break;
711 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;745 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
712 if (this.selectedItem.name == this.name) break;746 if (this.selectedItem.name == this.name) break;
747 if (!this.selectedItem.isSelectable) break;
713 evt.preventDefault();748 evt.preventDefault();
714 evt.stopImmediatePropagation();749 evt.stopImmediatePropagation();
715 this.select();750 this.select();
@@ -717,9 +752,11 @@ export class AutoComplete {
717 }752 }
718 case 'Tab': {753 case 'Tab': {
719 // pick the selected item to autocomplete754 // pick the selected item to autocomplete
755 if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break;
720 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;756 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
721 evt.preventDefault();757 evt.preventDefault();
722 evt.stopImmediatePropagation();758 evt.stopImmediatePropagation();
759 if (!this.selectedItem.isSelectable) break;
723 this.select();760 this.select();
724 return;761 return;
725 }762 }
@@ -772,30 +809,16 @@ export class AutoComplete {
772 // ignore keydown on modifier keys809 // ignore keydown on modifier keys
773 return;810 return;
774 }811 }
775 switch (evt.key) {812 // await keyup to see if cursor position or text has changed
776 case 'ArrowUp':813 const oldText = this.textarea.value;
777 case 'ArrowDown':814 await new Promise(resolve=>{
778 case 'ArrowRight':815 window.addEventListener('keyup', resolve, { once:true });
779 case 'ArrowLeft': {816 });
780 if (this.isActive) {817 if (this.selectionStart != this.textarea.selectionStart) {
781 // keyboard navigation, wait for keyup to complete cursor move818 this.selectionStart = this.textarea.selectionStart;
782 const oldText = this.textarea.value;819 this.show(this.isReplaceable || oldText != this.textarea.value);
783 await new Promise(resolve=>{820 } else if (this.isActive) {
784 window.addEventListener('keyup', resolve, { once:true });821 this.text != this.textarea.value && this.show(this.isReplaceable);
785 });
786 if (this.selectionStart != this.textarea.selectionStart) {
787 this.selectionStart = this.textarea.selectionStart;
788 this.show(this.isReplaceable || oldText != this.textarea.value);
789 }
790 }
791 break;
792 }
793 default: {
794 if (this.isActive) {
795 this.text != this.textarea.value && this.show(this.isReplaceable);
796 }
797 break;
798 }
799 }822 }
800 }823 }
801}824}
public/scripts/autocomplete/AutoCompleteNameResult.js+3 -30
@@ -1,36 +1,9 @@
1import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';1import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
2import { AutoCompleteOption } from './AutoCompleteOption.js';2import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
3// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
43
54
65
7export class AutoCompleteNameResult {6export class AutoCompleteNameResult extends AutoCompleteNameResultBase {
8 /**@type {string} */ name;
9 /**@type {number} */ start;
10 /**@type {AutoCompleteOption[]} */ optionList = [];
11 /**@type {boolean} */ canBeQuoted = false;
12 /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
13 /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
14
15
16 /**
17 * @param {string} name Name (potentially partial) of the name at the requested index.
18 * @param {number} start Index where the name starts.
19 * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
20 * @param {boolean} canBeQuoted Whether the name can be inside quotes.
21 * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
22 * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
23 */
24 constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
25 this.name = name;
26 this.start = start;
27 this.optionList = optionList;
28 this.canBeQuoted = canBeQuoted;
29 this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
30 this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
31 }
32
33
34 /**7 /**
35 *8 *
36 * @param {string} text The whole text9 * @param {string} text The whole text
public/scripts/autocomplete/AutoCompleteNameResultBase.js+31 -0
@@ -0,0 +1,31 @@
1import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
2import { AutoCompleteOption } from './AutoCompleteOption.js';
3
4
5
6export class AutoCompleteNameResultBase {
7 /**@type {string} */ name;
8 /**@type {number} */ start;
9 /**@type {AutoCompleteOption[]} */ optionList = [];
10 /**@type {boolean} */ canBeQuoted = false;
11 /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
12 /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
13
14
15 /**
16 * @param {string} name Name (potentially partial) of the name at the requested index.
17 * @param {number} start Index where the name starts.
18 * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
19 * @param {boolean} canBeQuoted Whether the name can be inside quotes.
20 * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
21 * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
22 */
23 constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
24 this.name = name;
25 this.start = start;
26 this.optionList = optionList;
27 this.canBeQuoted = canBeQuoted;
28 this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
29 this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
30 }
31}
public/scripts/autocomplete/AutoCompleteOption.js+11 -1
@@ -11,6 +11,9 @@ export class AutoCompleteOption {
11 /**@type {AutoCompleteFuzzyScore}*/ score;11 /**@type {AutoCompleteFuzzyScore}*/ score;
12 /**@type {string}*/ replacer;12 /**@type {string}*/ replacer;
13 /**@type {HTMLElement}*/ dom;13 /**@type {HTMLElement}*/ dom;
14 /**@type {(input:string)=>boolean}*/ matchProvider;
15 /**@type {(input:string)=>string}*/ valueProvider;
16 /**@type {boolean}*/ makeSelectable = false;
1417
1518
16 /**19 /**
@@ -21,14 +24,21 @@ export class AutoCompleteOption {
21 return this.name;24 return this.name;
22 }25 }
2326
27 get isSelectable() {
28 return this.makeSelectable || !this.valueProvider;
29 }
30
2431
25 /**32 /**
26 * @param {string} name33 * @param {string} name
27 */34 */
28 constructor(name, typeIcon = ' ', type = '') {35 constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) {
29 this.name = name;36 this.name = name;
30 this.typeIcon = typeIcon;37 this.typeIcon = typeIcon;
31 this.type = type;38 this.type = type;
39 this.matchProvider = matchProvider;
40 this.valueProvider = valueProvider;
41 this.makeSelectable = makeSelectable;
32 }42 }
3343
3444
public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js+2 -2
@@ -1,6 +1,6 @@
1import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';1import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
22
3export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {3export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultBase {
4 /**@type {boolean}*/ isRequired = false;4 /**@type {boolean}*/ isRequired = false;
5 /**@type {boolean}*/ forceMatch = true;5 /**@type {boolean}*/ forceMatch = true;
6}6}
public/scripts/extensions.js+49 -22
@@ -21,6 +21,7 @@ const defaultUrl = 'http://localhost:5100';
21let saveMetadataTimeout = null;21let saveMetadataTimeout = null;
2222
23let requiresReload = false;23let requiresReload = false;
24let stateChanged = false;
2425
25export function saveMetadataDebounced() {26export function saveMetadataDebounced() {
26 const context = getContext();27 const context = getContext();
@@ -238,6 +239,7 @@ function onEnableExtensionClick() {
238239
239async function enableExtension(name, reload = true) {240async function enableExtension(name, reload = true) {
240 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);241 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
242 stateChanged = true;
241 await saveSettings();243 await saveSettings();
242 if (reload) {244 if (reload) {
243 location.reload();245 location.reload();
@@ -248,6 +250,7 @@ async function enableExtension(name, reload = true) {
248250
249async function disableExtension(name, reload = true) {251async function disableExtension(name, reload = true) {
250 extension_settings.disabledExtensions.push(name);252 extension_settings.disabledExtensions.push(name);
253 stateChanged = true;
251 await saveSettings();254 await saveSettings();
252 if (reload) {255 if (reload) {
253 location.reload();256 location.reload();
@@ -657,7 +660,29 @@ async function showExtensionsDetails() {
657 await oldPopup.complete(POPUP_RESULT.CANCELLED);660 await oldPopup.complete(POPUP_RESULT.CANCELLED);
658 }661 }
659662
660 const popup = new Popup(html, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton], allowVerticalScrolling: true });663 let waitingForSave = false;
664
665 const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
666 okButton: 'Close',
667 wide: true,
668 large: true,
669 customButtons: [updateAllButton],
670 allowVerticalScrolling: true,
671 onClosing: async () => {
672 if (waitingForSave) {
673 return false;
674 }
675 if (stateChanged) {
676 waitingForSave = true;
677 const toast = toastr.info('The page will be reloaded shortly...', 'Extensions state changed');
678 await saveSettings();
679 toastr.clear(toast);
680 waitingForSave = false;
681 requiresReload = true;
682 }
683 return true;
684 },
685 });
661 popupPromise = popup.show();686 popupPromise = popup.show();
662 } catch (error) {687 } catch (error) {
663 toastr.error('Error loading extensions. See browser console for details.');688 toastr.error('Error loading extensions. See browser console for details.');
@@ -989,6 +1014,28 @@ export async function writeExtensionField(characterId, key, value) {
989 }1014 }
990}1015}
9911016
1017/**
1018 * Prompts the user to enter the Git URL of the extension to import.
1019 * After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
1020 * If the extension is imported successfully, a success message is displayed.
1021 * If the extension import fails, an error message is displayed and the error is logged to the console.
1022 * After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
1023 * @param {string} [suggestUrl] Suggested URL to install
1024 * @returns {Promise<void>}
1025 */
1026export async function openThirdPartyExtensionMenu(suggestUrl = '') {
1027 const html = await renderTemplateAsync('installExtension');
1028 const input = await callGenericPopup(html, POPUP_TYPE.INPUT, suggestUrl ?? '');
1029
1030 if (!input) {
1031 console.debug('Extension install cancelled');
1032 return;
1033 }
1034
1035 const url = String(input).trim();
1036 await installExtension(url);
1037}
1038
992jQuery(async function () {1039jQuery(async function () {
993 await addExtensionsButtonAndMenu();1040 await addExtensionsButtonAndMenu();
994 $('#extensionsMenuButton').css('display', 'flex');1041 $('#extensionsMenuButton').css('display', 'flex');
@@ -1004,28 +1051,8 @@ jQuery(async function () {
10041051
1005 /**1052 /**
1006 * Handles the click event for the third-party extension import button.1053 * Handles the click event for the third-party extension import button.
1007 * Prompts the user to enter the Git URL of the extension to import.
1008 * After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
1009 * If the extension is imported successfully, a success message is displayed.
1010 * If the extension import fails, an error message is displayed and the error is logged to the console.
1011 * After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
1012 *1054 *
1013 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.1055 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
1014 */1056 */
1015 $('#third_party_extension_button').on('click', async () => {1057 $('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
1016 const html = `<h3>Enter the Git URL of the extension to install</h3>
1017 <br>
1018 <p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
1019 <br>
1020 <p>Example: <tt> https://github.com/author/extension-name </tt></p>`;
1021 const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '');
1022
1023 if (!input) {
1024 console.debug('Extension install cancelled');
1025 return;
1026 }
1027
1028 const url = String(input).trim();
1029 await installExtension(url);
1030 });
1031});1058});
public/scripts/extensions/caption/index.js+7 -3
@@ -8,13 +8,12 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
8import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';8import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
9import { SlashCommand } from '../../slash-commands/SlashCommand.js';9import { SlashCommand } from '../../slash-commands/SlashCommand.js';
10import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';10import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
11import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
12import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';11import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
13export { MODULE_NAME };12export { MODULE_NAME };
1413
15const MODULE_NAME = 'caption';14const MODULE_NAME = 'caption';
1615
17const PROMPT_DEFAULT = 'What’s in this image?';16const PROMPT_DEFAULT = 'What\'s in this image?';
18const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';17const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';
1918
20/**19/**
@@ -170,7 +169,11 @@ async function sendCaptionedMessage(caption, image) {
170 },169 },
171 };170 };
172 context.chat.push(message);171 context.chat.push(message);
172 const messageId = context.chat.length - 1;
173 await eventSource.emit(event_types.MESSAGE_SENT, messageId);
173 context.addOneMessage(message);174 context.addOneMessage(message);
175 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
176 await context.saveChat();
174}177}
175178
176/**179/**
@@ -334,7 +337,7 @@ async function getCaptionForFile(file, prompt, quiet) {
334 }337 }
335 catch (error) {338 catch (error) {
336 const errorMessage = error.message || 'Unknown error';339 const errorMessage = error.message || 'Unknown error';
337 toastr.error(errorMessage, "Failed to caption image.");340 toastr.error(errorMessage, 'Failed to caption image.');
338 console.error(error);341 console.error(error);
339 return '';342 return '';
340 }343 }
@@ -399,6 +402,7 @@ jQuery(async function () {
399 (modules.includes('caption') && extension_settings.caption.source === 'extras') ||402 (modules.includes('caption') && extension_settings.caption.source === 'extras') ||
400 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
401 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||404 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
405 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||
402 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||406 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||407 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
404 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||408 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
public/scripts/extensions/caption/settings.html+6 -1
@@ -17,9 +17,10 @@
17 <div class="flex1 flex-container flexFlowColumn flexNoGap">17 <div class="flex1 flex-container flexFlowColumn flexNoGap">
18 <label for="caption_multimodal_api" data-i18n="API">API</label>18 <label for="caption_multimodal_api" data-i18n="API">API</label>
19 <select id="caption_multimodal_api" class="flex1 text_pole">19 <select id="caption_multimodal_api" class="flex1 text_pole">
20 <option value="zerooneai">01.AI (Yi)</option>
20 <option value="anthropic">Anthropic</option>21 <option value="anthropic">Anthropic</option>
21 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>22 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
22 <option value="google">Google MakerSuite</option>23 <option value="google">Google AI Studio</option>
23 <option value="koboldcpp">KoboldCpp</option>24 <option value="koboldcpp">KoboldCpp</option>
24 <option value="llamacpp">llama.cpp</option>25 <option value="llamacpp">llama.cpp</option>
25 <option value="ollama">Ollama</option>26 <option value="ollama">Ollama</option>
@@ -32,16 +33,20 @@
32 <div class="flex1 flex-container flexFlowColumn flexNoGap">33 <div class="flex1 flex-container flexFlowColumn flexNoGap">
33 <label for="caption_multimodal_model" data-i18n="Model">Model</label>34 <label for="caption_multimodal_model" data-i18n="Model">Model</label>
34 <select id="caption_multimodal_model" class="flex1 text_pole">35 <select id="caption_multimodal_model" class="flex1 text_pole">
36 <option data-type="zerooneai" value="yi-vision">yi-vision</option>
35 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>37 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
36 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>38 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
37 <option data-type="openai" value="gpt-4o">gpt-4o</option>39 <option data-type="openai" value="gpt-4o">gpt-4o</option>
38 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>40 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>
41 <option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
39 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>42 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
40 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>43 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
41 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>44 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
42 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>45 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
43 <option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>46 <option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
44 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>47 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
48 <option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
49 <option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
45 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>50 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
46 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>51 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
47 <option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>52 <option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
public/scripts/extensions/expressions/index.js+2 -2
@@ -1,4 +1,4 @@
1import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';1import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';
2import { dragElement, isMobile } from '../../RossAscends-mods.js';2import { dragElement, isMobile } from '../../RossAscends-mods.js';
3import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';3import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
4import { loadMovingUIState, power_user } from '../../power-user.js';4import { loadMovingUIState, power_user } from '../../power-user.js';
@@ -1156,7 +1156,7 @@ async function getExpressionLabel(text) {
11561156
1157 functionResult = args?.arguments;1157 functionResult = args?.arguments;
1158 });1158 });
1159 const emotionResponse = await generateQuietPrompt(prompt, false, false);1159 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
1160 return parseLlmResponse(functionResult || emotionResponse, expressionsList);1160 return parseLlmResponse(functionResult || emotionResponse, expressionsList);
1161 }1161 }
1162 // Extras1162 // Extras
public/scripts/extensions/memory/index.js+160 -31
@@ -1,4 +1,4 @@
1import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js';1import { getStringHash, debounce, waitUntilCondition, extractAllWords, isTrueBoolean } from '../../utils.js';
2import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';2import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
3import {3import {
4 activateSendButtons,4 activateSendButtons,
@@ -25,6 +25,8 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
25import { SlashCommand } from '../../slash-commands/SlashCommand.js';25import { SlashCommand } from '../../slash-commands/SlashCommand.js';
26import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';26import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
27import { MacrosParser } from '../../macros.js';27import { MacrosParser } from '../../macros.js';
28import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
29import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
28export { MODULE_NAME };30export { MODULE_NAME };
2931
30const MODULE_NAME = '1_memory';32const MODULE_NAME = '1_memory';
@@ -36,6 +38,41 @@ let lastMessageHash = null;
36let lastMessageId = null;38let lastMessageId = null;
37let inApiCall = false;39let inApiCall = false;
3840
41/**
42 * Count the number of tokens in the provided text.
43 * @param {string} text Text to count tokens for
44 * @param {number} padding Number of additional tokens to add to the count
45 * @returns {Promise<number>} Number of tokens in the text
46 */
47async function countSourceTokens(text, padding = 0) {
48 if (extension_settings.memory.source === summary_sources.webllm) {
49 const count = await countWebLlmTokens(text);
50 return count + padding;
51 }
52
53 if (extension_settings.memory.source === summary_sources.extras) {
54 const count = getTextTokens(tokenizers.GPT2, text).length;
55 return count + padding;
56 }
57
58 return await getTokenCountAsync(text, padding);
59}
60
61async function getSourceContextSize() {
62 const overrideLength = extension_settings.memory.overrideResponseLength;
63
64 if (extension_settings.memory.source === summary_sources.webllm) {
65 const maxContext = await getWebLlmContextSize();
66 return overrideLength > 0 ? (maxContext - overrideLength) : Math.round(maxContext * 0.75);
67 }
68
69 if (extension_settings.source === summary_sources.extras) {
70 return 1024 - 64;
71 }
72
73 return getMaxContextSize(overrideLength);
74}
75
39const formatMemoryValue = function (value) {76const formatMemoryValue = function (value) {
40 if (!value) {77 if (!value) {
41 return '';78 return '';
@@ -55,6 +92,7 @@ const saveChatDebounced = debounce(() => getContext().saveChat(), debounce_timeo
55const summary_sources = {92const summary_sources = {
56 'extras': 'extras',93 'extras': 'extras',
57 'main': 'main',94 'main': 'main',
95 'webllm': 'webllm',
58};96};
5997
60const prompt_builders = {98const prompt_builders = {
@@ -130,12 +168,12 @@ function loadSettings() {
130168
131async function onPromptForceWordsAutoClick() {169async function onPromptForceWordsAutoClick() {
132 const context = getContext();170 const context = getContext();
133 const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength);171 const maxPromptLength = await getSourceContextSize();
134 const chat = context.chat;172 const chat = context.chat;
135 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);173 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
136 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;174 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
137 const averageMessageWordCount = messagesWordCount / allMessages.length;175 const averageMessageWordCount = messagesWordCount / allMessages.length;
138 const tokensPerWord = await getTokenCountAsync(allMessages.join('\n')) / messagesWordCount;176 const tokensPerWord = await countSourceTokens(allMessages.join('\n')) / messagesWordCount;
139 const wordsPerToken = 1 / tokensPerWord;177 const wordsPerToken = 1 / tokensPerWord;
140 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);178 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);
141 // How many words should pass so that messages will start be dropped out of context;179 // How many words should pass so that messages will start be dropped out of context;
@@ -168,15 +206,15 @@ async function onPromptForceWordsAutoClick() {
168206
169async function onPromptIntervalAutoClick() {207async function onPromptIntervalAutoClick() {
170 const context = getContext();208 const context = getContext();
171 const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength);209 const maxPromptLength = await getSourceContextSize();
172 const chat = context.chat;210 const chat = context.chat;
173 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);211 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
174 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;212 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
175 const messagesTokenCount = await getTokenCountAsync(allMessages.join('\n'));213 const messagesTokenCount = await countSourceTokens(allMessages.join('\n'));
176 const tokensPerWord = messagesTokenCount / messagesWordCount;214 const tokensPerWord = messagesTokenCount / messagesWordCount;
177 const averageMessageTokenCount = messagesTokenCount / allMessages.length;215 const averageMessageTokenCount = messagesTokenCount / allMessages.length;
178 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);216 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);
179 const promptTokens = await getTokenCountAsync(extension_settings.memory.prompt);217 const promptTokens = await countSourceTokens(extension_settings.memory.prompt);
180 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;218 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;
181 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;219 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
182 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);220 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);
@@ -213,8 +251,8 @@ function onSummarySourceChange(event) {
213251
214function switchSourceControls(value) {252function switchSourceControls(value) {
215 $('#memory_settings [data-summary-source]').each((_, element) => {253 $('#memory_settings [data-summary-source]').each((_, element) => {
216 const source = $(element).data('summary-source');254 const source = element.dataset.summarySource.split(',').map(s => s.trim());
217 $(element).toggle(source === value);255 $(element).toggle(source.includes(value));
218 });256 });
219}257}
220258
@@ -353,10 +391,13 @@ function getIndexOfLatestChatSummary(chat) {
353391
354async function onChatEvent() {392async function onChatEvent() {
355 // Module not enabled393 // Module not enabled
356 if (extension_settings.memory.source === summary_sources.extras) {394 if (extension_settings.memory.source === summary_sources.extras && !modules.includes('summarize')) {
357 if (!modules.includes('summarize')) {395 return;
358 return;396 }
359 }397
398 // WebLLM is not supported
399 if (extension_settings.memory.source === summary_sources.webllm && !isWebLlmSupported()) {
400 return;
360 }401 }
361402
362 const context = getContext();403 const context = getContext();
@@ -416,7 +457,12 @@ async function onChatEvent() {
416 }457 }
417}458}
418459
419async function forceSummarizeChat() {460/**
461 * Forces a summary generation for the current chat.
462 * @param {boolean} quiet If an informational toast should be displayed
463 * @returns {Promise<string>} Summarized text
464 */
465async function forceSummarizeChat(quiet) {
420 if (extension_settings.memory.source === summary_sources.extras) {466 if (extension_settings.memory.source === summary_sources.extras) {
421 toastr.warning('Force summarization is not supported for Extras API');467 toastr.warning('Force summarization is not supported for Extras API');
422 return;468 return;
@@ -431,8 +477,12 @@ async function forceSummarizeChat() {
431 return '';477 return '';
432 }478 }
433479
434 toastr.info('Summarizing chat...', 'Please wait');480 const toast = quiet ? jQuery() : toastr.info('Summarizing chat...', 'Please wait', { timeOut: 0, extendedTimeOut: 0 });
435 const value = await summarizeChatMain(context, true, skipWIAN);481 const value = extension_settings.memory.source === summary_sources.main
482 ? await summarizeChatMain(context, true, skipWIAN)
483 : await summarizeChatWebLLM(context, true);
484
485 toastr.clear(toast);
436486
437 if (!value) {487 if (!value) {
438 toastr.warning('Failed to summarize chat');488 toastr.warning('Failed to summarize chat');
@@ -450,9 +500,10 @@ async function forceSummarizeChat() {
450async function summarizeCallback(args, text) {500async function summarizeCallback(args, text) {
451 text = text.trim();501 text = text.trim();
452502
453 // Using forceSummarizeChat to summarize the current chat503 // Summarize the current chat if no text provided
454 if (!text) {504 if (!text) {
455 return await forceSummarizeChat();505 const quiet = isTrueBoolean(args.quiet);
506 return await forceSummarizeChat(quiet);
456 }507 }
457508
458 const source = args.source || extension_settings.memory.source;509 const source = args.source || extension_settings.memory.source;
@@ -464,6 +515,11 @@ async function summarizeCallback(args, text) {
464 return await callExtrasSummarizeAPI(text);515 return await callExtrasSummarizeAPI(text);
465 case summary_sources.main:516 case summary_sources.main:
466 return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);517 return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
518 case summary_sources.webllm: {
519 const messages = [{ role: 'system', content: prompt }, { role: 'user', content: text }].filter(m => m.content);
520 const params = extension_settings.memory.overrideResponseLength > 0 ? { max_tokens: extension_settings.memory.overrideResponseLength } : {};
521 return await generateWebLlmChatPrompt(messages, params);
522 }
467 default:523 default:
468 toastr.warning('Invalid summarization source specified');524 toastr.warning('Invalid summarization source specified');
469 return '';525 return '';
@@ -484,16 +540,25 @@ async function summarizeChat(context) {
484 case summary_sources.main:540 case summary_sources.main:
485 await summarizeChatMain(context, false, skipWIAN);541 await summarizeChatMain(context, false, skipWIAN);
486 break;542 break;
543 case summary_sources.webllm:
544 await summarizeChatWebLLM(context, false);
545 break;
487 default:546 default:
488 break;547 break;
489 }548 }
490}549}
491550
492async function summarizeChatMain(context, force, skipWIAN) {551/**
493552 * Check if the chat should be summarized based on the current conditions.
553 * Return summary prompt if it should be summarized.
554 * @param {any} context ST context
555 * @param {boolean} force Summarize the chat regardless of the conditions
556 * @returns {Promise<string>} Summary prompt or empty string
557 */
558async function getSummaryPromptForNow(context, force) {
494 if (extension_settings.memory.promptInterval === 0 && !force) {559 if (extension_settings.memory.promptInterval === 0 && !force) {
495 console.debug('Prompt interval is set to 0, skipping summarization');560 console.debug('Prompt interval is set to 0, skipping summarization');
496 return;561 return '';
497 }562 }
498563
499 try {564 try {
@@ -505,17 +570,17 @@ async function summarizeChatMain(context, force, skipWIAN) {
505 waitUntilCondition(() => is_send_press === false, 30000, 100);570 waitUntilCondition(() => is_send_press === false, 30000, 100);
506 } catch {571 } catch {
507 console.debug('Timeout waiting for is_send_press');572 console.debug('Timeout waiting for is_send_press');
508 return;573 return '';
509 }574 }
510575
511 if (!context.chat.length) {576 if (!context.chat.length) {
512 console.debug('No messages in chat to summarize');577 console.debug('No messages in chat to summarize');
513 return;578 return '';
514 }579 }
515580
516 if (context.chat.length < extension_settings.memory.promptInterval && !force) {581 if (context.chat.length < extension_settings.memory.promptInterval && !force) {
517 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);582 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);
518 return;583 return '';
519 }584 }
520585
521 let messagesSinceLastSummary = 0;586 let messagesSinceLastSummary = 0;
@@ -539,7 +604,7 @@ async function summarizeChatMain(context, force, skipWIAN) {
539604
540 if (!conditionSatisfied && !force) {605 if (!conditionSatisfied && !force) {
541 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);606 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);
542 return;607 return '';
543 }608 }
544609
545 console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);610 console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);
@@ -547,6 +612,63 @@ async function summarizeChatMain(context, force, skipWIAN) {
547612
548 if (!prompt) {613 if (!prompt) {
549 console.debug('Summarization prompt is empty. Skipping summarization.');614 console.debug('Summarization prompt is empty. Skipping summarization.');
615 return '';
616 }
617
618 return prompt;
619}
620
621async function summarizeChatWebLLM(context, force) {
622 if (!isWebLlmSupported()) {
623 return;
624 }
625
626 const prompt = await getSummaryPromptForNow(context, force);
627
628 if (!prompt) {
629 return;
630 }
631
632 const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
633
634 if (lastUsedIndex === null || lastUsedIndex === -1) {
635 if (force) {
636 toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');
637 }
638
639 return null;
640 }
641
642 const messages = [
643 { role: 'system', content: prompt },
644 { role: 'user', content: rawPrompt },
645 ];
646
647 const params = {};
648
649 if (extension_settings.memory.overrideResponseLength > 0) {
650 params.max_tokens = extension_settings.memory.overrideResponseLength;
651 }
652
653 const summary = await generateWebLlmChatPrompt(messages, params);
654 const newContext = getContext();
655
656 // something changed during summarization request
657 if (newContext.groupId !== context.groupId ||
658 newContext.chatId !== context.chatId ||
659 (!newContext.groupId && (newContext.characterId !== context.characterId))) {
660 console.log('Context changed, summary discarded');
661 return;
662 }
663
664 setMemoryContext(summary, true, lastUsedIndex);
665 return summary;
666}
667
668async function summarizeChatMain(context, force, skipWIAN) {
669 const prompt = await getSummaryPromptForNow(context, force);
670
671 if (!prompt) {
550 return;672 return;
551 }673 }
552674
@@ -634,7 +756,7 @@ async function getRawSummaryPrompt(context, prompt) {
634 chat.pop(); // We always exclude the last message from the buffer756 chat.pop(); // We always exclude the last message from the buffer
635 const chatBuffer = [];757 const chatBuffer = [];
636 const PADDING = 64;758 const PADDING = 64;
637 const PROMPT_SIZE = getMaxContextSize(extension_settings.memory.overrideResponseLength);759 const PROMPT_SIZE = await getSourceContextSize();
638 let latestUsedMessage = null;760 let latestUsedMessage = null;
639761
640 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {762 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {
@@ -651,7 +773,7 @@ async function getRawSummaryPrompt(context, prompt) {
651 const entry = `${message.name}:\n${message.mes}`;773 const entry = `${message.name}:\n${message.mes}`;
652 chatBuffer.push(entry);774 chatBuffer.push(entry);
653775
654 const tokens = await getTokenCountAsync(getMemoryString(true), PADDING);776 const tokens = await countSourceTokens(getMemoryString(true), PADDING);
655777
656 if (tokens > PROMPT_SIZE) {778 if (tokens > PROMPT_SIZE) {
657 chatBuffer.pop();779 chatBuffer.pop();
@@ -680,7 +802,7 @@ async function summarizeChatExtras(context) {
680 const reversedChat = chat.slice().reverse();802 const reversedChat = chat.slice().reverse();
681 reversedChat.shift();803 reversedChat.shift();
682 const memoryBuffer = [];804 const memoryBuffer = [];
683 const CONTEXT_SIZE = 1024 - 64;805 const CONTEXT_SIZE = await getSourceContextSize();
684806
685 for (const message of reversedChat) {807 for (const message of reversedChat) {
686 // we reached the point of latest memory808 // we reached the point of latest memory
@@ -698,14 +820,14 @@ async function summarizeChatExtras(context) {
698 memoryBuffer.push(entry);820 memoryBuffer.push(entry);
699821
700 // check if token limit was reached822 // check if token limit was reached
701 const tokens = getTextTokens(tokenizers.GPT2, getMemoryString()).length;823 const tokens = await countSourceTokens(getMemoryString());
702 if (tokens >= CONTEXT_SIZE) {824 if (tokens >= CONTEXT_SIZE) {
703 break;825 break;
704 }826 }
705 }827 }
706828
707 const resultingString = getMemoryString();829 const resultingString = getMemoryString();
708 const resultingTokens = getTextTokens(tokenizers.GPT2, resultingString).length;830 const resultingTokens = await countSourceTokens(resultingString);
709831
710 if (!resultingString || resultingTokens < CONTEXT_SIZE) {832 if (!resultingString || resultingTokens < CONTEXT_SIZE) {
711 console.debug('Not enough context to summarize');833 console.debug('Not enough context to summarize');
@@ -890,7 +1012,7 @@ function setupListeners() {
890 $('#memory_prompt_words').off('click').on('input', onMemoryPromptWordsInput);1012 $('#memory_prompt_words').off('click').on('input', onMemoryPromptWordsInput);
891 $('#memory_prompt_interval').off('click').on('input', onMemoryPromptIntervalInput);1013 $('#memory_prompt_interval').off('click').on('input', onMemoryPromptIntervalInput);
892 $('#memory_prompt').off('click').on('input', onMemoryPromptInput);1014 $('#memory_prompt').off('click').on('input', onMemoryPromptInput);
893 $('#memory_force_summarize').off('click').on('click', forceSummarizeChat);1015 $('#memory_force_summarize').off('click').on('click', () => forceSummarizeChat(false));
894 $('#memory_template').off('click').on('input', onMemoryTemplateInput);1016 $('#memory_template').off('click').on('input', onMemoryTemplateInput);
895 $('#memory_depth').off('click').on('input', onMemoryDepthInput);1017 $('#memory_depth').off('click').on('input', onMemoryDepthInput);
896 $('#memory_role').off('click').on('input', onMemoryRoleInput);1018 $('#memory_role').off('click').on('input', onMemoryRoleInput);
@@ -933,13 +1055,20 @@ jQuery(async function () {
933 name: 'summarize',1055 name: 'summarize',
934 callback: summarizeCallback,1056 callback: summarizeCallback,
935 namedArgumentList: [1057 namedArgumentList: [
936 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']),1058 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', Object.values(summary_sources)),
937 SlashCommandNamedArgument.fromProps({1059 SlashCommandNamedArgument.fromProps({
938 name: 'prompt',1060 name: 'prompt',
939 description: 'prompt to use for summarization',1061 description: 'prompt to use for summarization',
940 typeList: [ARGUMENT_TYPE.STRING],1062 typeList: [ARGUMENT_TYPE.STRING],
941 defaultValue: '',1063 defaultValue: '',
942 }),1064 }),
1065 SlashCommandNamedArgument.fromProps({
1066 name: 'quiet',
1067 description: 'suppress the toast message when summarizing the chat',
1068 typeList: [ARGUMENT_TYPE.BOOLEAN],
1069 defaultValue: 'false',
1070 enumList: commonEnumProviders.boolean('trueFalse')(),
1071 }),
943 ],1072 ],
944 unnamedArgumentList: [1073 unnamedArgumentList: [
945 new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),1074 new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
public/scripts/extensions/memory/settings.html+4 -3
@@ -13,6 +13,7 @@
13 <select id="summary_source">13 <select id="summary_source">
14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
15 <option value="extras">Extras API</option>15 <option value="extras">Extras API</option>
16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
16 </select><br>17 </select><br>
1718
18 <div class="flex-container justifyspacebetween alignitemscenter">19 <div class="flex-container justifyspacebetween alignitemscenter">
@@ -24,7 +25,7 @@
2425
25 <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>26 <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>
26 <div class="memory_contents_controls">27 <div class="memory_contents_controls">
27 <div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="[title]ext_sum_force_tip">28 <div id="memory_force_summarize" data-summary-source="main,webllm" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="[title]ext_sum_force_tip">
28 <i class="fa-solid fa-database"></i>29 <i class="fa-solid fa-database"></i>
29 <span data-i18n="ext_sum_force_text">Summarize now</span>30 <span data-i18n="ext_sum_force_text">Summarize now</span>
30 </div>31 </div>
@@ -58,7 +59,7 @@
58 <span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>59 <span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>
59 </label>60 </label>
60 </div>61 </div>
61 <div data-summary-source="main">62 <div data-summary-source="main,webllm">
62 <label for="memory_prompt" class="title_restorable">63 <label for="memory_prompt" class="title_restorable">
63 <span data-i18n="Summary Prompt">Summary Prompt</span>64 <span data-i18n="Summary Prompt">Summary Prompt</span>
64 <div id="memory_prompt_restore" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">65 <div id="memory_prompt_restore" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">
@@ -74,7 +75,7 @@
74 </label>75 </label>
75 <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />76 <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />
76 <label for="memory_max_messages_per_request">77 <label for="memory_max_messages_per_request">
77 <span data-i18n="ext_sum_raw_max_msg">[Raw] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)78 <span data-i18n="ext_sum_raw_max_msg">[Raw/WebLLM] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)
78 <small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>79 <small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>
79 </label>80 </label>
80 <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />81 <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />
public/scripts/extensions/quick-reply/api/QuickReplyApi.js+94 -66
@@ -24,9 +24,17 @@ export class QuickReplyApi {
2424
2525
26 /**26 /**
27 * @param {QuickReply} qr
28 * @returns {QuickReplySet}
29 */
30 getSetByQr(qr) {
31 return QuickReplySet.list.find(it=>it.qrList.includes(qr));
32 }
33
34 /**
27 * Finds and returns an existing Quick Reply Set by its name.35 * Finds and returns an existing Quick Reply Set by its name.
28 *36 *
29 * @param {String} name name of the quick reply set37 * @param {string} name name of the quick reply set
30 * @returns the quick reply set, or undefined if not found38 * @returns the quick reply set, or undefined if not found
31 */39 */
32 getSetByName(name) {40 getSetByName(name) {
@@ -36,13 +44,14 @@ export class QuickReplyApi {
36 /**44 /**
37 * Finds and returns an existing Quick Reply by its set's name and its label.45 * Finds and returns an existing Quick Reply by its set's name and its label.
38 *46 *
39 * @param {String} setName name of the quick reply set47 * @param {string} setName name of the quick reply set
40 * @param {String} label label of the quick reply48 * @param {string|number} label label or numeric ID of the quick reply
41 * @returns the quick reply, or undefined if not found49 * @returns the quick reply, or undefined if not found
42 */50 */
43 getQrByLabel(setName, label) {51 getQrByLabel(setName, label) {
44 const set = this.getSetByName(setName);52 const set = this.getSetByName(setName);
45 if (!set) return;53 if (!set) return;
54 if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label);
46 return set.qrList.find(it=>it.label == label);55 return set.qrList.find(it=>it.label == label);
47 }56 }
4857
@@ -70,24 +79,25 @@ export class QuickReplyApi {
70 /**79 /**
71 * Executes an existing quick reply.80 * Executes an existing quick reply.
72 *81 *
73 * @param {String} setName name of the existing quick reply set82 * @param {string} setName name of the existing quick reply set
74 * @param {String} label label of the existing quick reply (text on the button)83 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
75 * @param {Object} [args] optional arguments84 * @param {object} [args] optional arguments
85 * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
76 */86 */
77 async executeQuickReply(setName, label, args = {}) {87 async executeQuickReply(setName, label, args = {}, options = {}) {
78 const qr = this.getQrByLabel(setName, label);88 const qr = this.getQrByLabel(setName, label);
79 if (!qr) {89 if (!qr) {
80 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);90 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
81 }91 }
82 return await qr.execute(args);92 return await qr.execute(args, false, false, options);
83 }93 }
8494
8595
86 /**96 /**
87 * Adds or removes a quick reply set to the list of globally active quick reply sets.97 * Adds or removes a quick reply set to the list of globally active quick reply sets.
88 *98 *
89 * @param {String} name the name of the set99 * @param {string} name the name of the set
90 * @param {Boolean} isVisible whether to show the set's buttons or not100 * @param {boolean} isVisible whether to show the set's buttons or not
91 */101 */
92 toggleGlobalSet(name, isVisible = true) {102 toggleGlobalSet(name, isVisible = true) {
93 const set = this.getSetByName(name);103 const set = this.getSetByName(name);
@@ -104,8 +114,8 @@ export class QuickReplyApi {
104 /**114 /**
105 * Adds a quick reply set to the list of globally active quick reply sets.115 * Adds a quick reply set to the list of globally active quick reply sets.
106 *116 *
107 * @param {String} name the name of the set117 * @param {string} name the name of the set
108 * @param {Boolean} isVisible whether to show the set's buttons or not118 * @param {boolean} isVisible whether to show the set's buttons or not
109 */119 */
110 addGlobalSet(name, isVisible = true) {120 addGlobalSet(name, isVisible = true) {
111 const set = this.getSetByName(name);121 const set = this.getSetByName(name);
@@ -118,7 +128,7 @@ export class QuickReplyApi {
118 /**128 /**
119 * Removes a quick reply set from the list of globally active quick reply sets.129 * Removes a quick reply set from the list of globally active quick reply sets.
120 *130 *
121 * @param {String} name the name of the set131 * @param {string} name the name of the set
122 */132 */
123 removeGlobalSet(name) {133 removeGlobalSet(name) {
124 const set = this.getSetByName(name);134 const set = this.getSetByName(name);
@@ -132,8 +142,8 @@ export class QuickReplyApi {
132 /**142 /**
133 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.143 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
134 *144 *
135 * @param {String} name the name of the set145 * @param {string} name the name of the set
136 * @param {Boolean} isVisible whether to show the set's buttons or not146 * @param {boolean} isVisible whether to show the set's buttons or not
137 */147 */
138 toggleChatSet(name, isVisible = true) {148 toggleChatSet(name, isVisible = true) {
139 if (!this.settings.chatConfig) return;149 if (!this.settings.chatConfig) return;
@@ -151,8 +161,8 @@ export class QuickReplyApi {
151 /**161 /**
152 * Adds a quick reply set to the list of the current chat's active quick reply sets.162 * Adds a quick reply set to the list of the current chat's active quick reply sets.
153 *163 *
154 * @param {String} name the name of the set164 * @param {string} name the name of the set
155 * @param {Boolean} isVisible whether to show the set's buttons or not165 * @param {boolean} isVisible whether to show the set's buttons or not
156 */166 */
157 addChatSet(name, isVisible = true) {167 addChatSet(name, isVisible = true) {
158 if (!this.settings.chatConfig) return;168 if (!this.settings.chatConfig) return;
@@ -166,7 +176,7 @@ export class QuickReplyApi {
166 /**176 /**
167 * Removes a quick reply set from the list of the current chat's active quick reply sets.177 * Removes a quick reply set from the list of the current chat's active quick reply sets.
168 *178 *
169 * @param {String} name the name of the set179 * @param {string} name the name of the set
170 */180 */
171 removeChatSet(name) {181 removeChatSet(name) {
172 if (!this.settings.chatConfig) return;182 if (!this.settings.chatConfig) return;
@@ -181,21 +191,26 @@ export class QuickReplyApi {
181 /**191 /**
182 * Creates a new quick reply in an existing quick reply set.192 * Creates a new quick reply in an existing quick reply set.
183 *193 *
184 * @param {String} setName name of the quick reply set to insert the new quick reply into194 * @param {string} setName name of the quick reply set to insert the new quick reply into
185 * @param {String} label label for the new quick reply (text on the button)195 * @param {string} label label for the new quick reply (text on the button)
186 * @param {Object} [props]196 * @param {object} [props]
187 * @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply197 * @param {string} [props.icon] the icon to show on the QR button
188 * @param {String} [props.title] the title / tooltip to be shown on the quick reply button198 * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
189 * @param {Boolean} [props.isHidden] whether to hide or show the button199 * @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply
190 * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts200 * @param {string} [props.title] the title / tooltip to be shown on the quick reply button
191 * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message201 * @param {boolean} [props.isHidden] whether to hide or show the button
192 * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message202 * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
193 * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded203 * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
194 * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected204 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
195 * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated205 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
206 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
207 * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
208 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
196 * @returns {QuickReply} the new quick reply209 * @returns {QuickReply} the new quick reply
197 */210 */
198 createQuickReply(setName, label, {211 createQuickReply(setName, label, {
212 icon,
213 showLabel,
199 message,214 message,
200 title,215 title,
201 isHidden,216 isHidden,
@@ -204,6 +219,7 @@ export class QuickReplyApi {
204 executeOnAi,219 executeOnAi,
205 executeOnChatChange,220 executeOnChatChange,
206 executeOnGroupMemberDraft,221 executeOnGroupMemberDraft,
222 executeOnNewChat,
207 automationId,223 automationId,
208 } = {}) {224 } = {}) {
209 const set = this.getSetByName(setName);225 const set = this.getSetByName(setName);
@@ -212,6 +228,8 @@ export class QuickReplyApi {
212 }228 }
213 const qr = set.addQuickReply();229 const qr = set.addQuickReply();
214 qr.label = label ?? '';230 qr.label = label ?? '';
231 qr.icon = icon ?? '';
232 qr.showLabel = showLabel ?? false;
215 qr.message = message ?? '';233 qr.message = message ?? '';
216 qr.title = title ?? '';234 qr.title = title ?? '';
217 qr.isHidden = isHidden ?? false;235 qr.isHidden = isHidden ?? false;
@@ -220,6 +238,7 @@ export class QuickReplyApi {
220 qr.executeOnAi = executeOnAi ?? false;238 qr.executeOnAi = executeOnAi ?? false;
221 qr.executeOnChatChange = executeOnChatChange ?? false;239 qr.executeOnChatChange = executeOnChatChange ?? false;
222 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;240 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
241 qr.executeOnNewChat = executeOnNewChat ?? false;
223 qr.automationId = automationId ?? '';242 qr.automationId = automationId ?? '';
224 qr.onUpdate();243 qr.onUpdate();
225 return qr;244 return qr;
@@ -228,22 +247,27 @@ export class QuickReplyApi {
228 /**247 /**
229 * Updates an existing quick reply.248 * Updates an existing quick reply.
230 *249 *
231 * @param {String} setName name of the existing quick reply set250 * @param {string} setName name of the existing quick reply set
232 * @param {String} label label of the existing quick reply (text on the button)251 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
233 * @param {Object} [props]252 * @param {object} [props]
234 * @param {String} [props.newLabel] new label for quick reply (text on the button)253 * @param {string} [props.icon] the icon to show on the QR button
235 * @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply254 * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
236 * @param {String} [props.title] the title / tooltip to be shown on the quick reply button255 * @param {string} [props.newLabel] new label for quick reply (text on the button)
237 * @param {Boolean} [props.isHidden] whether to hide or show the button256 * @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply
238 * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts257 * @param {string} [props.title] the title / tooltip to be shown on the quick reply button
239 * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message258 * @param {boolean} [props.isHidden] whether to hide or show the button
240 * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message259 * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
241 * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded260 * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
242 * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected261 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
243 * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated262 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
263 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
264 * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
265 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
244 * @returns {QuickReply} the altered quick reply266 * @returns {QuickReply} the altered quick reply
245 */267 */
246 updateQuickReply(setName, label, {268 updateQuickReply(setName, label, {
269 icon,
270 showLabel,
247 newLabel,271 newLabel,
248 message,272 message,
249 title,273 title,
@@ -253,12 +277,15 @@ export class QuickReplyApi {
253 executeOnAi,277 executeOnAi,
254 executeOnChatChange,278 executeOnChatChange,
255 executeOnGroupMemberDraft,279 executeOnGroupMemberDraft,
280 executeOnNewChat,
256 automationId,281 automationId,
257 } = {}) {282 } = {}) {
258 const qr = this.getQrByLabel(setName, label);283 const qr = this.getQrByLabel(setName, label);
259 if (!qr) {284 if (!qr) {
260 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);285 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
261 }286 }
287 qr.updateIcon(icon ?? qr.icon);
288 qr.updateShowLabel(showLabel ?? qr.showLabel);
262 qr.updateLabel(newLabel ?? qr.label);289 qr.updateLabel(newLabel ?? qr.label);
263 qr.updateMessage(message ?? qr.message);290 qr.updateMessage(message ?? qr.message);
264 qr.updateTitle(title ?? qr.title);291 qr.updateTitle(title ?? qr.title);
@@ -268,6 +295,7 @@ export class QuickReplyApi {
268 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;295 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
269 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;296 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
270 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;297 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
298 qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
271 qr.automationId = automationId ?? qr.automationId;299 qr.automationId = automationId ?? qr.automationId;
272 qr.onUpdate();300 qr.onUpdate();
273 return qr;301 return qr;
@@ -276,8 +304,8 @@ export class QuickReplyApi {
276 /**304 /**
277 * Deletes an existing quick reply.305 * Deletes an existing quick reply.
278 *306 *
279 * @param {String} setName name of the existing quick reply set307 * @param {string} setName name of the existing quick reply set
280 * @param {String} label label of the existing quick reply (text on the button)308 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
281 */309 */
282 deleteQuickReply(setName, label) {310 deleteQuickReply(setName, label) {
283 const qr = this.getQrByLabel(setName, label);311 const qr = this.getQrByLabel(setName, label);
@@ -291,10 +319,10 @@ export class QuickReplyApi {
291 /**319 /**
292 * Adds an existing quick reply set as a context menu to an existing quick reply.320 * Adds an existing quick reply set as a context menu to an existing quick reply.
293 *321 *
294 * @param {String} setName name of the existing quick reply set containing the quick reply322 * @param {string} setName name of the existing quick reply set containing the quick reply
295 * @param {String} label label of the existing quick reply323 * @param {string|number} label label of the existing quick reply or its numeric ID
296 * @param {String} contextSetName name of the existing quick reply set to be used as a context menu324 * @param {string} contextSetName name of the existing quick reply set to be used as a context menu
297 * @param {Boolean} isChained whether or not to chain the context menu quick replies325 * @param {boolean} isChained whether or not to chain the context menu quick replies
298 */326 */
299 createContextItem(setName, label, contextSetName, isChained = false) {327 createContextItem(setName, label, contextSetName, isChained = false) {
300 const qr = this.getQrByLabel(setName, label);328 const qr = this.getQrByLabel(setName, label);
@@ -314,9 +342,9 @@ export class QuickReplyApi {
314 /**342 /**
315 * Removes a quick reply set from a quick reply's context menu.343 * Removes a quick reply set from a quick reply's context menu.
316 *344 *
317 * @param {String} setName name of the existing quick reply set containing the quick reply345 * @param {string} setName name of the existing quick reply set containing the quick reply
318 * @param {String} label label of the existing quick reply346 * @param {string|number} label label of the existing quick reply or its numeric ID
319 * @param {String} contextSetName name of the existing quick reply set to be used as a context menu347 * @param {string} contextSetName name of the existing quick reply set to be used as a context menu
320 */348 */
321 deleteContextItem(setName, label, contextSetName) {349 deleteContextItem(setName, label, contextSetName) {
322 const qr = this.getQrByLabel(setName, label);350 const qr = this.getQrByLabel(setName, label);
@@ -333,8 +361,8 @@ export class QuickReplyApi {
333 /**361 /**
334 * Removes all entries from a quick reply's context menu.362 * Removes all entries from a quick reply's context menu.
335 *363 *
336 * @param {String} setName name of the existing quick reply set containing the quick reply364 * @param {string} setName name of the existing quick reply set containing the quick reply
337 * @param {String} label label of the existing quick reply365 * @param {string|number} label label of the existing quick reply or its numeric ID
338 */366 */
339 clearContextMenu(setName, label) {367 clearContextMenu(setName, label) {
340 const qr = this.getQrByLabel(setName, label);368 const qr = this.getQrByLabel(setName, label);
@@ -348,11 +376,11 @@ export class QuickReplyApi {
348 /**376 /**
349 * Create a new quick reply set.377 * Create a new quick reply set.
350 *378 *
351 * @param {String} name name of the new quick reply set379 * @param {string} name name of the new quick reply set
352 * @param {Object} [props]380 * @param {object} [props]
353 * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box381 * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
354 * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input382 * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
355 * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply383 * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
356 * @returns {Promise<QuickReplySet>} the new quick reply set384 * @returns {Promise<QuickReplySet>} the new quick reply set
357 */385 */
358 async createSet(name, {386 async createSet(name, {
@@ -384,11 +412,11 @@ export class QuickReplyApi {
384 /**412 /**
385 * Update an existing quick reply set.413 * Update an existing quick reply set.
386 *414 *
387 * @param {String} name name of the existing quick reply set415 * @param {string} name name of the existing quick reply set
388 * @param {Object} [props]416 * @param {object} [props]
389 * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box417 * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
390 * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input418 * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
391 * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply419 * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
392 * @returns {Promise<QuickReplySet>} the altered quick reply set420 * @returns {Promise<QuickReplySet>} the altered quick reply set
393 */421 */
394 async updateSet(name, {422 async updateSet(name, {
@@ -411,7 +439,7 @@ export class QuickReplyApi {
411 /**439 /**
412 * Delete an existing quick reply set.440 * Delete an existing quick reply set.
413 *441 *
414 * @param {String} name name of the existing quick reply set442 * @param {string} name name of the existing quick reply set
415 */443 */
416 async deleteSet(name) {444 async deleteSet(name) {
417 const set = this.getSetByName(name);445 const set = this.getSetByName(name);
@@ -451,7 +479,7 @@ export class QuickReplyApi {
451 /**479 /**
452 * Gets a list of all quick replies in the quick reply set.480 * Gets a list of all quick replies in the quick reply set.
453 *481 *
454 * @param {String} setName name of the existing quick reply set482 * @param {string} setName name of the existing quick reply set
455 * @returns array with the labels of this set's quick replies483 * @returns array with the labels of this set's quick replies
456 */484 */
457 listQuickReplies(setName) {485 listQuickReplies(setName) {
public/scripts/extensions/quick-reply/html/qrEditor.html+38 -8
@@ -2,10 +2,23 @@
2 <div id="qr--main">2 <div id="qr--main">
3 <h3 data-i18n="Labels and Message">Labels and Message</h3>3 <h3 data-i18n="Labels and Message">Labels and Message</h3>
4 <div class="qr--labels">4 <div class="qr--labels">
5 <label>5 <label class="qr--fit">
6 <span class="qr--labelText" data-i18n="Label">Label</span>6 <span class="qr--labelText" data-i18n="Label">Icon</span>
7 <input type="text" class="text_pole" id="qr--modal-label">7 <small class="qr--labelHint">&nbsp;</small>
8 <div class="menu_button fa-fw" id="qr--modal-icon" title="Click to change icon"></div>
8 </label>9 </label>
10 <div class="label">
11 <span class="qr--labelText" data-i18n="Label">Label</span>
12 <small class="qr--labelHint" data-i18n="(label of the button, if no icon is chosen) ">(label of the button, if no icon is chosen)</small>
13 <div class="qr--inputGroup">
14 <label class="checkbox_label" title="Show label even if an icon is assigned">
15 <input type="checkbox" id="qr--modal-showLabel">
16 Show
17 </label>
18 <input type="text" class="text_pole" id="qr--modal-label">
19 <div class="menu_button fa-fw fa-solid fa-chevron-down" id="qr--modal-switcher" title="Switch to another QR"></div>
20 </div>
21 </div>
9 <label>22 <label>
10 <span class="qr--labelText" data-i18n="Title">Title</span>23 <span class="qr--labelText" data-i18n="Title">Title</span>
11 <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>24 <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>
@@ -33,6 +46,8 @@
33 <input type="checkbox" id="qr--modal-syntax">46 <input type="checkbox" id="qr--modal-syntax">
34 <span>Syntax highlight</span>47 <span>Syntax highlight</span>
35 </label>48 </label>
49 <small>Ctrl+Alt+Click (or F9) to set / remove breakpoints</small>
50 <small>Ctrl+<span id="qr--modal-commentKey"></span> to toggle block comments</small>
36 </div>51 </div>
37 <div id="qr--modal-messageHolder">52 <div id="qr--modal-messageHolder">
38 <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>53 <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
@@ -43,6 +58,10 @@
4358
4459
4560
61 <div id="qr--resizeHandle"></div>
62
63
64
46 <div id="qr--qrOptions">65 <div id="qr--qrOptions">
47 <h3 data-i18n="Context Menu">Context Menu</h3>66 <h3 data-i18n="Context Menu">Context Menu</h3>
48 <div id="qr--ctxEditor">67 <div id="qr--ctxEditor">
@@ -64,7 +83,7 @@
6483
6584
66 <h3 data-i18n="Auto-Execute">Auto-Execute</h3>85 <h3 data-i18n="Auto-Execute">Auto-Execute</h3>
67 <div class="flex-container flexFlowColumn">86 <div id="qr--autoExec" class="flex-container flexFlowColumn">
68 <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">87 <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
69 <input type="checkbox" id="qr--preventAutoExecute" >88 <input type="checkbox" id="qr--preventAutoExecute" >
70 <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>89 <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>
@@ -90,6 +109,10 @@
90 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>109 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>
91 </label>110 </label>
92 <label class="checkbox_label">111 <label class="checkbox_label">
112 <input type="checkbox" id="qr--executeOnNewChat">
113 <span><i class="fa-solid fa-fw fa-comments"></i><span data-i18n="Execute on new chat">Execute on new chat</span></span>
114 </label>
115 <label class="checkbox_label">
93 <input type="checkbox" id="qr--executeOnGroupMemberDraft">116 <input type="checkbox" id="qr--executeOnGroupMemberDraft">
94 <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>117 <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>
95 </label>118 </label>
@@ -117,11 +140,18 @@
117 </div>140 </div>
118 </div>141 </div>
119 <div id="qr--modal-executeProgress"></div>142 <div id="qr--modal-executeProgress"></div>
120 <label class="checkbox_label">
121 <input type="checkbox" id="qr--modal-executeHide">
122 <span title="Hide editor while executing"> Hide editor while executing</span>
123 </label>
124 <div id="qr--modal-executeErrors"></div>143 <div id="qr--modal-executeErrors"></div>
125 <div id="qr--modal-executeResult"></div>144 <div id="qr--modal-executeResult"></div>
145
146 <div id="qr--modal-debugButtons">
147 <div title="Resume" id="qr--modal-resume" class="qr--modal-debugButton menu_button"></div>
148 <div title="Step Over" id="qr--modal-step" class="qr--modal-debugButton menu_button"></div>
149 <div title="Step Into" id="qr--modal-stepInto" class="qr--modal-debugButton menu_button"></div>
150 <div title="Step Out" id="qr--modal-stepOut" class="qr--modal-debugButton menu_button"></div>
151 <div title="Minimize" id="qr--modal-minimize" class="qr--modal-debugButton menu_button fa-solid fa-minimize"></div>
152 <div title="Maximize" id="qr--modal-maximize" class="qr--modal-debugButton menu_button fa-solid fa-maximize"></div>
153 </div>
154 <textarea rows="1" id="qr--modal-send_textarea" placeholder="Chat input for use with {{input}}" title="Chat input for use with {{input}}"></textarea>
155 <div id="qr--modal-debugState"></div>
126 </div>156 </div>
127</div>157</div>
public/scripts/extensions/quick-reply/html/settings.html+13 -0
@@ -11,6 +11,9 @@
11 <label class="flex-container">11 <label class="flex-container">
12 <input type="checkbox" id="qr--isCombined"><span data-i18n="Combine Quick Replies">Combine Quick Replies</span>12 <input type="checkbox" id="qr--isCombined"><span data-i18n="Combine Quick Replies">Combine Quick Replies</span>
13 </label>13 </label>
14 <label class="flex-container">
15 <input type="checkbox" id="qr--showPopoutButton"><span data-i18n="Show Popout Button">Show Popout Button</span>
16 </label>
1417
15 <hr>18 <hr>
1619
@@ -60,10 +63,20 @@
60 <label class="flex-container" id="qr--injectInputContainer">63 <label class="flex-container" id="qr--injectInputContainer">
61 <input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>64 <input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>
62 </label>65 </label>
66 <div class="flex-container alignItemsCenter">
67 <toolcool-color-picker id="qr--color"></toolcool-color-picker>
68 <div class="menu_button" id="qr--colorClear">Clear</div>
69 <span data-i18n="Color">Color</span>
70 </div>
71 <label class="flex-container" id="qr--onlyBorderColorContainer">
72 <input type="checkbox" id="qr--onlyBorderColor"> <span data-i18n="Only apply color as accent">Only apply color as accent</span>
73 </label>
63 </div>74 </div>
64 <div id="qr--set-qrList" class="qr--qrList"></div>75 <div id="qr--set-qrList" class="qr--qrList"></div>
65 <div class="qr--set-qrListActions">76 <div class="qr--set-qrListActions">
66 <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>77 <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
78 <div class="qr--paste menu_button menu_button_icon fa-solid fa-paste" id="qr--set-paste" title="Paste quick reply from clipboard"></div>
79 <div class="qr--import menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-importQr" title="Import quick reply from file"></div>
67 </div>80 </div>
68 </div>81 </div>
69 </div>82 </div>
public/scripts/extensions/quick-reply/index.js+8 -2
@@ -105,6 +105,7 @@ const loadSets = async () => {
105 qr.executeOnAi = slot.autoExecute_botMessage ?? false;105 qr.executeOnAi = slot.autoExecute_botMessage ?? false;
106 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;106 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
107 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;107 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
108 qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
108 qr.automationId = slot.automationId ?? '';109 qr.automationId = slot.automationId ?? '';
109 qr.contextList = (slot.contextMenu ?? []).map(it=>({110 qr.contextList = (slot.contextMenu ?? []).map(it=>({
110 set: it.preset,111 set: it.preset,
@@ -176,7 +177,7 @@ const init = async () => {
176 buttons.show();177 buttons.show();
177 settings.onSave = ()=>buttons.refresh();178 settings.onSave = ()=>buttons.refresh();
178179
179 window['executeQuickReplyByName'] = async(name, args = {}) => {180 window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
180 let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]181 let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
181 .map(it=>it.set.qrList)182 .map(it=>it.set.qrList)
182 .flat()183 .flat()
@@ -191,7 +192,7 @@ const init = async () => {
191 }192 }
192 }193 }
193 if (qr && qr.onExecute) {194 if (qr && qr.onExecute) {
194 return await qr.execute(args, false, true);195 return await qr.execute(args, false, true, options);
195 } else {196 } else {
196 throw new Error(`No Quick Reply found for "${name}".`);197 throw new Error(`No Quick Reply found for "${name}".`);
197 }198 }
@@ -260,3 +261,8 @@ const onWIActivation = async (entries) => {
260 await autoExec.handleWIActivation(entries);261 await autoExec.handleWIActivation(entries);
261};262};
262eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));263eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));
264
265const onNewChat = async () => {
266 await autoExec.handleNewChat();
267};
268eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
public/scripts/extensions/quick-reply/lib/morphdom-esm.js+769 -0
@@ -0,0 +1,769 @@
1var DOCUMENT_FRAGMENT_NODE = 11;
2
3function morphAttrs(fromNode, toNode) {
4 var toNodeAttrs = toNode.attributes;
5 var attr;
6 var attrName;
7 var attrNamespaceURI;
8 var attrValue;
9 var fromValue;
10
11 // document-fragments dont have attributes so lets not do anything
12 if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
13 return;
14 }
15
16 // update attributes on original DOM element
17 for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
18 attr = toNodeAttrs[i];
19 attrName = attr.name;
20 attrNamespaceURI = attr.namespaceURI;
21 attrValue = attr.value;
22
23 if (attrNamespaceURI) {
24 attrName = attr.localName || attrName;
25 fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
26
27 if (fromValue !== attrValue) {
28 if (attr.prefix === 'xmlns'){
29 attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
30 }
31 fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
32 }
33 } else {
34 fromValue = fromNode.getAttribute(attrName);
35
36 if (fromValue !== attrValue) {
37 fromNode.setAttribute(attrName, attrValue);
38 }
39 }
40 }
41
42 // Remove any extra attributes found on the original DOM element that
43 // weren't found on the target element.
44 var fromNodeAttrs = fromNode.attributes;
45
46 for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
47 attr = fromNodeAttrs[d];
48 attrName = attr.name;
49 attrNamespaceURI = attr.namespaceURI;
50
51 if (attrNamespaceURI) {
52 attrName = attr.localName || attrName;
53
54 if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
55 fromNode.removeAttributeNS(attrNamespaceURI, attrName);
56 }
57 } else {
58 if (!toNode.hasAttribute(attrName)) {
59 fromNode.removeAttribute(attrName);
60 }
61 }
62 }
63}
64
65var range; // Create a range object for efficently rendering strings to elements.
66var NS_XHTML = 'http://www.w3.org/1999/xhtml';
67
68var doc = typeof document === 'undefined' ? undefined : document;
69var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
70var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
71
72function createFragmentFromTemplate(str) {
73 var template = doc.createElement('template');
74 template.innerHTML = str;
75 return template.content.childNodes[0];
76}
77
78function createFragmentFromRange(str) {
79 if (!range) {
80 range = doc.createRange();
81 range.selectNode(doc.body);
82 }
83
84 var fragment = range.createContextualFragment(str);
85 return fragment.childNodes[0];
86}
87
88function createFragmentFromWrap(str) {
89 var fragment = doc.createElement('body');
90 fragment.innerHTML = str;
91 return fragment.childNodes[0];
92}
93
94/**
95 * This is about the same
96 * var html = new DOMParser().parseFromString(str, 'text/html');
97 * return html.body.firstChild;
98 *
99 * @method toElement
100 * @param {String} str
101 */
102function toElement(str) {
103 str = str.trim();
104 if (HAS_TEMPLATE_SUPPORT) {
105 // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
106 // createContextualFragment doesn't support
107 // <template> support not available in IE
108 return createFragmentFromTemplate(str);
109 } else if (HAS_RANGE_SUPPORT) {
110 return createFragmentFromRange(str);
111 }
112
113 return createFragmentFromWrap(str);
114}
115
116/**
117 * Returns true if two node's names are the same.
118 *
119 * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
120 * nodeName and different namespace URIs.
121 *
122 * @param {Element} a
123 * @param {Element} b The target element
124 * @return {boolean}
125 */
126function compareNodeNames(fromEl, toEl) {
127 var fromNodeName = fromEl.nodeName;
128 var toNodeName = toEl.nodeName;
129 var fromCodeStart, toCodeStart;
130
131 if (fromNodeName === toNodeName) {
132 return true;
133 }
134
135 fromCodeStart = fromNodeName.charCodeAt(0);
136 toCodeStart = toNodeName.charCodeAt(0);
137
138 // If the target element is a virtual DOM node or SVG node then we may
139 // need to normalize the tag name before comparing. Normal HTML elements that are
140 // in the "http://www.w3.org/1999/xhtml"
141 // are converted to upper case
142 if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
143 return fromNodeName === toNodeName.toUpperCase();
144 } else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
145 return toNodeName === fromNodeName.toUpperCase();
146 } else {
147 return false;
148 }
149}
150
151/**
152 * Create an element, optionally with a known namespace URI.
153 *
154 * @param {string} name the element name, e.g. 'div' or 'svg'
155 * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
156 * its `xmlns` attribute or its inferred namespace.
157 *
158 * @return {Element}
159 */
160function createElementNS(name, namespaceURI) {
161 return !namespaceURI || namespaceURI === NS_XHTML ?
162 doc.createElement(name) :
163 doc.createElementNS(namespaceURI, name);
164}
165
166/**
167 * Copies the children of one DOM element to another DOM element
168 */
169function moveChildren(fromEl, toEl) {
170 var curChild = fromEl.firstChild;
171 while (curChild) {
172 var nextChild = curChild.nextSibling;
173 toEl.appendChild(curChild);
174 curChild = nextChild;
175 }
176 return toEl;
177}
178
179function syncBooleanAttrProp(fromEl, toEl, name) {
180 if (fromEl[name] !== toEl[name]) {
181 fromEl[name] = toEl[name];
182 if (fromEl[name]) {
183 fromEl.setAttribute(name, '');
184 } else {
185 fromEl.removeAttribute(name);
186 }
187 }
188}
189
190var specialElHandlers = {
191 OPTION: function(fromEl, toEl) {
192 var parentNode = fromEl.parentNode;
193 if (parentNode) {
194 var parentName = parentNode.nodeName.toUpperCase();
195 if (parentName === 'OPTGROUP') {
196 parentNode = parentNode.parentNode;
197 parentName = parentNode && parentNode.nodeName.toUpperCase();
198 }
199 if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
200 if (fromEl.hasAttribute('selected') && !toEl.selected) {
201 // Workaround for MS Edge bug where the 'selected' attribute can only be
202 // removed if set to a non-empty value:
203 // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
204 fromEl.setAttribute('selected', 'selected');
205 fromEl.removeAttribute('selected');
206 }
207 // We have to reset select element's selectedIndex to -1, otherwise setting
208 // fromEl.selected using the syncBooleanAttrProp below has no effect.
209 // The correct selectedIndex will be set in the SELECT special handler below.
210 parentNode.selectedIndex = -1;
211 }
212 }
213 syncBooleanAttrProp(fromEl, toEl, 'selected');
214 },
215 /**
216 * The "value" attribute is special for the <input> element since it sets
217 * the initial value. Changing the "value" attribute without changing the
218 * "value" property will have no effect since it is only used to the set the
219 * initial value. Similar for the "checked" attribute, and "disabled".
220 */
221 INPUT: function(fromEl, toEl) {
222 syncBooleanAttrProp(fromEl, toEl, 'checked');
223 syncBooleanAttrProp(fromEl, toEl, 'disabled');
224
225 if (fromEl.value !== toEl.value) {
226 fromEl.value = toEl.value;
227 }
228
229 if (!toEl.hasAttribute('value')) {
230 fromEl.removeAttribute('value');
231 }
232 },
233
234 TEXTAREA: function(fromEl, toEl) {
235 var newValue = toEl.value;
236 if (fromEl.value !== newValue) {
237 fromEl.value = newValue;
238 }
239
240 var firstChild = fromEl.firstChild;
241 if (firstChild) {
242 // Needed for IE. Apparently IE sets the placeholder as the
243 // node value and vise versa. This ignores an empty update.
244 var oldValue = firstChild.nodeValue;
245
246 if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
247 return;
248 }
249
250 firstChild.nodeValue = newValue;
251 }
252 },
253 SELECT: function(fromEl, toEl) {
254 if (!toEl.hasAttribute('multiple')) {
255 var selectedIndex = -1;
256 var i = 0;
257 // We have to loop through children of fromEl, not toEl since nodes can be moved
258 // from toEl to fromEl directly when morphing.
259 // At the time this special handler is invoked, all children have already been morphed
260 // and appended to / removed from fromEl, so using fromEl here is safe and correct.
261 var curChild = fromEl.firstChild;
262 var optgroup;
263 var nodeName;
264 while(curChild) {
265 nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
266 if (nodeName === 'OPTGROUP') {
267 optgroup = curChild;
268 curChild = optgroup.firstChild;
269 } else {
270 if (nodeName === 'OPTION') {
271 if (curChild.hasAttribute('selected')) {
272 selectedIndex = i;
273 break;
274 }
275 i++;
276 }
277 curChild = curChild.nextSibling;
278 if (!curChild && optgroup) {
279 curChild = optgroup.nextSibling;
280 optgroup = null;
281 }
282 }
283 }
284
285 fromEl.selectedIndex = selectedIndex;
286 }
287 }
288};
289
290var ELEMENT_NODE = 1;
291var DOCUMENT_FRAGMENT_NODE$1 = 11;
292var TEXT_NODE = 3;
293var COMMENT_NODE = 8;
294
295function noop() {}
296
297function defaultGetNodeKey(node) {
298 if (node) {
299 return (node.getAttribute && node.getAttribute('id')) || node.id;
300 }
301}
302
303function morphdomFactory(morphAttrs) {
304
305 return function morphdom(fromNode, toNode, options) {
306 if (!options) {
307 options = {};
308 }
309
310 if (typeof toNode === 'string') {
311 if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') {
312 var toNodeHtml = toNode;
313 toNode = doc.createElement('html');
314 toNode.innerHTML = toNodeHtml;
315 } else {
316 toNode = toElement(toNode);
317 }
318 } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
319 toNode = toNode.firstElementChild;
320 }
321
322 var getNodeKey = options.getNodeKey || defaultGetNodeKey;
323 var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
324 var onNodeAdded = options.onNodeAdded || noop;
325 var onBeforeElUpdated = options.onBeforeElUpdated || noop;
326 var onElUpdated = options.onElUpdated || noop;
327 var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
328 var onNodeDiscarded = options.onNodeDiscarded || noop;
329 var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
330 var skipFromChildren = options.skipFromChildren || noop;
331 var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); };
332 var childrenOnly = options.childrenOnly === true;
333
334 // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
335 var fromNodesLookup = Object.create(null);
336 var keyedRemovalList = [];
337
338 function addKeyedRemoval(key) {
339 keyedRemovalList.push(key);
340 }
341
342 function walkDiscardedChildNodes(node, skipKeyedNodes) {
343 if (node.nodeType === ELEMENT_NODE) {
344 var curChild = node.firstChild;
345 while (curChild) {
346
347 var key = undefined;
348
349 if (skipKeyedNodes && (key = getNodeKey(curChild))) {
350 // If we are skipping keyed nodes then we add the key
351 // to a list so that it can be handled at the very end.
352 addKeyedRemoval(key);
353 } else {
354 // Only report the node as discarded if it is not keyed. We do this because
355 // at the end we loop through all keyed elements that were unmatched
356 // and then discard them in one final pass.
357 onNodeDiscarded(curChild);
358 if (curChild.firstChild) {
359 walkDiscardedChildNodes(curChild, skipKeyedNodes);
360 }
361 }
362
363 curChild = curChild.nextSibling;
364 }
365 }
366 }
367
368 /**
369 * Removes a DOM node out of the original DOM
370 *
371 * @param {Node} node The node to remove
372 * @param {Node} parentNode The nodes parent
373 * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
374 * @return {undefined}
375 */
376 function removeNode(node, parentNode, skipKeyedNodes) {
377 if (onBeforeNodeDiscarded(node) === false) {
378 return;
379 }
380
381 if (parentNode) {
382 parentNode.removeChild(node);
383 }
384
385 onNodeDiscarded(node);
386 walkDiscardedChildNodes(node, skipKeyedNodes);
387 }
388
389 // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
390 // function indexTree(root) {
391 // var treeWalker = document.createTreeWalker(
392 // root,
393 // NodeFilter.SHOW_ELEMENT);
394 //
395 // var el;
396 // while((el = treeWalker.nextNode())) {
397 // var key = getNodeKey(el);
398 // if (key) {
399 // fromNodesLookup[key] = el;
400 // }
401 // }
402 // }
403
404 // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
405 //
406 // function indexTree(node) {
407 // var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
408 // var el;
409 // while((el = nodeIterator.nextNode())) {
410 // var key = getNodeKey(el);
411 // if (key) {
412 // fromNodesLookup[key] = el;
413 // }
414 // }
415 // }
416
417 function indexTree(node) {
418 if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
419 var curChild = node.firstChild;
420 while (curChild) {
421 var key = getNodeKey(curChild);
422 if (key) {
423 fromNodesLookup[key] = curChild;
424 }
425
426 // Walk recursively
427 indexTree(curChild);
428
429 curChild = curChild.nextSibling;
430 }
431 }
432 }
433
434 indexTree(fromNode);
435
436 function handleNodeAdded(el) {
437 onNodeAdded(el);
438
439 var curChild = el.firstChild;
440 while (curChild) {
441 var nextSibling = curChild.nextSibling;
442
443 var key = getNodeKey(curChild);
444 if (key) {
445 var unmatchedFromEl = fromNodesLookup[key];
446 // if we find a duplicate #id node in cache, replace `el` with cache value
447 // and morph it to the child node.
448 if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
449 curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
450 morphEl(unmatchedFromEl, curChild);
451 } else {
452 handleNodeAdded(curChild);
453 }
454 } else {
455 // recursively call for curChild and it's children to see if we find something in
456 // fromNodesLookup
457 handleNodeAdded(curChild);
458 }
459
460 curChild = nextSibling;
461 }
462 }
463
464 function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
465 // We have processed all of the "to nodes". If curFromNodeChild is
466 // non-null then we still have some from nodes left over that need
467 // to be removed
468 while (curFromNodeChild) {
469 var fromNextSibling = curFromNodeChild.nextSibling;
470 if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
471 // Since the node is keyed it might be matched up later so we defer
472 // the actual removal to later
473 addKeyedRemoval(curFromNodeKey);
474 } else {
475 // NOTE: we skip nested keyed nodes from being removed since there is
476 // still a chance they will be matched up later
477 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
478 }
479 curFromNodeChild = fromNextSibling;
480 }
481 }
482
483 function morphEl(fromEl, toEl, childrenOnly) {
484 var toElKey = getNodeKey(toEl);
485
486 if (toElKey) {
487 // If an element with an ID is being morphed then it will be in the final
488 // DOM so clear it out of the saved elements collection
489 delete fromNodesLookup[toElKey];
490 }
491
492 if (!childrenOnly) {
493 // optional
494 var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);
495 if (beforeUpdateResult === false) {
496 return;
497 } else if (beforeUpdateResult instanceof HTMLElement) {
498 fromEl = beforeUpdateResult;
499 // reindex the new fromEl in case it's not in the same
500 // tree as the original fromEl
501 // (Phoenix LiveView sometimes returns a cloned tree,
502 // but keyed lookups would still point to the original tree)
503 indexTree(fromEl);
504 }
505
506 // update attributes on original DOM element first
507 morphAttrs(fromEl, toEl);
508 // optional
509 onElUpdated(fromEl);
510
511 if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
512 return;
513 }
514 }
515
516 if (fromEl.nodeName !== 'TEXTAREA') {
517 morphChildren(fromEl, toEl);
518 } else {
519 specialElHandlers.TEXTAREA(fromEl, toEl);
520 }
521 }
522
523 function morphChildren(fromEl, toEl) {
524 var skipFrom = skipFromChildren(fromEl, toEl);
525 var curToNodeChild = toEl.firstChild;
526 var curFromNodeChild = fromEl.firstChild;
527 var curToNodeKey;
528 var curFromNodeKey;
529
530 var fromNextSibling;
531 var toNextSibling;
532 var matchingFromEl;
533
534 // walk the children
535 outer: while (curToNodeChild) {
536 toNextSibling = curToNodeChild.nextSibling;
537 curToNodeKey = getNodeKey(curToNodeChild);
538
539 // walk the fromNode children all the way through
540 while (!skipFrom && curFromNodeChild) {
541 fromNextSibling = curFromNodeChild.nextSibling;
542
543 if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
544 curToNodeChild = toNextSibling;
545 curFromNodeChild = fromNextSibling;
546 continue outer;
547 }
548
549 curFromNodeKey = getNodeKey(curFromNodeChild);
550
551 var curFromNodeType = curFromNodeChild.nodeType;
552
553 // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
554 var isCompatible = undefined;
555
556 if (curFromNodeType === curToNodeChild.nodeType) {
557 if (curFromNodeType === ELEMENT_NODE) {
558 // Both nodes being compared are Element nodes
559
560 if (curToNodeKey) {
561 // The target node has a key so we want to match it up with the correct element
562 // in the original DOM tree
563 if (curToNodeKey !== curFromNodeKey) {
564 // The current element in the original DOM tree does not have a matching key so
565 // let's check our lookup to see if there is a matching element in the original
566 // DOM tree
567 if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
568 if (fromNextSibling === matchingFromEl) {
569 // Special case for single element removals. To avoid removing the original
570 // DOM node out of the tree (since that can break CSS transitions, etc.),
571 // we will instead discard the current node and wait until the next
572 // iteration to properly match up the keyed target element with its matching
573 // element in the original tree
574 isCompatible = false;
575 } else {
576 // We found a matching keyed element somewhere in the original DOM tree.
577 // Let's move the original DOM node into the current position and morph
578 // it.
579
580 // NOTE: We use insertBefore instead of replaceChild because we want to go through
581 // the `removeNode()` function for the node that is being discarded so that
582 // all lifecycle hooks are correctly invoked
583 fromEl.insertBefore(matchingFromEl, curFromNodeChild);
584
585 // fromNextSibling = curFromNodeChild.nextSibling;
586
587 if (curFromNodeKey) {
588 // Since the node is keyed it might be matched up later so we defer
589 // the actual removal to later
590 addKeyedRemoval(curFromNodeKey);
591 } else {
592 // NOTE: we skip nested keyed nodes from being removed since there is
593 // still a chance they will be matched up later
594 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
595 }
596
597 curFromNodeChild = matchingFromEl;
598 curFromNodeKey = getNodeKey(curFromNodeChild);
599 }
600 } else {
601 // The nodes are not compatible since the "to" node has a key and there
602 // is no matching keyed node in the source tree
603 isCompatible = false;
604 }
605 }
606 } else if (curFromNodeKey) {
607 // The original has a key
608 isCompatible = false;
609 }
610
611 isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
612 if (isCompatible) {
613 // We found compatible DOM elements so transform
614 // the current "from" node to match the current
615 // target DOM node.
616 // MORPH
617 morphEl(curFromNodeChild, curToNodeChild);
618 }
619
620 } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
621 // Both nodes being compared are Text or Comment nodes
622 isCompatible = true;
623 // Simply update nodeValue on the original node to
624 // change the text value
625 if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
626 curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
627 }
628
629 }
630 }
631
632 if (isCompatible) {
633 // Advance both the "to" child and the "from" child since we found a match
634 // Nothing else to do as we already recursively called morphChildren above
635 curToNodeChild = toNextSibling;
636 curFromNodeChild = fromNextSibling;
637 continue outer;
638 }
639
640 // No compatible match so remove the old node from the DOM and continue trying to find a
641 // match in the original DOM. However, we only do this if the from node is not keyed
642 // since it is possible that a keyed node might match up with a node somewhere else in the
643 // target tree and we don't want to discard it just yet since it still might find a
644 // home in the final DOM tree. After everything is done we will remove any keyed nodes
645 // that didn't find a home
646 if (curFromNodeKey) {
647 // Since the node is keyed it might be matched up later so we defer
648 // the actual removal to later
649 addKeyedRemoval(curFromNodeKey);
650 } else {
651 // NOTE: we skip nested keyed nodes from being removed since there is
652 // still a chance they will be matched up later
653 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
654 }
655
656 curFromNodeChild = fromNextSibling;
657 } // END: while(curFromNodeChild) {}
658
659 // If we got this far then we did not find a candidate match for
660 // our "to node" and we exhausted all of the children "from"
661 // nodes. Therefore, we will just append the current "to" node
662 // to the end
663 if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
664 // MORPH
665 if(!skipFrom){ addChild(fromEl, matchingFromEl); }
666 morphEl(matchingFromEl, curToNodeChild);
667 } else {
668 var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
669 if (onBeforeNodeAddedResult !== false) {
670 if (onBeforeNodeAddedResult) {
671 curToNodeChild = onBeforeNodeAddedResult;
672 }
673
674 if (curToNodeChild.actualize) {
675 curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
676 }
677 addChild(fromEl, curToNodeChild);
678 handleNodeAdded(curToNodeChild);
679 }
680 }
681
682 curToNodeChild = toNextSibling;
683 curFromNodeChild = fromNextSibling;
684 }
685
686 cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
687
688 var specialElHandler = specialElHandlers[fromEl.nodeName];
689 if (specialElHandler) {
690 specialElHandler(fromEl, toEl);
691 }
692 } // END: morphChildren(...)
693
694 var morphedNode = fromNode;
695 var morphedNodeType = morphedNode.nodeType;
696 var toNodeType = toNode.nodeType;
697
698 if (!childrenOnly) {
699 // Handle the case where we are given two DOM nodes that are not
700 // compatible (e.g. <div> --> <span> or <div> --> TEXT)
701 if (morphedNodeType === ELEMENT_NODE) {
702 if (toNodeType === ELEMENT_NODE) {
703 if (!compareNodeNames(fromNode, toNode)) {
704 onNodeDiscarded(fromNode);
705 morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
706 }
707 } else {
708 // Going from an element node to a text node
709 morphedNode = toNode;
710 }
711 } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
712 if (toNodeType === morphedNodeType) {
713 if (morphedNode.nodeValue !== toNode.nodeValue) {
714 morphedNode.nodeValue = toNode.nodeValue;
715 }
716
717 return morphedNode;
718 } else {
719 // Text node to something else
720 morphedNode = toNode;
721 }
722 }
723 }
724
725 if (morphedNode === toNode) {
726 // The "to node" was not compatible with the "from node" so we had to
727 // toss out the "from node" and use the "to node"
728 onNodeDiscarded(fromNode);
729 } else {
730 if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
731 return;
732 }
733
734 morphEl(morphedNode, toNode, childrenOnly);
735
736 // We now need to loop over any keyed nodes that might need to be
737 // removed. We only do the removal if we know that the keyed node
738 // never found a match. When a keyed node is matched up we remove
739 // it out of fromNodesLookup and we use fromNodesLookup to determine
740 // if a keyed node has been matched up or not
741 if (keyedRemovalList) {
742 for (var i=0, len=keyedRemovalList.length; i<len; i++) {
743 var elToRemove = fromNodesLookup[keyedRemovalList[i]];
744 if (elToRemove) {
745 removeNode(elToRemove, elToRemove.parentNode, false);
746 }
747 }
748 }
749 }
750
751 if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
752 if (morphedNode.actualize) {
753 morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
754 }
755 // If we had to swap out the from node with a new node because the old
756 // node was not compatible with the target node then we need to
757 // replace the old DOM node in the original DOM tree. This is only
758 // possible if the original DOM node was part of a DOM tree which
759 // we know is the case if it has a parent node.
760 fromNode.parentNode.replaceChild(morphedNode, fromNode);
761 }
762
763 return morphedNode;
764 };
765}
766
767var morphdom = morphdomFactory(morphAttrs);
768
769export default morphdom;
public/scripts/extensions/quick-reply/lib/morphdom.LICENSE.txt+21 -0
@@ -0,0 +1,21 @@
1The MIT License (MIT)
2
3Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com)
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in
13all copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21THE SOFTWARE.
\ No newline at end of file21 \ No newline at end of file
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+9 -0
@@ -83,6 +83,15 @@ export class AutoExecuteHandler {
83 await this.performAutoExecute(qrList);83 await this.performAutoExecute(qrList);
84 }84 }
8585
86 async handleNewChat() {
87 if (!this.checkExecute()) return;
88 const qrList = [
89 ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat)).flat(),
90 ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat))?.flat() ?? []),
91 ];
92 await this.performAutoExecute(qrList);
93 }
94
86 /**95 /**
87 * @param {any[]} entries Set of activated entries96 * @param {any[]} entries Set of activated entries
88 */97 */
public/scripts/extensions/quick-reply/src/QuickReply.js+1285 -87
@@ -1,10 +1,17 @@
1import { POPUP_TYPE, Popup } from '../../../popup.js';1import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js';
2import { setSlashCommandAutoComplete } from '../../../slash-commands.js';2import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
3import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';3import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
4import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js';
5import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
6import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js';
7import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
8import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js';
9import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
4import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';10import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';11import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { debounce, getSortableDelay } from '../../../utils.js';12import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
7import { log, warn } from '../index.js';13import { log, quickReplyApi, warn } from '../index.js';
14import morphdom from '../lib/morphdom-esm.js';
8import { QuickReplyContextLink } from './QuickReplyContextLink.js';15import { QuickReplyContextLink } from './QuickReplyContextLink.js';
9import { QuickReplySet } from './QuickReplySet.js';16import { QuickReplySet } from './QuickReplySet.js';
10import { ContextMenu } from './ui/ctx/ContextMenu.js';17import { ContextMenu } from './ui/ctx/ContextMenu.js';
@@ -21,48 +28,62 @@ export class QuickReply {
2128
2229
2330
24 /**@type {Number}*/ id;31 /**@type {number}*/ id;
25 /**@type {String}*/ label = '';32 /**@type {string}*/ icon;
26 /**@type {String}*/ title = '';33 /**@type {string}*/ label = '';
27 /**@type {String}*/ message = '';34 /**@type {boolean}*/ showLabel = false;
35 /**@type {string}*/ title = '';
36 /**@type {string}*/ message = '';
2837
29 /**@type {QuickReplyContextLink[]}*/ contextList;38 /**@type {QuickReplyContextLink[]}*/ contextList;
3039
31 /**@type {Boolean}*/ preventAutoExecute = true;40 /**@type {boolean}*/ preventAutoExecute = true;
32 /**@type {Boolean}*/ isHidden = false;41 /**@type {boolean}*/ isHidden = false;
33 /**@type {Boolean}*/ executeOnStartup = false;42 /**@type {boolean}*/ executeOnStartup = false;
34 /**@type {Boolean}*/ executeOnUser = false;43 /**@type {boolean}*/ executeOnUser = false;
35 /**@type {Boolean}*/ executeOnAi = false;44 /**@type {boolean}*/ executeOnAi = false;
36 /**@type {Boolean}*/ executeOnChatChange = false;45 /**@type {boolean}*/ executeOnChatChange = false;
37 /**@type {Boolean}*/ executeOnGroupMemberDraft = false;46 /**@type {boolean}*/ executeOnGroupMemberDraft = false;
38 /**@type {String}*/ automationId = '';47 /**@type {boolean}*/ executeOnNewChat = false;
48 /**@type {string}*/ automationId = '';
3949
40 /**@type {Function}*/ onExecute;50 /**@type {function}*/ onExecute;
41 /**@type {Function}*/ onDelete;51 /**@type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>}*/ onDebug;
42 /**@type {Function}*/ onUpdate;52 /**@type {function}*/ onDelete;
53 /**@type {function}*/ onUpdate;
54 /**@type {function}*/ onInsertBefore;
55 /**@type {function}*/ onTransfer;
4356
4457
45 /**@type {HTMLElement}*/ dom;58 /**@type {HTMLElement}*/ dom;
59 /**@type {HTMLElement}*/ domIcon;
46 /**@type {HTMLElement}*/ domLabel;60 /**@type {HTMLElement}*/ domLabel;
47 /**@type {HTMLElement}*/ settingsDom;61 /**@type {HTMLElement}*/ settingsDom;
62 /**@type {HTMLElement}*/ settingsDomIcon;
48 /**@type {HTMLInputElement}*/ settingsDomLabel;63 /**@type {HTMLInputElement}*/ settingsDomLabel;
49 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;64 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;
5065
51 /**@type {Popup}*/ editorPopup;66 /**@type {Popup}*/ editorPopup;
67 /**@type {HTMLElement}*/ editorDom;
5268
69 /**@type {HTMLTextAreaElement}*/ editorMessage;
70 /**@type {HTMLTextAreaElement}*/ editorMessageLabel;
71 /**@type {HTMLElement}*/ editorSyntax;
53 /**@type {HTMLElement}*/ editorExecuteBtn;72 /**@type {HTMLElement}*/ editorExecuteBtn;
54 /**@type {HTMLElement}*/ editorExecuteBtnPause;73 /**@type {HTMLElement}*/ editorExecuteBtnPause;
55 /**@type {HTMLElement}*/ editorExecuteBtnStop;74 /**@type {HTMLElement}*/ editorExecuteBtnStop;
56 /**@type {HTMLElement}*/ editorExecuteProgress;75 /**@type {HTMLElement}*/ editorExecuteProgress;
57 /**@type {HTMLElement}*/ editorExecuteErrors;76 /**@type {HTMLElement}*/ editorExecuteErrors;
58 /**@type {HTMLElement}*/ editorExecuteResult;77 /**@type {HTMLElement}*/ editorExecuteResult;
59 /**@type {HTMLInputElement}*/ editorExecuteHide;78 /**@type {HTMLElement}*/ editorDebugState;
60 /**@type {Promise}*/ editorExecutePromise;79 /**@type {Promise}*/ editorExecutePromise;
80 /**@type {boolean}*/ isExecuting;
61 /**@type {SlashCommandAbortController}*/ abortController;81 /**@type {SlashCommandAbortController}*/ abortController;
82 /**@type {SlashCommandDebugController}*/ debugController;
6283
6384
64 get hasContext() {85 get hasContext() {
65 return this.contextList && this.contextList.length > 0;86 return this.contextList && this.contextList.filter(it => it.set).length > 0;
66 }87 }
6788
6889
@@ -75,6 +96,14 @@ export class QuickReply {
75 updateRender() {96 updateRender() {
76 if (!this.dom) return;97 if (!this.dom) return;
77 this.dom.title = this.title || this.message;98 this.dom.title = this.title || this.message;
99 if (this.icon) {
100 this.domIcon.classList.remove('qr--hidden');
101 if (this.showLabel) this.domLabel.classList.remove('qr--hidden');
102 else this.domLabel.classList.add('qr--hidden');
103 } else {
104 this.domIcon.classList.add('qr--hidden');
105 this.domLabel.classList.remove('qr--hidden');
106 }
78 this.domLabel.textContent = this.label;107 this.domLabel.textContent = this.label;
79 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');108 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');
80 }109 }
@@ -105,9 +134,18 @@ export class QuickReply {
105 }134 }
106 this.execute();135 this.execute();
107 });136 });
137 const icon = document.createElement('div'); {
138 this.domIcon = icon;
139 icon.classList.add('qr--button-icon');
140 icon.classList.add('fa-solid');
141 if (!this.icon) icon.classList.add('qr--hidden');
142 else icon.classList.add(this.icon);
143 root.append(icon);
144 }
108 const lbl = document.createElement('div'); {145 const lbl = document.createElement('div'); {
109 this.domLabel = lbl;146 this.domLabel = lbl;
110 lbl.classList.add('qr--button-label');147 lbl.classList.add('qr--button-label');
148 if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
111 lbl.textContent = this.label;149 lbl.textContent = this.label;
112 root.append(lbl);150 root.append(lbl);
113 }151 }
@@ -138,36 +176,115 @@ export class QuickReply {
138 item.classList.add('qr--set-item');176 item.classList.add('qr--set-item');
139 item.setAttribute('data-order', String(idx));177 item.setAttribute('data-order', String(idx));
140 item.setAttribute('data-id', String(this.id));178 item.setAttribute('data-id', String(this.id));
141 const drag = document.createElement('div'); {179 const adder = document.createElement('div'); {
142 drag.classList.add('drag-handle');180 adder.classList.add('qr--set-itemAdder');
143 drag.classList.add('ui-sortable-handle');181 const actions = document.createElement('div'); {
144 drag.textContent = '☰';182 actions.classList.add('qr--actions');
145 item.append(drag);183 const addNew = document.createElement('div'); {
146 }184 addNew.classList.add('qr--action');
147 const lblContainer = document.createElement('div'); {185 addNew.classList.add('qr--add');
148 lblContainer.classList.add('qr--set-itemLabelContainer');186 addNew.classList.add('menu_button');
149 const lbl = document.createElement('input'); {187 addNew.classList.add('menu_button_icon');
150 this.settingsDomLabel = lbl;188 addNew.classList.add('fa-solid');
151 lbl.classList.add('qr--set-itemLabel');189 addNew.classList.add('fa-plus');
152 lbl.classList.add('text_pole');190 addNew.title = 'Add quick reply';
153 lbl.value = this.label;191 addNew.addEventListener('click', ()=>this.onInsertBefore());
154 lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));192 actions.append(addNew);
155 lblContainer.append(lbl);193 }
194 const paste = document.createElement('div'); {
195 paste.classList.add('qr--action');
196 paste.classList.add('qr--paste');
197 paste.classList.add('menu_button');
198 paste.classList.add('menu_button_icon');
199 paste.classList.add('fa-solid');
200 paste.classList.add('fa-paste');
201 paste.title = 'Add quick reply from clipboard';
202 paste.addEventListener('click', async()=>{
203 const text = await navigator.clipboard.readText();
204 this.onInsertBefore(text);
205 });
206 actions.append(paste);
207 }
208 const importFile = document.createElement('div'); {
209 importFile.classList.add('qr--action');
210 importFile.classList.add('qr--importFile');
211 importFile.classList.add('menu_button');
212 importFile.classList.add('menu_button_icon');
213 importFile.classList.add('fa-solid');
214 importFile.classList.add('fa-file-import');
215 importFile.title = 'Add quick reply from JSON file';
216 importFile.addEventListener('click', async()=>{
217 const inp = document.createElement('input'); {
218 inp.type = 'file';
219 inp.accept = '.json';
220 inp.addEventListener('change', async()=>{
221 if (inp.files.length > 0) {
222 for (const file of inp.files) {
223 const text = await file.text();
224 this.onInsertBefore(text);
225 }
226 }
227 });
228 inp.click();
229 }
230 });
231 actions.append(importFile);
232 }
233 adder.append(actions);
234 }
235 item.append(adder);
236 }
237 const itemContent = document.createElement('div'); {
238 itemContent.classList.add('qr--content');
239 const drag = document.createElement('div'); {
240 drag.classList.add('drag-handle');
241 drag.classList.add('ui-sortable-handle');
242 drag.textContent = '☰';
243 itemContent.append(drag);
244 }
245 const lblContainer = document.createElement('div'); {
246 lblContainer.classList.add('qr--set-itemLabelContainer');
247 const icon = document.createElement('div'); {
248 this.settingsDomIcon = icon;
249 icon.title = 'Click to change icon';
250 icon.classList.add('qr--set-itemIcon');
251 icon.classList.add('menu_button');
252 icon.classList.add('fa-fw');
253 if (this.icon) {
254 icon.classList.add('fa-solid');
255 icon.classList.add(this.icon);
256 }
257 icon.addEventListener('click', async()=>{
258 let value = await showFontAwesomePicker();
259 this.updateIcon(value);
260 });
261 lblContainer.append(icon);
262 }
263 const lbl = document.createElement('input'); {
264 this.settingsDomLabel = lbl;
265 lbl.classList.add('qr--set-itemLabel');
266 lbl.classList.add('text_pole');
267 lbl.value = this.label;
268 lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
269 lblContainer.append(lbl);
270 }
271 itemContent.append(lblContainer);
156 }272 }
157 item.append(lblContainer);273 item.append(itemContent);
158 }274 }
159 const optContainer = document.createElement('div'); {275 const optContainer = document.createElement('div'); {
160 optContainer.classList.add('qr--set-optionsContainer');276 optContainer.classList.add('qr--set-optionsContainer');
161 const opt = document.createElement('div'); {277 const opt = document.createElement('div'); {
162 opt.classList.add('qr--action');278 opt.classList.add('qr--action');
163 opt.classList.add('menu_button');279 opt.classList.add('menu_button');
280 opt.classList.add('fa-fw');
164 opt.classList.add('fa-solid');281 opt.classList.add('fa-solid');
165 opt.textContent = '⁝';282 opt.textContent = '⁝';
166 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';283 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';
167 opt.addEventListener('click', ()=>this.showEditor());284 opt.addEventListener('click', ()=>this.showEditor());
168 optContainer.append(opt);285 optContainer.append(opt);
169 }286 }
170 item.append(optContainer);287 itemContent.append(optContainer);
171 }288 }
172 const mes = document.createElement('textarea'); {289 const mes = document.createElement('textarea'); {
173 this.settingsDomMessage = mes;290 this.settingsDomMessage = mes;
@@ -176,22 +293,89 @@ export class QuickReply {
176 mes.value = this.message;293 mes.value = this.message;
177 //HACK need to use jQuery to catch the triggered event from the expanded editor294 //HACK need to use jQuery to catch the triggered event from the expanded editor
178 $(mes).on('input', ()=>this.updateMessage(mes.value));295 $(mes).on('input', ()=>this.updateMessage(mes.value));
179 item.append(mes);296 itemContent.append(mes);
180 }297 }
181 const actions = document.createElement('div'); {298 const actions = document.createElement('div'); {
182 actions.classList.add('qr--actions');299 actions.classList.add('qr--actions');
300 const move = document.createElement('div'); {
301 move.classList.add('qr--action');
302 move.classList.add('menu_button');
303 move.classList.add('fa-fw');
304 move.classList.add('fa-solid');
305 move.classList.add('fa-truck-arrow-right');
306 move.title = 'Move quick reply to other set';
307 move.addEventListener('click', ()=>this.onTransfer(this));
308 actions.append(move);
309 }
310 const copy = document.createElement('div'); {
311 copy.classList.add('qr--action');
312 copy.classList.add('menu_button');
313 copy.classList.add('fa-fw');
314 copy.classList.add('fa-solid');
315 copy.classList.add('fa-copy');
316 copy.title = 'Copy quick reply to clipboard';
317 copy.addEventListener('click', async()=>{
318 await navigator.clipboard.writeText(JSON.stringify(this));
319 copy.classList.add('qr--success');
320 await delay(3010);
321 copy.classList.remove('qr--success');
322 });
323 actions.append(copy);
324 }
325 const cut = document.createElement('div'); {
326 cut.classList.add('qr--action');
327 cut.classList.add('menu_button');
328 cut.classList.add('fa-fw');
329 cut.classList.add('fa-solid');
330 cut.classList.add('fa-cut');
331 cut.title = 'Cut quick reply to clipboard (copy and remove)';
332 cut.addEventListener('click', async()=>{
333 await navigator.clipboard.writeText(JSON.stringify(this));
334 this.delete();
335 });
336 actions.append(cut);
337 }
338 const exp = document.createElement('div'); {
339 exp.classList.add('qr--action');
340 exp.classList.add('menu_button');
341 exp.classList.add('fa-fw');
342 exp.classList.add('fa-solid');
343 exp.classList.add('fa-file-export');
344 exp.title = 'Export quick reply as file';
345 exp.addEventListener('click', ()=>{
346 const blob = new Blob([JSON.stringify(this)], { type:'text' });
347 const url = URL.createObjectURL(blob);
348 const a = document.createElement('a'); {
349 a.href = url;
350 a.download = `${this.label}.qr.json`;
351 a.click();
352 }
353 });
354 actions.append(exp);
355 }
183 const del = document.createElement('div'); {356 const del = document.createElement('div'); {
184 del.classList.add('qr--action');357 del.classList.add('qr--action');
185 del.classList.add('menu_button');358 del.classList.add('menu_button');
186 del.classList.add('menu_button_icon');359 del.classList.add('fa-fw');
187 del.classList.add('fa-solid');360 del.classList.add('fa-solid');
188 del.classList.add('fa-trash-can');361 del.classList.add('fa-trash-can');
189 del.classList.add('redWarningBG');362 del.classList.add('redWarningBG');
190 del.title = 'Remove quick reply';363 del.title = 'Remove Quick Reply\n---\nShit+Click to skip confirmation';
191 del.addEventListener('click', ()=>this.delete());364 del.addEventListener('click', async(evt)=>{
365 if (!evt.shiftKey) {
366 const result = await Popup.show.confirm(
367 'Remove Quick Reply',
368 'Are you sure you want to remove this Quick Reply?',
369 );
370 if (result != POPUP_RESULT.AFFIRMATIVE) {
371 return;
372 }
373 }
374 this.delete();
375 });
192 actions.append(del);376 actions.append(del);
193 }377 }
194 item.append(actions);378 itemContent.append(actions);
195 }379 }
196 }380 }
197 }381 }
@@ -208,22 +392,156 @@ export class QuickReply {
208 /**@type {HTMLElement} */392 /**@type {HTMLElement} */
209 // @ts-ignore393 // @ts-ignore
210 const dom = this.template.cloneNode(true);394 const dom = this.template.cloneNode(true);
395 this.editorDom = dom;
211 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });396 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
212 const popupResult = this.editorPopup.show();397 const popupResult = this.editorPopup.show();
213398
214 // basics399 // basics
400 /**@type {HTMLElement}*/
401 const icon = dom.querySelector('#qr--modal-icon');
402 if (this.icon) {
403 icon.classList.add('fa-solid');
404 icon.classList.add(this.icon);
405 }
406 else {
407 icon.textContent = '…';
408 }
409 icon.addEventListener('click', async()=>{
410 let value = await showFontAwesomePicker();
411 if (value === null) return;
412 if (this.icon) icon.classList.remove(this.icon);
413 if (value == '') {
414 icon.classList.remove('fa-solid');
415 icon.textContent = '…';
416 } else {
417 icon.textContent = '';
418 icon.classList.add('fa-solid');
419 icon.classList.add(value);
420 }
421 this.updateIcon(value);
422 });
423 /**@type {HTMLInputElement}*/
424 const showLabel = dom.querySelector('#qr--modal-showLabel');
425 showLabel.checked = this.showLabel;
426 showLabel.addEventListener('click', ()=>{
427 this.updateShowLabel(showLabel.checked);
428 });
215 /**@type {HTMLInputElement}*/429 /**@type {HTMLInputElement}*/
216 const label = dom.querySelector('#qr--modal-label');430 const label = dom.querySelector('#qr--modal-label');
217 label.value = this.label;431 label.value = this.label;
218 label.addEventListener('input', ()=>{432 label.addEventListener('input', ()=>{
219 this.updateLabel(label.value);433 this.updateLabel(label.value);
220 });434 });
435 let switcherList;
436 dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{
437 if (switcherList) {
438 switcherList.remove();
439 switcherList = null;
440 return;
441 }
442 const list = document.createElement('ul'); {
443 switcherList = list;
444 list.classList.add('qr--modal-switcherList');
445 const makeList = (qrs)=>{
446 const setItem = document.createElement('li'); {
447 setItem.classList.add('qr--modal-switcherItem');
448 setItem.addEventListener('click', ()=>{
449 list.innerHTML = '';
450 for (const qrs of quickReplyApi.listSets()) {
451 const item = document.createElement('li'); {
452 item.classList.add('qr--modal-switcherItem');
453 item.addEventListener('click', ()=>{
454 list.innerHTML = '';
455 makeList(quickReplyApi.getSetByName(qrs));
456 });
457 const lbl = document.createElement('div'); {
458 lbl.classList.add('qr--label');
459 lbl.textContent = qrs;
460 item.append(lbl);
461 }
462 list.append(item);
463 }
464 }
465 });
466 const lbl = document.createElement('div'); {
467 lbl.classList.add('qr--label');
468 const icon = document.createElement('i'); {
469 icon.classList.add('fa-solid');
470 icon.classList.add('fa-arrow-alt-circle-right');
471 icon.classList.add('menu_button');
472 lbl.append(icon);
473 }
474 const text = document.createElement('span'); {
475 text.textContent = 'Switch QR Sets...';
476 lbl.append(text);
477 }
478 setItem.append(lbl);
479 }
480 list.append(setItem);
481 }
482 const addItem = document.createElement('li'); {
483 addItem.classList.add('qr--modal-switcherItem');
484 addItem.addEventListener('click', ()=>{
485 const qr = quickReplyApi.getSetByQr(this).addQuickReply();
486 this.editorPopup.completeAffirmative();
487 qr.showEditor();
488 });
489 const lbl = document.createElement('div'); {
490 lbl.classList.add('qr--label');
491 const icon = document.createElement('i'); {
492 icon.classList.add('fa-solid');
493 icon.classList.add('fa-plus');
494 icon.classList.add('menu_button');
495 lbl.append(icon);
496 }
497 const text = document.createElement('span'); {
498 text.textContent = 'Add QR';
499 lbl.append(text);
500 }
501 addItem.append(lbl);
502 }
503 list.append(addItem);
504 }
505 for (const qr of qrs.qrList.toSorted((a,b)=>a.label.toLowerCase().localeCompare(b.label.toLowerCase()))) {
506 const item = document.createElement('li'); {
507 item.classList.add('qr--modal-switcherItem');
508 if (qr == this) item.classList.add('qr--current');
509 else item.addEventListener('click', ()=>{
510 this.editorPopup.completeAffirmative();
511 qr.showEditor();
512 });
513 const lbl = document.createElement('div'); {
514 lbl.classList.add('qr--label');
515 lbl.textContent = qr.label;
516 item.append(lbl);
517 }
518 const id = document.createElement('div'); {
519 id.classList.add('qr--id');
520 id.textContent = qr.id.toString();
521 item.append(id);
522 }
523 const mes = document.createElement('div'); {
524 mes.classList.add('qr--message');
525 mes.textContent = qr.message;
526 item.append(mes);
527 }
528 list.append(item);
529 }
530 }
531 };
532 makeList(quickReplyApi.getSetByQr(this));
533 }
534 label.parentElement.append(list);
535 });
221 /**@type {HTMLInputElement}*/536 /**@type {HTMLInputElement}*/
222 const title = dom.querySelector('#qr--modal-title');537 const title = dom.querySelector('#qr--modal-title');
223 title.value = this.title;538 title.value = this.title;
224 title.addEventListener('input', () => {539 title.addEventListener('input', () => {
225 this.updateTitle(title.value);540 this.updateTitle(title.value);
226 });541 });
542 /**@type {HTMLElement}*/
543 const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
544 this.editorSyntax = messageSyntaxInner;
227 /**@type {HTMLInputElement}*/545 /**@type {HTMLInputElement}*/
228 const wrap = dom.querySelector('#qr--modal-wrap');546 const wrap = dom.querySelector('#qr--modal-wrap');
229 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');547 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');
@@ -235,9 +553,15 @@ export class QuickReply {
235 if (wrap.checked) {553 if (wrap.checked) {
236 message.style.whiteSpace = 'pre-wrap';554 message.style.whiteSpace = 'pre-wrap';
237 messageSyntaxInner.style.whiteSpace = 'pre-wrap';555 messageSyntaxInner.style.whiteSpace = 'pre-wrap';
556 if (this.clone) {
557 this.clone.style.whiteSpace = 'pre-wrap';
558 }
238 } else {559 } else {
239 message.style.whiteSpace = 'pre';560 message.style.whiteSpace = 'pre';
240 messageSyntaxInner.style.whiteSpace = 'pre';561 messageSyntaxInner.style.whiteSpace = 'pre';
562 if (this.clone) {
563 this.clone.style.whiteSpace = 'pre';
564 }
241 }565 }
242 updateScrollDebounced();566 updateScrollDebounced();
243 };567 };
@@ -261,11 +585,8 @@ export class QuickReply {
261 });585 });
262 };586 };
263 const updateScrollDebounced = updateScroll;587 const updateScrollDebounced = updateScroll;
264 const updateSyntax = ()=>{
265 messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
266 };
267 const updateSyntaxEnabled = ()=>{588 const updateSyntaxEnabled = ()=>{
268 if (JSON.parse(localStorage.getItem('qr--syntax'))) {589 if (syntax.checked) {
269 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');590 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
270 } else {591 } else {
271 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');592 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
@@ -296,48 +617,108 @@ export class QuickReply {
296 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));617 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
297 updateSyntaxEnabled();618 updateSyntaxEnabled();
298 });619 });
620 if (navigator.keyboard) {
621 navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash'));
622 } else {
623 dom.querySelector('#qr--modal-commentKey').closest('small').remove();
624 }
625 this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]');
299 /**@type {HTMLTextAreaElement}*/626 /**@type {HTMLTextAreaElement}*/
300 const message = dom.querySelector('#qr--modal-message');627 const message = dom.querySelector('#qr--modal-message');
628 this.editorMessage = message;
301 message.value = this.message;629 message.value = this.message;
630 const updateMessageDebounced = debounce((value)=>this.updateMessage(value), 10);
302 message.addEventListener('input', () => {631 message.addEventListener('input', () => {
303 updateSyntax();632 updateMessageDebounced(message.value);
304 this.updateMessage(message.value);
305 updateScrollDebounced();633 updateScrollDebounced();
306 });634 }, { passive:true });
307 setSlashCommandAutoComplete(message, true);635 const getLineStart = ()=>{
308 //TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize636 const start = message.selectionStart;
637 const end = message.selectionEnd;
638 let lineStart;
639 if (start == 0 || message.value[start - 1] == '\n') {
640 // cursor is already at beginning of line
641 // -> keep start
642 lineStart = start;
643 } else {
644 // cursor is at end of line or somewhere in the line
645 // -> find last newline before cursor and start after that
646 lineStart = message.value.lastIndexOf('\n', start - 1) + 1;
647 }
648 return lineStart;
649 };
309 message.addEventListener('keydown', async(evt) => {650 message.addEventListener('keydown', async(evt) => {
651 if (this.isExecuting) return;
310 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {652 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
653 // increase indent
311 evt.preventDefault();654 evt.preventDefault();
312 const start = message.selectionStart;655 const start = message.selectionStart;
313 const end = message.selectionEnd;656 const end = message.selectionEnd;
314 if (end - start > 0 && message.value.substring(start, end).includes('\n')) {657 if (end - start > 0 && message.value.substring(start, end).includes('\n')) {
315 const lineStart = message.value.lastIndexOf('\n', start);658 evt.stopImmediatePropagation();
316 const count = message.value.substring(lineStart, end).split('\n').length - 1;659 evt.stopPropagation();
317 message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;660 const lineStart = getLineStart();
318 message.selectionStart = start + 1;661 message.selectionStart = lineStart;
319 message.selectionEnd = end + count;662 const affectedLines = message.value.substring(lineStart, end).split('\n');
320 updateSyntax();663 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
321 } else {664 document.execCommand('insertText', false, `\t${affectedLines.join('\n\t')}`);
322 message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
323 message.selectionStart = start + 1;665 message.selectionStart = start + 1;
324 message.selectionEnd = end + 1;666 message.selectionEnd = end + affectedLines.length;
325 updateSyntax();667 message.dispatchEvent(new Event('input', { bubbles:true }));
668 } else if (!(ac.isReplaceable && ac.isActive)) {
669 evt.stopImmediatePropagation();
670 evt.stopPropagation();
671 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
672 document.execCommand('insertText', false, '\t');
673 message.dispatchEvent(new Event('input', { bubbles:true }));
326 }674 }
327 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {675 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
676 // decrease indent
328 evt.preventDefault();677 evt.preventDefault();
678 evt.stopImmediatePropagation();
679 evt.stopPropagation();
329 const start = message.selectionStart;680 const start = message.selectionStart;
330 const end = message.selectionEnd;681 const end = message.selectionEnd;
331 const lineStart = message.value.lastIndexOf('\n', start);682 const lineStart = getLineStart();
332 const count = message.value.substring(lineStart, end).split('\n\t').length - 1;683 message.selectionStart = lineStart;
333 message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;684 const affectedLines = message.value.substring(lineStart, end).split('\n');
334 message.selectionStart = start - 1;685 const newText = affectedLines.map(it=>it.replace(/^\t/, '')).join('\n');
335 message.selectionEnd = end - count;686 const delta = affectedLines.join('\n').length - newText.length;
336 updateSyntax();687 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
688 if (delta > 0) {
689 if (newText == '') {
690 document.execCommand('delete', false);
691 } else {
692 document.execCommand('insertText', false, newText);
693 }
694 message.selectionStart = start - (affectedLines[0].startsWith('\t') ? 1 : 0);
695 message.selectionEnd = end - delta;
696 message.dispatchEvent(new Event('input', { bubbles:true }));
697 } else {
698 message.selectionStart = start;
699 }
700 } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) {
701 // new line, keep indent
702 const start = message.selectionStart;
703 const end = message.selectionEnd;
704 let lineStart = getLineStart();
705 const indent = /^([^\S\n]*)/.exec(message.value.slice(lineStart))[1] ?? '';
706 if (indent.length) {
707 evt.stopImmediatePropagation();
708 evt.stopPropagation();
709 evt.preventDefault();
710 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
711 document.execCommand('insertText', false, `\n${indent}`);
712 message.selectionStart = start + 1 + indent.length;
713 message.selectionEnd = message.selectionStart;
714 message.dispatchEvent(new Event('input', { bubbles:true }));
715 }
337 } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {716 } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
338 evt.stopPropagation();
339 evt.preventDefault();
340 if (executeShortcut.checked) {717 if (executeShortcut.checked) {
718 // execute QR
719 evt.stopImmediatePropagation();
720 evt.stopPropagation();
721 evt.preventDefault();
341 const selectionStart = message.selectionStart;722 const selectionStart = message.selectionStart;
342 const selectionEnd = message.selectionEnd;723 const selectionEnd = message.selectionEnd;
343 message.blur();724 message.blur();
@@ -348,17 +729,175 @@ export class QuickReply {
348 message.selectionEnd = selectionEnd;729 message.selectionEnd = selectionEnd;
349 }730 }
350 }731 }
732 } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
733 // toggle breakpoint
734 evt.stopImmediatePropagation();
735 evt.stopPropagation();
736 evt.preventDefault();
737 preBreakPointStart = message.selectionStart;
738 preBreakPointEnd = message.selectionEnd;
739 toggleBreakpoint();
740 } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
741 // toggle block comment
742 // (evt.code will use the same physical key on the keyboard across different keyboard layouts)
743 evt.stopImmediatePropagation();
744 evt.stopPropagation();
745 evt.preventDefault();
746 // check if we are inside a comment -> uncomment
747 const parser = new SlashCommandParser();
748 parser.parse(message.value, false);
749 const start = message.selectionStart;
750 const end = message.selectionEnd;
751 const comment = parser.commandIndex.findLast(it=>it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end));
752 if (comment) {
753 // uncomment
754 let content = message.value.slice(comment.start + 1, comment.end - 1);
755 let len = content.length;
756 content = content.replace(/^ /, '');
757 const offsetStart = len - content.length;
758 len = content.length;
759 content = content.replace(/ $/, '');
760 const offsetEnd = len - content.length;
761 message.selectionStart = comment.start - 1;
762 message.selectionEnd = comment.end + 1;
763 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
764 document.execCommand('insertText', false, content);
765 message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0);
766 message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0);
767 } else {
768 // comment
769 const lineStart = getLineStart();
770 const lineEnd = message.value.indexOf('\n', end);
771 message.selectionStart = lineStart;
772 message.selectionEnd = lineEnd;
773 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
774 document.execCommand('insertText', false, `/* ${message.value.slice(lineStart, lineEnd)} *|`);
775 message.selectionStart = start + 3;
776 message.selectionEnd = end + 3;
777 }
778 message.dispatchEvent(new Event('input', { bubbles:true }));
351 }779 }
352 });780 });
781 const ac = await setSlashCommandAutoComplete(message, true);
353 message.addEventListener('wheel', (evt)=>{782 message.addEventListener('wheel', (evt)=>{
354 updateScrollDebounced(evt);783 updateScrollDebounced(evt);
355 });784 });
356 message.addEventListener('scroll', (evt)=>{785 message.addEventListener('scroll', (evt)=>{
357 updateScrollDebounced();786 updateScrollDebounced();
358 });787 });
788 let preBreakPointStart;
789 let preBreakPointEnd;
790 /**
791 * @param {SlashCommandBreakPoint} bp
792 */
793 const removeBreakpoint = (bp)=>{
794 // start at -1 because "/" is not included in start-end
795 let start = bp.start - 1;
796 // step left until forward slash "/"
797 while (message.value[start] != '/') start--;
798 // step left while whitespace (except newline) before start
799 while (/[^\S\n]/.test(message.value[start - 1])) start--;
800 // if newline before indent, include the newline for removal
801 if (message.value[start - 1] == '\n') start--;
802 let end = bp.end;
803 // step right while whitespace
804 while (/\s/.test(message.value[end])) end++;
805 // if pipe after whitepace, include pipe for removal
806 if (message.value[end] == '|') end++;
807 message.selectionStart = start;
808 message.selectionEnd = end;
809 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
810 document.execCommand('insertText', false, '');
811 message.dispatchEvent(new Event('input', { bubbles:true }));
812 let postStart = preBreakPointStart;
813 let postEnd = preBreakPointEnd;
814 // set caret back to where it was
815 if (preBreakPointStart <= start) {
816 // selection start was before breakpoint: do nothing
817 } else if (preBreakPointStart > start && preBreakPointEnd < end) {
818 // selection start was inside breakpoint: move to index before breakpoint
819 postStart = start;
820 } else if (preBreakPointStart >= end) {
821 // selection start was behind breakpoint: move back by length of removed string
822 postStart = preBreakPointStart - (end - start);
823 }
824 if (preBreakPointEnd <= start) {
825 // do nothing
826 } else if (preBreakPointEnd > start && preBreakPointEnd < end) {
827 // selection end was inside breakpoint: move to index before breakpoint
828 postEnd = start;
829 } else if (preBreakPointEnd >= end) {
830 // selection end was behind breakpoint: move back by length of removed string
831 postEnd = preBreakPointEnd - (end - start);
832 }
833 return { start:postStart, end:postEnd };
834 };
835 /**
836 * @param {SlashCommandExecutor} cmd
837 */
838 const addBreakpoint = (cmd)=>{
839 // start at -1 because "/" is not included in start-end
840 let start = cmd.start - 1;
841 let indent = '';
842 // step left until forward slash "/"
843 while (message.value[start] != '/') start--;
844 // step left while whitespace (except newline) before start, collect the whitespace to help build indentation
845 while (/[^\S\n]/.test(message.value[start - 1])) {
846 start--;
847 indent += message.value[start];
848 }
849 // if newline before indent, include the newline
850 if (message.value[start - 1] == '\n') {
851 start--;
852 indent = `\n${indent}`;
853 }
854 const breakpointText = `${indent}/breakpoint |`;
855 message.selectionStart = start;
856 message.selectionEnd = start;
857 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
858 document.execCommand('insertText', false, breakpointText);
859 message.dispatchEvent(new Event('input', { bubbles:true }));
860 return breakpointText.length;
861 };
862 const toggleBreakpoint = ()=>{
863 const idx = message.selectionStart;
864 let postStart = preBreakPointStart;
865 let postEnd = preBreakPointEnd;
866 const parser = new SlashCommandParser();
867 parser.parse(message.value, false);
868 const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx);
869 if (cmdIdx > -1) {
870 const cmd = parser.commandIndex[cmdIdx];
871 if (cmd instanceof SlashCommandBreakPoint) {
872 const bp = cmd;
873 const { start, end } = removeBreakpoint(bp);
874 postStart = start;
875 postEnd = end;
876 } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) {
877 const bp = parser.commandIndex[cmdIdx - 1];
878 const { start, end } = removeBreakpoint(bp);
879 postStart = start;
880 postEnd = end;
881 } else {
882 const len = addBreakpoint(cmd);
883 postStart += len;
884 postEnd += len;
885 }
886 message.selectionStart = postStart;
887 message.selectionEnd = postEnd;
888 }
889 };
890 message.addEventListener('pointerdown', (evt)=>{
891 if (!evt.ctrlKey || !evt.altKey) return;
892 preBreakPointStart = message.selectionStart;
893 preBreakPointEnd = message.selectionEnd;
894 });
895 message.addEventListener('pointerup', async(evt)=>{
896 if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return;
897 toggleBreakpoint();
898 });
359 /** @type {any} */899 /** @type {any} */
360 const resizeListener = debounce((evt) => {900 const resizeListener = debounce((evt) => {
361 updateSyntax();
362 updateScrollDebounced(evt);901 updateScrollDebounced(evt);
363 if (document.activeElement == message) {902 if (document.activeElement == message) {
364 message.blur();903 message.blur();
@@ -367,10 +906,43 @@ export class QuickReply {
367 });906 });
368 window.addEventListener('resize', resizeListener);907 window.addEventListener('resize', resizeListener);
369 updateSyntaxEnabled();908 updateSyntaxEnabled();
909 const updateSyntax = ()=>{
910 if (messageSyntaxInner && syntax.checked) {
911 morphdom(
912 messageSyntaxInner,
913 `<div>${hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value}</div>`,
914 { childrenOnly: true },
915 );
916 updateScrollDebounced();
917 }
918 };
919 let lastSyntaxUpdate = 0;
920 const fpsTime = 1000 / 30;
921 let lastMessageValue = null;
922 let wasSyntax = null;
923 const updateSyntaxLoop = ()=>{
924 const now = Date.now();
925 // fps limit
926 if (now - lastSyntaxUpdate < fpsTime) return requestAnimationFrame(updateSyntaxLoop);
927 // elements don't exist (yet?)
928 if (!messageSyntaxInner || !message) return requestAnimationFrame(updateSyntaxLoop);
929 // elements no longer part of the document
930 if (!messageSyntaxInner.closest('body')) return;
931 // debugger is running
932 if (this.isExecuting) {
933 lastMessageValue = null;
934 return requestAnimationFrame(updateSyntaxLoop);
935 }
936 // value hasn't changed
937 if (wasSyntax == syntax.checked && lastMessageValue == message.value) return requestAnimationFrame(updateSyntaxLoop);
938 wasSyntax = syntax.checked;
939 lastSyntaxUpdate = now;
940 lastMessageValue = message.value;
941 updateSyntax();
942 requestAnimationFrame(updateSyntaxLoop);
943 };
944 requestAnimationFrame(()=>updateSyntaxLoop());
370 message.style.setProperty('text-shadow', 'none', 'important');945 message.style.setProperty('text-shadow', 'none', 'important');
371 /**@type {HTMLElement}*/
372 const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
373 updateSyntax();
374 updateWrap();946 updateWrap();
375 updateTabSize();947 updateTabSize();
376948
@@ -379,7 +951,7 @@ export class QuickReply {
379 const tpl = dom.querySelector('#qr--ctxItem');951 const tpl = dom.querySelector('#qr--ctxItem');
380 const linkList = dom.querySelector('#qr--ctxEditor');952 const linkList = dom.querySelector('#qr--ctxEditor');
381 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {953 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
382 [{ name: 'Select a QR set' }, ...QuickReplySet.list].forEach(qrs => {954 [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => {
383 const opt = document.createElement('option'); {955 const opt = document.createElement('option'); {
384 opt.value = qrs.name;956 opt.value = qrs.name;
385 opt.textContent = qrs.name;957 opt.textContent = qrs.name;
@@ -388,7 +960,7 @@ export class QuickReply {
388 }960 }
389 });961 });
390 };962 };
391 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Number}*/idx) => {963 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {number}*/idx) => {
392 /**@type {HTMLElement} */964 /**@type {HTMLElement} */
393 // @ts-ignore965 // @ts-ignore
394 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {966 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
@@ -490,6 +1062,13 @@ export class QuickReply {
490 this.updateContext();1062 this.updateContext();
491 });1063 });
492 /**@type {HTMLInputElement}*/1064 /**@type {HTMLInputElement}*/
1065 const executeOnNewChat = dom.querySelector('#qr--executeOnNewChat');
1066 executeOnNewChat.checked = this.executeOnNewChat;
1067 executeOnNewChat.addEventListener('click', ()=>{
1068 this.executeOnNewChat = executeOnNewChat.checked;
1069 this.updateContext();
1070 });
1071 /**@type {HTMLInputElement}*/
493 const automationId = dom.querySelector('#qr--automationId');1072 const automationId = dom.querySelector('#qr--automationId');
494 automationId.value = this.automationId;1073 automationId.value = this.automationId;
495 automationId.addEventListener('input', () => {1074 automationId.addEventListener('input', () => {
@@ -506,9 +1085,9 @@ export class QuickReply {
506 /**@type {HTMLElement}*/1085 /**@type {HTMLElement}*/
507 const executeResult = dom.querySelector('#qr--modal-executeResult');1086 const executeResult = dom.querySelector('#qr--modal-executeResult');
508 this.editorExecuteResult = executeResult;1087 this.editorExecuteResult = executeResult;
509 /**@type {HTMLInputElement}*/1088 /**@type {HTMLElement}*/
510 const executeHide = dom.querySelector('#qr--modal-executeHide');1089 const debugState = dom.querySelector('#qr--modal-debugState');
511 this.editorExecuteHide = executeHide;1090 this.editorDebugState = debugState;
512 /**@type {HTMLElement}*/1091 /**@type {HTMLElement}*/
513 const executeBtn = dom.querySelector('#qr--modal-execute');1092 const executeBtn = dom.querySelector('#qr--modal-execute');
514 this.editorExecuteBtn = executeBtn;1093 this.editorExecuteBtn = executeBtn;
@@ -536,6 +1115,76 @@ export class QuickReply {
536 this.abortController?.abort('Stop button clicked');1115 this.abortController?.abort('Stop button clicked');
537 });1116 });
5381117
1118 /**@type {HTMLTextAreaElement} */
1119 const inputOg = document.querySelector('#send_textarea');
1120 const inputMirror = dom.querySelector('#qr--modal-send_textarea');
1121 inputMirror.value = inputOg.value;
1122 const inputOgMo = new MutationObserver(muts=>{
1123 if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) {
1124 inputOg.removeEventListener('input', inputOgListener);
1125 }
1126 });
1127 inputOgMo.observe(document.body, { childList:true });
1128 const inputOgListener = ()=>{
1129 inputMirror.value = inputOg.value;
1130 };
1131 inputOg.addEventListener('input', inputOgListener);
1132 inputMirror.addEventListener('input', ()=>{
1133 inputOg.value = inputMirror.value;
1134 });
1135
1136 /**@type {HTMLElement}*/
1137 const resumeBtn = dom.querySelector('#qr--modal-resume');
1138 resumeBtn.addEventListener('click', ()=>{
1139 this.debugController?.resume();
1140 });
1141 /**@type {HTMLElement}*/
1142 const stepBtn = dom.querySelector('#qr--modal-step');
1143 stepBtn.addEventListener('click', ()=>{
1144 this.debugController?.step();
1145 });
1146 /**@type {HTMLElement}*/
1147 const stepIntoBtn = dom.querySelector('#qr--modal-stepInto');
1148 stepIntoBtn.addEventListener('click', ()=>{
1149 this.debugController?.stepInto();
1150 });
1151 /**@type {HTMLElement}*/
1152 const stepOutBtn = dom.querySelector('#qr--modal-stepOut');
1153 stepOutBtn.addEventListener('click', ()=>{
1154 this.debugController?.stepOut();
1155 });
1156 /**@type {HTMLElement}*/
1157 const minimizeBtn = dom.querySelector('#qr--modal-minimize');
1158 minimizeBtn.addEventListener('click', ()=>{
1159 this.editorDom.classList.add('qr--minimized');
1160 });
1161 const maximizeBtn = dom.querySelector('#qr--modal-maximize');
1162 maximizeBtn.addEventListener('click', ()=>{
1163 this.editorDom.classList.remove('qr--minimized');
1164 });
1165 /**@type {boolean}*/
1166 let isResizing = false;
1167 let resizeStart;
1168 let wStart;
1169 /**@type {HTMLElement}*/
1170 const resizeHandle = dom.querySelector('#qr--resizeHandle');
1171 resizeHandle.addEventListener('pointerdown', (evt)=>{
1172 if (isResizing) return;
1173 isResizing = true;
1174 evt.preventDefault();
1175 resizeStart = evt.x;
1176 wStart = dom.querySelector('#qr--qrOptions').offsetWidth;
1177 const dragListener = debounce((evt)=>{
1178 const w = wStart + resizeStart - evt.x;
1179 dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`);
1180 }, 5);
1181 window.addEventListener('pointerup', ()=>{
1182 window.removeEventListener('pointermove', dragListener);
1183 isResizing = false;
1184 }, { once:true });
1185 window.addEventListener('pointermove', dragListener);
1186 });
1187
539 await popupResult;1188 await popupResult;
5401189
541 window.removeEventListener('resize', resizeListener);1190 window.removeEventListener('resize', resizeListener);
@@ -544,8 +1193,62 @@ export class QuickReply {
544 }1193 }
545 }1194 }
5461195
1196 getEditorPosition(start, end, message = null) {
1197 const inputRect = this.editorMessage.getBoundingClientRect();
1198 const style = window.getComputedStyle(this.editorMessage);
1199 if (!this.clone) {
1200 this.clone = document.createElement('div');
1201 for (const key of style) {
1202 this.clone.style[key] = style[key];
1203 }
1204 this.clone.style.position = 'fixed';
1205 this.clone.style.visibility = 'hidden';
1206 const mo = new MutationObserver(muts=>{
1207 if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) {
1208 this.clone?.remove();
1209 this.clone = null;
1210 }
1211 });
1212 mo.observe(document.body, { childList:true });
1213 }
1214 document.body.append(this.clone);
1215 this.clone.style.width = `${inputRect.width}px`;
1216 this.clone.style.height = `${inputRect.height}px`;
1217 this.clone.style.left = `${inputRect.left}px`;
1218 this.clone.style.top = `${inputRect.top}px`;
1219 this.clone.style.whiteSpace = style.whiteSpace;
1220 this.clone.style.tabSize = style.tabSize;
1221 const text = message ?? this.editorMessage.value;
1222 const before = text.slice(0, start);
1223 this.clone.textContent = before;
1224 const locator = document.createElement('span');
1225 locator.textContent = text.slice(start, end);
1226 this.clone.append(locator);
1227 this.clone.append(text.slice(end));
1228 this.clone.scrollTop = this.editorSyntax.scrollTop;
1229 this.clone.scrollLeft = this.editorSyntax.scrollLeft;
1230 const locatorRect = locator.getBoundingClientRect();
1231 const bodyRect = document.body.getBoundingClientRect();
1232 const location = {
1233 left: locatorRect.left - bodyRect.left,
1234 right: locatorRect.right - bodyRect.left,
1235 top: locatorRect.top - bodyRect.top,
1236 bottom: locatorRect.bottom - bodyRect.top,
1237 };
1238 // this.clone.remove();
1239 return location;
1240 }
547 async executeFromEditor() {1241 async executeFromEditor() {
548 if (this.editorExecutePromise) return;1242 if (this.isExecuting) return;
1243 this.editorPopup.onClosing = ()=>false;
1244 const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/;
1245 const oText = this.message;
1246 this.isExecuting = true;
1247 this.editorDom.classList.add('qr--isExecuting');
1248 const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax');
1249 if (noSyntax) {
1250 this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
1251 }
549 this.editorExecuteBtn.classList.add('qr--busy');1252 this.editorExecuteBtn.classList.add('qr--busy');
550 this.editorExecuteProgress.style.setProperty('--prog', '0');1253 this.editorExecuteProgress.style.setProperty('--prog', '0');
551 this.editorExecuteErrors.classList.remove('qr--hasErrors');1254 this.editorExecuteErrors.classList.remove('qr--hasErrors');
@@ -556,12 +1259,451 @@ export class QuickReply {
556 this.editorExecuteProgress.classList.remove('qr--aborted');1259 this.editorExecuteProgress.classList.remove('qr--aborted');
557 this.editorExecuteErrors.innerHTML = '';1260 this.editorExecuteErrors.innerHTML = '';
558 this.editorExecuteResult.innerHTML = '';1261 this.editorExecuteResult.innerHTML = '';
559 if (this.editorExecuteHide.checked) {1262 const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner');
560 this.editorPopup.dlg.classList.add('qr--hide');1263 const updateScroll = (evt) => {
561 }1264 let left = syntax.scrollLeft;
1265 let top = syntax.scrollTop;
1266 if (evt) {
1267 evt.preventDefault();
1268 left = syntax.scrollLeft + evt.deltaX;
1269 top = syntax.scrollTop + evt.deltaY;
1270 syntax.scrollTo({
1271 behavior: 'instant',
1272 left,
1273 top,
1274 });
1275 }
1276 this.editorMessage.scrollTo({
1277 behavior: 'instant',
1278 left,
1279 top,
1280 });
1281 };
1282 const updateScrollDebounced = updateScroll;
1283 syntax.addEventListener('wheel', (evt)=>{
1284 updateScrollDebounced(evt);
1285 });
1286 syntax.addEventListener('scroll', (evt)=>{
1287 updateScrollDebounced();
1288 });
562 try {1289 try {
563 this.editorExecutePromise = this.execute({}, true);1290 this.abortController = new SlashCommandAbortController();
564 const result = await this.editorExecutePromise;1291 this.debugController = new SlashCommandDebugController();
1292 this.debugController.onBreakPoint = async(closure, executor)=>{
1293 this.editorDom.classList.add('qr--isPaused');
1294 syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
1295 this.editorMessageLabel.innerHTML = '';
1296 if (uuidCheck.test(closure.source)) {
1297 const p0 = document.createElement('span'); {
1298 p0.textContent = 'anonymous: ';
1299 this.editorMessageLabel.append(p0);
1300 }
1301 const p1 = document.createElement('strong'); {
1302 p1.textContent = executor.source.slice(0,5);
1303 this.editorMessageLabel.append(p1);
1304 }
1305 const p2 = document.createElement('span'); {
1306 p2.textContent = executor.source.slice(5, -5);
1307 this.editorMessageLabel.append(p2);
1308 }
1309 const p3 = document.createElement('strong'); {
1310 p3.textContent = executor.source.slice(-5);
1311 this.editorMessageLabel.append(p3);
1312 }
1313 } else {
1314 this.editorMessageLabel.textContent = executor.source;
1315 }
1316 const source = closure.source;
1317 this.editorDebugState.innerHTML = '';
1318 let ci = -1;
1319 const varNames = [];
1320 const macroNames = [];
1321 /**
1322 * @param {SlashCommandScope} scope
1323 */
1324 const buildVars = (scope, isCurrent = false)=>{
1325 if (!isCurrent) {
1326 ci--;
1327 }
1328 const c = this.debugController.stack.slice(ci)[0];
1329 const wrap = document.createElement('div'); {
1330 wrap.classList.add('qr--scope');
1331 if (isCurrent) {
1332 const executor = this.debugController.cmdStack.slice(-1)[0];
1333 { // named args
1334 const namedTitle = document.createElement('div'); {
1335 namedTitle.classList.add('qr--title');
1336 namedTitle.textContent = `Named Args - /${executor.name}`;
1337 if (executor.command.name == 'run') {
1338 namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1339 }
1340 wrap.append(namedTitle);
1341 }
1342 const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it=>it.name)]);
1343 for (const key of keys) {
1344 if (key[0] == '_') continue;
1345 const item = document.createElement('div'); {
1346 item.classList.add('qr--var');
1347 const k = document.createElement('div'); {
1348 k.classList.add('qr--key');
1349 k.textContent = key;
1350 item.append(k);
1351 }
1352 const vUnresolved = document.createElement('div'); {
1353 vUnresolved.classList.add('qr--val');
1354 vUnresolved.classList.add('qr--singleCol');
1355 const val = executor.namedArgumentList.find(it=>it.name == key)?.value;
1356 if (val instanceof SlashCommandClosure) {
1357 vUnresolved.classList.add('qr--closure');
1358 vUnresolved.title = val.rawText;
1359 vUnresolved.textContent = val.toString();
1360 } else if (val === undefined) {
1361 vUnresolved.classList.add('qr--undefined');
1362 vUnresolved.textContent = 'undefined';
1363 } else {
1364 let jsonVal;
1365 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1366 if (jsonVal && typeof jsonVal == 'object') {
1367 vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1368 } else {
1369 vUnresolved.textContent = val;
1370 vUnresolved.classList.add('qr--simple');
1371 }
1372 }
1373 item.append(vUnresolved);
1374 }
1375 const vResolved = document.createElement('div'); {
1376 vResolved.classList.add('qr--val');
1377 vResolved.classList.add('qr--singleCol');
1378 if (this.debugController.namedArguments === undefined) {
1379 vResolved.classList.add('qr--unresolved');
1380 } else {
1381 const val = this.debugController.namedArguments?.[key];
1382 if (val instanceof SlashCommandClosure) {
1383 vResolved.classList.add('qr--closure');
1384 vResolved.title = val.rawText;
1385 vResolved.textContent = val.toString();
1386 } else if (val === undefined) {
1387 vResolved.classList.add('qr--undefined');
1388 vResolved.textContent = 'undefined';
1389 } else {
1390 let jsonVal;
1391 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1392 if (jsonVal && typeof jsonVal == 'object') {
1393 vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1394 } else {
1395 vResolved.textContent = val;
1396 vResolved.classList.add('qr--simple');
1397 }
1398 }
1399 }
1400 item.append(vResolved);
1401 }
1402 wrap.append(item);
1403 }
1404 }
1405 }
1406 { // unnamed args
1407 const unnamedTitle = document.createElement('div'); {
1408 unnamedTitle.classList.add('qr--title');
1409 unnamedTitle.textContent = `Unnamed Args - /${executor.name}`;
1410 if (executor.command.name == 'run') {
1411 unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1412 }
1413 wrap.append(unnamedTitle);
1414 }
1415 let i = 0;
1416 let unnamed = this.debugController.unnamedArguments ?? [];
1417 if (!Array.isArray(unnamed)) unnamed = [unnamed];
1418 while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined);
1419 unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]);
1420 for (const arg of unnamed) {
1421 i++;
1422 const item = document.createElement('div'); {
1423 item.classList.add('qr--var');
1424 const k = document.createElement('div'); {
1425 k.classList.add('qr--key');
1426 k.textContent = i.toString();
1427 item.append(k);
1428 }
1429 const vUnresolved = document.createElement('div'); {
1430 vUnresolved.classList.add('qr--val');
1431 vUnresolved.classList.add('qr--singleCol');
1432 const val = arg[0]?.value;
1433 if (val instanceof SlashCommandClosure) {
1434 vUnresolved.classList.add('qr--closure');
1435 vUnresolved.title = val.rawText;
1436 vUnresolved.textContent = val.toString();
1437 } else if (val === undefined) {
1438 vUnresolved.classList.add('qr--undefined');
1439 vUnresolved.textContent = 'undefined';
1440 } else {
1441 let jsonVal;
1442 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1443 if (jsonVal && typeof jsonVal == 'object') {
1444 vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1445 } else {
1446 vUnresolved.textContent = val;
1447 vUnresolved.classList.add('qr--simple');
1448 }
1449 }
1450 item.append(vUnresolved);
1451 }
1452 const vResolved = document.createElement('div'); {
1453 vResolved.classList.add('qr--val');
1454 vResolved.classList.add('qr--singleCol');
1455 if (this.debugController.unnamedArguments === undefined) {
1456 vResolved.classList.add('qr--unresolved');
1457 } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) {
1458 // do nothing
1459 } else {
1460 const val = arg[1];
1461 if (val instanceof SlashCommandClosure) {
1462 vResolved.classList.add('qr--closure');
1463 vResolved.title = val.rawText;
1464 vResolved.textContent = val.toString();
1465 } else if (val === undefined) {
1466 vResolved.classList.add('qr--undefined');
1467 vResolved.textContent = 'undefined';
1468 } else {
1469 let jsonVal;
1470 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1471 if (jsonVal && typeof jsonVal == 'object') {
1472 vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1473 } else {
1474 vResolved.textContent = val;
1475 vResolved.classList.add('qr--simple');
1476 }
1477 }
1478 }
1479 item.append(vResolved);
1480 }
1481 wrap.append(item);
1482 }
1483 }
1484 }
1485 }
1486 // current scope
1487 const title = document.createElement('div'); {
1488 title.classList.add('qr--title');
1489 title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope';
1490 if (c.source == source) {
1491 let hi;
1492 title.addEventListener('pointerenter', ()=>{
1493 const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText);
1494 const layer = syntax.getBoundingClientRect();
1495 hi = document.createElement('div');
1496 hi.classList.add('qr--highlight-secondary');
1497 hi.style.left = `${loc.left - layer.left}px`;
1498 hi.style.width = `${loc.right - loc.left}px`;
1499 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1500 hi.style.height = `${loc.bottom - loc.top}px`;
1501 syntax.append(hi);
1502 });
1503 title.addEventListener('pointerleave', ()=>hi?.remove());
1504 }
1505 wrap.append(title);
1506 }
1507 for (const key of Object.keys(scope.variables)) {
1508 const isHidden = varNames.includes(key);
1509 if (!isHidden) varNames.push(key);
1510 const item = document.createElement('div'); {
1511 item.classList.add('qr--var');
1512 if (isHidden) item.classList.add('qr--isHidden');
1513 const k = document.createElement('div'); {
1514 k.classList.add('qr--key');
1515 k.textContent = key;
1516 item.append(k);
1517 }
1518 const v = document.createElement('div'); {
1519 v.classList.add('qr--val');
1520 const val = scope.variables[key];
1521 if (val instanceof SlashCommandClosure) {
1522 v.classList.add('qr--closure');
1523 v.title = val.rawText;
1524 v.textContent = val.toString();
1525 } else if (val === undefined) {
1526 v.classList.add('qr--undefined');
1527 v.textContent = 'undefined';
1528 } else {
1529 let jsonVal;
1530 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1531 if (jsonVal && typeof jsonVal == 'object') {
1532 v.textContent = JSON.stringify(jsonVal, null, 2);
1533 } else {
1534 v.textContent = val;
1535 v.classList.add('qr--simple');
1536 }
1537 }
1538 item.append(v);
1539 }
1540 wrap.append(item);
1541 }
1542 }
1543 for (const key of Object.keys(scope.macros)) {
1544 const isHidden = macroNames.includes(key);
1545 if (!isHidden) macroNames.push(key);
1546 const item = document.createElement('div'); {
1547 item.classList.add('qr--macro');
1548 if (isHidden) item.classList.add('qr--isHidden');
1549 const k = document.createElement('div'); {
1550 k.classList.add('qr--key');
1551 k.textContent = key;
1552 item.append(k);
1553 }
1554 const v = document.createElement('div'); {
1555 v.classList.add('qr--val');
1556 const val = scope.macros[key];
1557 if (val instanceof SlashCommandClosure) {
1558 v.classList.add('qr--closure');
1559 v.title = val.rawText;
1560 v.textContent = val.toString();
1561 } else if (val === undefined) {
1562 v.classList.add('qr--undefined');
1563 v.textContent = 'undefined';
1564 } else {
1565 let jsonVal;
1566 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1567 if (jsonVal && typeof jsonVal == 'object') {
1568 v.textContent = JSON.stringify(jsonVal, null, 2);
1569 } else {
1570 v.textContent = val;
1571 v.classList.add('qr--simple');
1572 }
1573 }
1574 item.append(v);
1575 }
1576 wrap.append(item);
1577 }
1578 }
1579 const pipeItem = document.createElement('div'); {
1580 pipeItem.classList.add('qr--pipe');
1581 const k = document.createElement('div'); {
1582 k.classList.add('qr--key');
1583 k.textContent = 'pipe';
1584 pipeItem.append(k);
1585 }
1586 const v = document.createElement('div'); {
1587 v.classList.add('qr--val');
1588 const val = scope.pipe;
1589 if (val instanceof SlashCommandClosure) {
1590 v.classList.add('qr--closure');
1591 v.title = val.rawText;
1592 v.textContent = val.toString();
1593 } else if (val === undefined) {
1594 v.classList.add('qr--undefined');
1595 v.textContent = 'undefined';
1596 } else {
1597 let jsonVal;
1598 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1599 if (jsonVal && typeof jsonVal == 'object') {
1600 v.textContent = JSON.stringify(jsonVal, null, 2);
1601 } else {
1602 v.textContent = val;
1603 v.classList.add('qr--simple');
1604 }
1605 }
1606 pipeItem.append(v);
1607 }
1608 wrap.append(pipeItem);
1609 }
1610 if (scope.parent) {
1611 wrap.append(buildVars(scope.parent));
1612 }
1613 }
1614 return wrap;
1615 };
1616 const buildStack = ()=>{
1617 const wrap = document.createElement('div'); {
1618 wrap.classList.add('qr--stack');
1619 const title = document.createElement('div'); {
1620 title.classList.add('qr--title');
1621 title.textContent = 'Call Stack';
1622 wrap.append(title);
1623 }
1624 let ei = -1;
1625 for (const executor of this.debugController.cmdStack.toReversed()) {
1626 ei++;
1627 const c = this.debugController.stack.toReversed()[ei];
1628 const item = document.createElement('div'); {
1629 item.classList.add('qr--item');
1630 if (executor.source == source) {
1631 let hi;
1632 item.addEventListener('pointerenter', ()=>{
1633 const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText);
1634 const layer = syntax.getBoundingClientRect();
1635 hi = document.createElement('div');
1636 hi.classList.add('qr--highlight-secondary');
1637 hi.style.left = `${loc.left - layer.left}px`;
1638 hi.style.width = `${loc.right - loc.left}px`;
1639 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1640 hi.style.height = `${loc.bottom - loc.top}px`;
1641 syntax.append(hi);
1642 });
1643 item.addEventListener('pointerleave', ()=>hi?.remove());
1644 }
1645 const cmd = document.createElement('div'); {
1646 cmd.classList.add('qr--cmd');
1647 cmd.textContent = `/${executor.name}`;
1648 if (executor.command.name == 'run') {
1649 cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1650 }
1651 item.append(cmd);
1652 }
1653 const src = document.createElement('div'); {
1654 src.classList.add('qr--source');
1655 const line = closure.fullText.slice(0, executor.start).split('\n').length;
1656 if (uuidCheck.test(executor.source)) {
1657 const p1 = document.createElement('span'); {
1658 p1.classList.add('qr--fixed');
1659 p1.textContent = executor.source.slice(0,5);
1660 src.append(p1);
1661 }
1662 const p2 = document.createElement('span'); {
1663 p2.classList.add('qr--truncated');
1664 p2.textContent = '…';
1665 src.append(p2);
1666 }
1667 const p3 = document.createElement('span'); {
1668 p3.classList.add('qr--fixed');
1669 p3.textContent = `${executor.source.slice(-5)}:${line}`;
1670 src.append(p3);
1671 }
1672 src.title = `anonymous: ${executor.source}`;
1673 } else {
1674 src.textContent = `${executor.source}:${line}`;
1675 }
1676 item.append(src);
1677 }
1678 wrap.append(item);
1679 }
1680 }
1681 }
1682 return wrap;
1683 };
1684 this.editorDebugState.append(buildVars(closure.scope, true));
1685 this.editorDebugState.append(buildStack());
1686 this.editorDebugState.classList.add('qr--active');
1687 const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText);
1688 const layer = syntax.getBoundingClientRect();
1689 const hi = document.createElement('div');
1690 hi.classList.add('qr--highlight');
1691 if (this.debugController.namedArguments === undefined) {
1692 hi.classList.add('qr--unresolved');
1693 }
1694 hi.style.left = `${loc.left - layer.left}px`;
1695 hi.style.width = `${loc.right - loc.left}px`;
1696 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1697 hi.style.height = `${loc.bottom - loc.top}px`;
1698 syntax.append(hi);
1699 const isStepping = await this.debugController.awaitContinue();
1700 hi.remove();
1701 this.editorDebugState.textContent = '';
1702 this.editorDebugState.classList.remove('qr--active');
1703 this.editorDom.classList.remove('qr--isPaused');
1704 return isStepping;
1705 };
1706 const result = await this.onDebug(this);
565 if (this.abortController?.signal?.aborted) {1707 if (this.abortController?.signal?.aborted) {
566 this.editorExecuteProgress.classList.add('qr--aborted');1708 this.editorExecuteProgress.classList.add('qr--aborted');
567 } else {1709 } else {
@@ -586,9 +1728,18 @@ export class QuickReply {
586 `;1728 `;
587 }1729 }
588 }1730 }
1731 if (noSyntax) {
1732 this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
1733 }
1734 this.editorMessageLabel.innerHTML = '';
1735 this.editorMessageLabel.textContent = 'Message / Command: ';
1736 this.editorMessage.value = oText;
1737 this.editorMessage.dispatchEvent(new Event('input', { bubbles:true }));
589 this.editorExecutePromise = null;1738 this.editorExecutePromise = null;
590 this.editorExecuteBtn.classList.remove('qr--busy');1739 this.editorExecuteBtn.classList.remove('qr--busy');
591 this.editorPopup.dlg.classList.remove('qr--hide');1740 this.editorDom.classList.remove('qr--isExecuting');
1741 this.isExecuting = false;
1742 this.editorPopup.onClosing = null;
592 }1743 }
5931744
594 updateEditorProgress(done, total) {1745 updateEditorProgress(done, total) {
@@ -623,6 +1774,47 @@ export class QuickReply {
623 /**1774 /**
624 * @param {string} value1775 * @param {string} value
625 */1776 */
1777 updateIcon(value) {
1778 if (this.onUpdate) {
1779 if (value === null) return;
1780 if (this.settingsDomIcon) {
1781 if (this.icon != value) {
1782 if (value == '') {
1783 if (this.icon) {
1784 this.settingsDomIcon.classList.remove(this.icon);
1785 }
1786 this.settingsDomIcon.textContent = '…';
1787 this.settingsDomIcon.classList.remove('fa-solid');
1788 } else {
1789 if (this.icon) {
1790 this.settingsDomIcon.classList.remove(this.icon);
1791 } else {
1792 this.settingsDomIcon.classList.add('fa-solid');
1793 }
1794 this.settingsDomIcon.classList.add(value);
1795 }
1796 }
1797 }
1798 this.icon = value;
1799 this.updateRender();
1800 this.onUpdate(this);
1801 }
1802 }
1803
1804 /**
1805 * @param {boolean} value
1806 */
1807 updateShowLabel(value) {
1808 if (this.onUpdate) {
1809 this.showLabel = value;
1810 this.updateRender();
1811 this.onUpdate(this);
1812 }
1813 }
1814
1815 /**
1816 * @param {string} value
1817 */
626 updateLabel(value) {1818 updateLabel(value) {
627 if (this.onUpdate) {1819 if (this.onUpdate) {
628 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {1820 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {
@@ -670,21 +1862,25 @@ export class QuickReply {
670 }1862 }
6711863
6721864
673 async execute(args = {}, isEditor = false, isRun = false) {1865 async execute(args = {}, isEditor = false, isRun = false, options = {}) {
674 if (this.message?.length > 0 && this.onExecute) {1866 if (this.message?.length > 0 && this.onExecute) {
675 const scope = new SlashCommandScope();1867 const scope = new SlashCommandScope();
676 for (const key of Object.keys(args)) {1868 for (const key of Object.keys(args)) {
1869 if (key[0] == '_') continue;
1870 if (key == 'isAutoExecute') continue;
677 scope.setMacro(`arg::${key}`, args[key]);1871 scope.setMacro(`arg::${key}`, args[key]);
678 }1872 }
1873 scope.setMacro('arg::*', '');
679 if (isEditor) {1874 if (isEditor) {
680 this.abortController = new SlashCommandAbortController();1875 this.abortController = new SlashCommandAbortController();
681 }1876 }
682 return await this.onExecute(this, {1877 return await this.onExecute(this, {
683 message:this.message,1878 message: this.message,
684 isAutoExecute: args.isAutoExecute ?? false,1879 isAutoExecute: args.isAutoExecute ?? false,
685 isEditor,1880 isEditor,
686 isRun,1881 isRun,
687 scope,1882 scope,
1883 executionOptions: options,
688 });1884 });
689 }1885 }
690 }1886 }
@@ -695,6 +1891,8 @@ export class QuickReply {
695 toJSON() {1891 toJSON() {
696 return {1892 return {
697 id: this.id,1893 id: this.id,
1894 icon: this.icon,
1895 showLabel: this.showLabel,
698 label: this.label,1896 label: this.label,
699 title: this.title,1897 title: this.title,
700 message: this.message,1898 message: this.message,
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+6 -1
@@ -60,7 +60,12 @@ export class QuickReplyConfig {
60 /**@type {HTMLElement}*/60 /**@type {HTMLElement}*/
61 this.setListDom = root.querySelector('.qr--setList');61 this.setListDom = root.querySelector('.qr--setList');
62 root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{62 root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
63 this.addSet(QuickReplySet.list[0]);63 const newSet = QuickReplySet.list.find(qr=>!this.setList.find(qrl=>qrl.set == qr));
64 if (newSet) {
65 this.addSet(newSet);
66 } else {
67 toastr.warning('All existing QR Sets have already been added.');
68 }
64 });69 });
65 this.updateSetListDom();70 this.updateSetListDom();
66 }71 }
public/scripts/extensions/quick-reply/src/QuickReplySet.js+191 -20
@@ -1,7 +1,9 @@
1import { getRequestHeaders, substituteParams } from '../../../../script.js';1import { getRequestHeaders, substituteParams } from '../../../../script.js';
2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
2import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';3import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
3import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
4import { debounceAsync, warn } from '../index.js';6import { debounceAsync, log, warn } from '../index.js';
5import { QuickReply } from './QuickReply.js';7import { QuickReply } from './QuickReply.js';
68
7export class QuickReplySet {9export class QuickReplySet {
@@ -16,7 +18,7 @@ export class QuickReplySet {
16 }18 }
1719
18 /**20 /**
19 * @param {String} name - name of the QuickReplySet21 * @param {string} name - name of the QuickReplySet
20 */22 */
21 static get(name) {23 static get(name) {
22 return this.list.find(it=>it.name == name);24 return this.list.find(it=>it.name == name);
@@ -25,17 +27,19 @@ export class QuickReplySet {
2527
2628
2729
28 /**@type {String}*/ name;30 /**@type {string}*/ name;
29 /**@type {Boolean}*/ disableSend = false;31 /**@type {boolean}*/ disableSend = false;
30 /**@type {Boolean}*/ placeBeforeInput = false;32 /**@type {boolean}*/ placeBeforeInput = false;
31 /**@type {Boolean}*/ injectInput = false;33 /**@type {boolean}*/ injectInput = false;
34 /**@type {string}*/ color = 'transparent';
35 /**@type {boolean}*/ onlyBorderColor = false;
32 /**@type {QuickReply[]}*/ qrList = [];36 /**@type {QuickReply[]}*/ qrList = [];
3337
34 /**@type {Number}*/ idIndex = 0;38 /**@type {number}*/ idIndex = 0;
3539
36 /**@type {Boolean}*/ isDeleted = false;40 /**@type {boolean}*/ isDeleted = false;
3741
38 /**@type {Function}*/ save;42 /**@type {function}*/ save;
3943
40 /**@type {HTMLElement}*/ dom;44 /**@type {HTMLElement}*/ dom;
41 /**@type {HTMLElement}*/ settingsDom;45 /**@type {HTMLElement}*/ settingsDom;
@@ -64,6 +68,7 @@ export class QuickReplySet {
64 const root = document.createElement('div'); {68 const root = document.createElement('div'); {
65 this.dom = root;69 this.dom = root;
66 root.classList.add('qr--buttons');70 root.classList.add('qr--buttons');
71 this.updateColor();
67 this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{72 this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
68 root.append(qr.render());73 root.append(qr.render());
69 });74 });
@@ -78,6 +83,22 @@ export class QuickReplySet {
78 this.dom.append(qr.render());83 this.dom.append(qr.render());
79 });84 });
80 }85 }
86 updateColor() {
87 if (!this.dom) return;
88 if (this.color && this.color != 'transparent') {
89 this.dom.style.setProperty('--qr--color', this.color);
90 this.dom.classList.add('qr--color');
91 if (this.onlyBorderColor) {
92 this.dom.classList.add('qr--borderColor');
93 } else {
94 this.dom.classList.remove('qr--borderColor');
95 }
96 } else {
97 this.dom.style.setProperty('--qr--color', 'transparent');
98 this.dom.classList.remove('qr--color');
99 this.dom.classList.remove('qr--borderColor');
100 }
101 }
81102
82103
83104
@@ -93,6 +114,11 @@ export class QuickReplySet {
93 }114 }
94 return this.settingsDom;115 return this.settingsDom;
95 }116 }
117 /**
118 *
119 * @param {QuickReply} qr
120 * @param {number} idx
121 */
96 renderSettingsItem(qr, idx) {122 renderSettingsItem(qr, idx) {
97 this.settingsDom.append(qr.renderSettings(idx));123 this.settingsDom.append(qr.renderSettings(idx));
98 }124 }
@@ -102,6 +128,18 @@ export class QuickReplySet {
102128
103 /**129 /**
104 *130 *
131 * @param {QuickReply} qr
132 */
133 async debug(qr) {
134 const parser = new SlashCommandParser();
135 const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController);
136 closure.source = `${this.name}.${qr.label}`;
137 closure.onProgress = (done, total) => qr.updateEditorProgress(done, total);
138 closure.scope.setMacro('arg::*', '');
139 return (await closure.execute())?.pipe;
140 }
141 /**
142 *
105 * @param {QuickReply} qr The QR to execute.143 * @param {QuickReply} qr The QR to execute.
106 * @param {object} options144 * @param {object} options
107 * @param {string} [options.message] (null) altered message to be used145 * @param {string} [options.message] (null) altered message to be used
@@ -109,6 +147,7 @@ export class QuickReplySet {
109 * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor147 * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
110 * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)148 * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
111 * @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command149 * @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
150 * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options
112 * @returns151 * @returns
113 */152 */
114 async executeWithOptions(qr, options = {}) {153 async executeWithOptions(qr, options = {}) {
@@ -118,7 +157,9 @@ export class QuickReplySet {
118 isEditor:false,157 isEditor:false,
119 isRun:false,158 isRun:false,
120 scope:null,159 scope:null,
160 executionOptions:{},
121 }, options);161 }, options);
162 const execOptions = options.executionOptions;
122 /**@type {HTMLTextAreaElement}*/163 /**@type {HTMLTextAreaElement}*/
123 const ta = document.querySelector('#send_textarea');164 const ta = document.querySelector('#send_textarea');
124 const finalMessage = options.message ?? qr.message;165 const finalMessage = options.message ?? qr.message;
@@ -136,21 +177,24 @@ export class QuickReplySet {
136 if (input[0] == '/' && !this.disableSend) {177 if (input[0] == '/' && !this.disableSend) {
137 let result;178 let result;
138 if (options.isAutoExecute || options.isRun) {179 if (options.isAutoExecute || options.isRun) {
139 result = await executeSlashCommandsWithOptions(input, {180 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
140 handleParserErrors: true,181 handleParserErrors: true,
141 scope: options.scope,182 scope: options.scope,
142 });183 source: `${this.name}.${qr.label}`,
184 }));
143 } else if (options.isEditor) {185 } else if (options.isEditor) {
144 result = await executeSlashCommandsWithOptions(input, {186 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
145 handleParserErrors: false,187 handleParserErrors: false,
146 scope: options.scope,188 scope: options.scope,
147 abortController: qr.abortController,189 abortController: qr.abortController,
190 source: `${this.name}.${qr.label}`,
148 onProgress: (done, total) => qr.updateEditorProgress(done, total),191 onProgress: (done, total) => qr.updateEditorProgress(done, total),
149 });192 }));
150 } else {193 } else {
151 result = await executeSlashCommandsOnChatInput(input, {194 result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, {
152 scope: options.scope,195 scope: options.scope,
153 });196 source: `${this.name}.${qr.label}`,
197 }));
154 }198 }
155 return typeof result === 'object' ? result?.pipe : '';199 return typeof result === 'object' ? result?.pipe : '';
156 }200 }
@@ -165,7 +209,7 @@ export class QuickReplySet {
165 }209 }
166 /**210 /**
167 * @param {QuickReply} qr211 * @param {QuickReply} qr
168 * @param {String} [message] - optional altered message to be used212 * @param {string} [message] - optional altered message to be used
169 * @param {SlashCommandScope} [scope] - optional scope to be used when running the command213 * @param {SlashCommandScope} [scope] - optional scope to be used when running the command
170 */214 */
171 async execute(qr, message = null, isAutoExecute = false, scope = null) {215 async execute(qr, message = null, isAutoExecute = false, scope = null) {
@@ -179,10 +223,11 @@ export class QuickReplySet {
179223
180224
181225
182 addQuickReply() {226 addQuickReply(data = {}) {
183 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;227 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228 data.id =
184 this.idIndex = id + 1;229 this.idIndex = id + 1;
185 const qr = QuickReply.from({ id });230 const qr = QuickReply.from(data);
186 this.qrList.push(qr);231 this.qrList.push(qr);
187 this.hookQuickReply(qr);232 this.hookQuickReply(qr);
188 if (this.settingsDom) {233 if (this.settingsDom) {
@@ -194,11 +239,131 @@ export class QuickReplySet {
194 this.save();239 this.save();
195 return qr;240 return qr;
196 }241 }
242 addQuickReplyFromText(qrJson) {
243 let data;
244 if (qrJson) {
245 try {
246 data = JSON.parse(qrJson ?? '{}');
247 delete data.id;
248 } catch {
249 // not JSON data
250 }
251 if (data) {
252 // JSON data
253 if (data.label === undefined || data.message === undefined) {
254 // not a QR
255 toastr.error('Not a QR.');
256 return;
257 }
258 } else {
259 // no JSON, use plaintext as QR message
260 data = { message: qrJson };
261 }
262 } else {
263 data = {};
264 }
265 const newQr = this.addQuickReply(data);
266 return newQr;
267 }
197268
269 /**
270 *
271 * @param {QuickReply} qr
272 */
198 hookQuickReply(qr) {273 hookQuickReply(qr) {
274 qr.onDebug = ()=>this.debug(qr);
199 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);275 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
200 qr.onDelete = ()=>this.removeQuickReply(qr);276 qr.onDelete = ()=>this.removeQuickReply(qr);
201 qr.onUpdate = ()=>this.save();277 qr.onUpdate = ()=>this.save();
278 qr.onInsertBefore = (qrJson)=>{
279 this.addQuickReplyFromText(qrJson);
280 const newQr = this.qrList.pop();
281 this.qrList.splice(this.qrList.indexOf(qr), 0, newQr);
282 if (qr.settingsDom) {
283 qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom);
284 }
285 this.save();
286 };
287 qr.onTransfer = async()=>{
288 /**@type {HTMLSelectElement} */
289 let sel;
290 let isCopy = false;
291 const dom = document.createElement('div'); {
292 dom.classList.add('qr--transferModal');
293 const title = document.createElement('h3'); {
294 title.textContent = 'Transfer Quick Reply';
295 dom.append(title);
296 }
297 const subTitle = document.createElement('h4'); {
298 const entryName = qr.label;
299 const bookName = this.name;
300 subTitle.textContent = `${bookName}: ${entryName}`;
301 dom.append(subTitle);
302 }
303 sel = document.createElement('select'); {
304 sel.classList.add('qr--transferSelect');
305 sel.setAttribute('autofocus', '1');
306 const noOpt = document.createElement('option'); {
307 noOpt.value = '';
308 noOpt.textContent = '-- Select QR Set --';
309 sel.append(noOpt);
310 }
311 for (const qrs of QuickReplySet.list) {
312 const opt = document.createElement('option'); {
313 opt.value = qrs.name;
314 opt.textContent = qrs.name;
315 sel.append(opt);
316 }
317 }
318 sel.addEventListener('keyup', (evt)=>{
319 if (evt.key == 'Shift') {
320 (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
321 return;
322 }
323 });
324 sel.addEventListener('keydown', (evt)=>{
325 if (evt.key == 'Shift') {
326 (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
327 return;
328 }
329 if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') {
330 evt.preventDefault();
331 if (evt.shiftKey) isCopy = true;
332 dlg.completeAffirmative();
333 }
334 });
335 dom.append(sel);
336 }
337 const hintP = document.createElement('p'); {
338 const hint = document.createElement('small'); {
339 hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.';
340 hintP.append(hint);
341 }
342 dom.append(hintP);
343 }
344 }
345 const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' });
346 const copyBtn = document.createElement('div'); {
347 copyBtn.classList.add('qr--copy');
348 copyBtn.classList.add('menu_button');
349 copyBtn.textContent = 'Copy';
350 copyBtn.addEventListener('click', ()=>{
351 isCopy = true;
352 dlg.completeAffirmative();
353 });
354 (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
355 }
356 const prom = dlg.show();
357 sel.focus();
358 await prom;
359 if (dlg.result == POPUP_RESULT.AFFIRMATIVE) {
360 const qrs = QuickReplySet.list.find(it=>it.name == sel.value);
361 qrs.addQuickReply(qr.toJSON());
362 if (!isCopy) {
363 qr.delete();
364 }
365 }
366 };
202 }367 }
203368
204 removeQuickReply(qr) {369 removeQuickReply(qr) {
@@ -214,6 +379,8 @@ export class QuickReplySet {
214 disableSend: this.disableSend,379 disableSend: this.disableSend,
215 placeBeforeInput: this.placeBeforeInput,380 placeBeforeInput: this.placeBeforeInput,
216 injectInput: this.injectInput,381 injectInput: this.injectInput,
382 color: this.color,
383 onlyBorderColor: this.onlyBorderColor,
217 qrList: this.qrList,384 qrList: this.qrList,
218 idIndex: this.idIndex,385 idIndex: this.idIndex,
219 };386 };
@@ -245,8 +412,12 @@ export class QuickReplySet {
245 if (response.ok) {412 if (response.ok) {
246 this.unrender();413 this.unrender();
247 const idx = QuickReplySet.list.indexOf(this);414 const idx = QuickReplySet.list.indexOf(this);
248 QuickReplySet.list.splice(idx, 1);415 if (idx > -1) {
249 this.isDeleted = true;416 QuickReplySet.list.splice(idx, 1);
417 this.isDeleted = true;
418 } else {
419 warn(`Deleted Quick Reply Set was not found in the list of sets: ${this.name}`);
420 }
250 } else {421 } else {
251 warn(`Failed to delete Quick Reply Set: ${this.name}`);422 warn(`Failed to delete Quick Reply Set: ${this.name}`);
252 }423 }
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js+1 -1
@@ -45,7 +45,7 @@ export class QuickReplySetLink {
45 this.set = QuickReplySet.get(set.value);45 this.set = QuickReplySet.get(set.value);
46 this.update();46 this.update();
47 });47 });
48 QuickReplySet.list.forEach(qrs=>{48 QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
49 const opt = document.createElement('option'); {49 const opt = document.createElement('option'); {
50 opt.value = qrs.name;50 opt.value = qrs.name;
51 opt.textContent = qrs.name;51 opt.textContent = qrs.name;
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+2 -0
@@ -16,6 +16,7 @@ export class QuickReplySettings {
16 /**@type {Boolean}*/ isEnabled = false;16 /**@type {Boolean}*/ isEnabled = false;
17 /**@type {Boolean}*/ isCombined = false;17 /**@type {Boolean}*/ isCombined = false;
18 /**@type {Boolean}*/ isPopout = false;18 /**@type {Boolean}*/ isPopout = false;
19 /**@type {Boolean}*/ showPopoutButton = true;
19 /**@type {QuickReplyConfig}*/ config;20 /**@type {QuickReplyConfig}*/ config;
20 /**@type {QuickReplyConfig}*/ _chatConfig;21 /**@type {QuickReplyConfig}*/ _chatConfig;
21 get chatConfig() {22 get chatConfig() {
@@ -79,6 +80,7 @@ export class QuickReplySettings {
79 isEnabled: this.isEnabled,80 isEnabled: this.isEnabled,
80 isCombined: this.isCombined,81 isCombined: this.isCombined,
81 isPopout: this.isPopout,82 isPopout: this.isPopout,
83 showPopoutButton: this.showPopoutButton,
82 config: this.config,84 config: this.config,
83 };85 };
84 }86 }
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+259 -18
@@ -1,8 +1,12 @@
1import { SlashCommand } from '../../../slash-commands/SlashCommand.js';1import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
2import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
2import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
4import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
3import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';5import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';
6import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
4import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';7import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';
5import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';8import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
9import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { isTrueBoolean } from '../../../utils.js';10import { isTrueBoolean } from '../../../utils.js';
7// eslint-disable-next-line no-unused-vars11// eslint-disable-next-line no-unused-vars
8import { QuickReplyApi } from '../api/QuickReplyApi.js';12import { QuickReplyApi } from '../api/QuickReplyApi.js';
@@ -47,6 +51,13 @@ export class SlashCommandHandler {
47 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);51 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);
48 }) ?? [],52 }) ?? [],
4953
54 /** All QRs inside a set, utilizing the "set" named argument, returns the QR's ID */
55 qrIds: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => {
56 const icons = getExecutionIcons(qr);
57 const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim();
58 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr, null, ()=>qr.id.toString(), true);
59 }) ?? [],
60
50 /** All QRs as a set.name string, to be able to execute, for example via the /run command */61 /** All QRs as a set.name string, to be able to execute, for example via the /run command */
51 qrExecutables: () => {62 qrExecutables: () => {
52 const globalSetList = this.api.settings.config.setList;63 const globalSetList = this.api.settings.config.setList;
@@ -63,7 +74,7 @@ export class SlashCommandHandler {
63 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),74 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),
64 ];75 ];
65 },76 },
66 }77 };
6778
68 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;79 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;
6980
@@ -234,8 +245,20 @@ export class SlashCommandHandler {
234 name: 'label',245 name: 'label',
235 description: 'text on the button, e.g., label=MyButton',246 description: 'text on the button, e.g., label=MyButton',
236 typeList: [ARGUMENT_TYPE.STRING],247 typeList: [ARGUMENT_TYPE.STRING],
237 isRequired: true,248 isRequired: false,
238 enumProvider: localEnumProviders.qrLabels,249 enumProvider: localEnumProviders.qrEntries,
250 }),
251 SlashCommandNamedArgument.fromProps({
252 name: 'icon',
253 description: 'icon to show on the button, e.g., icon=fa-pencil',
254 typeList: [ARGUMENT_TYPE.STRING],
255 isRequired: false,
256 }),
257 SlashCommandNamedArgument.fromProps({
258 name: 'showlabel',
259 description: 'whether to show the label even when an icon is assigned, e.g., icon=fa-pencil showlabel=true',
260 typeList: [ARGUMENT_TYPE.BOOLEAN],
261 isRequired: false,
239 }),262 }),
240 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),263 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
241 new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),264 new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
@@ -247,6 +270,13 @@ export class SlashCommandHandler {
247 ];270 ];
248 const qrUpdateArgs = [271 const qrUpdateArgs = [
249 new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),272 new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
273 SlashCommandNamedArgument.fromProps({
274 name: 'id',
275 description: 'numeric ID of the QR, e.g., id=42',
276 typeList: [ARGUMENT_TYPE.NUMBER],
277 isRequired: false,
278 enumProvider: localEnumProviders.qrIds,
279 }),
250 ];280 ];
251281
252 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',282 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
@@ -272,13 +302,61 @@ export class SlashCommandHandler {
272 </div>302 </div>
273 `,303 `,
274 }));304 }));
305 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-get',
306 callback: (args, _) => {
307 return this.getQuickReply(args);
308 },
309 namedArgumentList: [
310 SlashCommandNamedArgument.fromProps({
311 name: 'set',
312 description: 'name of the QR set, e.g., set=PresetName1',
313 typeList: [ARGUMENT_TYPE.STRING],
314 isRequired: true,
315 enumProvider: localEnumProviders.qrSets,
316 }),
317 SlashCommandNamedArgument.fromProps({
318 name: 'label',
319 description: 'text on the button, e.g., label=MyButton',
320 typeList: [ARGUMENT_TYPE.STRING],
321 isRequired: false,
322 enumProvider: localEnumProviders.qrEntries,
323 }),
324 SlashCommandNamedArgument.fromProps({
325 name: 'id',
326 description: 'numeric ID of the QR, e.g., id=42',
327 typeList: [ARGUMENT_TYPE.NUMBER],
328 isRequired: false,
329 enumProvider: localEnumProviders.qrIds,
330 }),
331 ],
332 returns: 'a dictionary with all the QR\'s properties',
333 helpString: `
334 <div>Get a Quick Reply's properties.</div>
335 <div>
336 <strong>Examples:</strong>
337 <ul>
338 <li>
339 <pre><code>/qr-get set=MyPreset label=MyButton | /echo</code></pre>
340 <pre><code>/qr-get set=MyPreset id=42 | /echo</code></pre>
341 </li>
342 </ul>
343 </div>
344 `,
345 }));
275 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',346 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
276 callback: (args, message) => {347 callback: (args, message) => {
277 this.updateQuickReply(args, message);348 this.updateQuickReply(args, message);
278 return '';349 return '';
279 },350 },
280 returns: 'updated quick reply',351 returns: 'updated quick reply',
281 namedArgumentList: [...qrUpdateArgs, ...qrArgs],352 namedArgumentList: [...qrUpdateArgs, ...qrArgs.map(it=>{
353 if (it.name == 'label') {
354 const clone = SlashCommandNamedArgument.fromProps(it);
355 clone.isRequired = false;
356 return clone;
357 }
358 return it;
359 })],
282 unnamedArgumentList: [360 unnamedArgumentList: [
283 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),361 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),
284 ],362 ],
@@ -315,6 +393,12 @@ export class SlashCommandHandler {
315 typeList: [ARGUMENT_TYPE.STRING],393 typeList: [ARGUMENT_TYPE.STRING],
316 enumProvider: localEnumProviders.qrEntries,394 enumProvider: localEnumProviders.qrEntries,
317 }),395 }),
396 SlashCommandNamedArgument.fromProps({
397 name: 'id',
398 description: 'numeric ID of the QR, e.g., id=42',
399 typeList: [ARGUMENT_TYPE.NUMBER],
400 enumProvider: localEnumProviders.qrIds,
401 }),
318 ],402 ],
319 unnamedArgumentList: [403 unnamedArgumentList: [
320 SlashCommandArgument.fromProps({404 SlashCommandArgument.fromProps({
@@ -344,6 +428,12 @@ export class SlashCommandHandler {
344 typeList: [ARGUMENT_TYPE.STRING],428 typeList: [ARGUMENT_TYPE.STRING],
345 enumProvider: localEnumProviders.qrEntries,429 enumProvider: localEnumProviders.qrEntries,
346 }),430 }),
431 SlashCommandNamedArgument.fromProps({
432 name: 'id',
433 description: 'numeric ID of the QR, e.g., id=42',
434 typeList: [ARGUMENT_TYPE.NUMBER],
435 enumProvider: localEnumProviders.qrIds,
436 }),
347 new SlashCommandNamedArgument(437 new SlashCommandNamedArgument(
348 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',438 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
349 ),439 ),
@@ -389,6 +479,12 @@ export class SlashCommandHandler {
389 typeList: [ARGUMENT_TYPE.STRING],479 typeList: [ARGUMENT_TYPE.STRING],
390 enumProvider: localEnumProviders.qrEntries,480 enumProvider: localEnumProviders.qrEntries,
391 }),481 }),
482 SlashCommandNamedArgument.fromProps({
483 name: 'id',
484 description: 'numeric ID of the QR, e.g., id=42',
485 typeList: [ARGUMENT_TYPE.NUMBER],
486 enumProvider: localEnumProviders.qrIds,
487 }),
392 ],488 ],
393 unnamedArgumentList: [489 unnamedArgumentList: [
394 SlashCommandArgument.fromProps({490 SlashCommandArgument.fromProps({
@@ -425,6 +521,12 @@ export class SlashCommandHandler {
425 isRequired: true,521 isRequired: true,
426 enumProvider: localEnumProviders.qrSets,522 enumProvider: localEnumProviders.qrSets,
427 }),523 }),
524 SlashCommandNamedArgument.fromProps({
525 name: 'id',
526 description: 'numeric ID of the QR, e.g., id=42',
527 typeList: [ARGUMENT_TYPE.NUMBER],
528 enumProvider: localEnumProviders.qrIds,
529 }),
428 ],530 ],
429 unnamedArgumentList: [531 unnamedArgumentList: [
430 SlashCommandArgument.fromProps({532 SlashCommandArgument.fromProps({
@@ -454,8 +556,8 @@ export class SlashCommandHandler {
454 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),556 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
455 ];557 ];
456 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',558 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
457 callback: (args, name) => {559 callback: async (args, name) => {
458 this.createSet(name, args);560 await this.createSet(name, args);
459 return '';561 return '';
460 },562 },
461 aliases: ['qr-presetadd'],563 aliases: ['qr-presetadd'],
@@ -485,8 +587,8 @@ export class SlashCommandHandler {
485 }));587 }));
486588
487 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',589 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
488 callback: (args, name) => {590 callback: async (args, name) => {
489 this.updateSet(name, args);591 await this.updateSet(name, args);
490 return '';592 return '';
491 },593 },
492 aliases: ['qr-presetupdate'],594 aliases: ['qr-presetupdate'],
@@ -510,8 +612,8 @@ export class SlashCommandHandler {
510 `,612 `,
511 }));613 }));
512 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',614 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
513 callback: (_, name) => {615 callback: async (_, name) => {
514 this.deleteSet(name);616 await this.deleteSet(name);
515 return '';617 return '';
516 },618 },
517 aliases: ['qr-presetdelete'],619 aliases: ['qr-presetdelete'],
@@ -533,6 +635,134 @@ export class SlashCommandHandler {
533 </div>635 </div>
534 `,636 `,
535 }));637 }));
638
639 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-arg',
640 callback: ({ _scope }, [key, value]) => {
641 _scope.setMacro(`arg::${key}`, value, key.includes('*'));
642 return '';
643 },
644 unnamedArgumentList: [
645 SlashCommandArgument.fromProps({ description: 'argument name',
646 typeList: ARGUMENT_TYPE.STRING,
647 isRequired: true,
648 }),
649 SlashCommandArgument.fromProps({ description: 'argument value',
650 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
651 isRequired: true,
652 }),
653 ],
654 splitUnnamedArgument: true,
655 splitUnnamedArgumentCount: 2,
656 helpString: `
657 <div>
658 Set a fallback value for a Quick Reply argument.
659 </div>
660 <div>
661 <strong>Example:</strong>
662 <pre><code>/qr-arg x foo |\n/echo {{arg::x}}</code></pre>
663 </div>
664 `,
665 }));
666
667 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'import',
668 /**
669 *
670 * @param {{_scope:SlashCommandScope, _abortController:SlashCommandAbortController, _debugController:SlashCommandDebugController, from:string}} args
671 * @param {string} value
672 */
673 callback: (args, value) => {
674 if (!args.from) throw new Error('/import requires from= to be set.');
675 if (!value) throw new Error('/import requires the unnamed argument to be set.');
676 let qr = [...this.api.listGlobalSets(), ...this.api.listChatSets()]
677 .map(it=>this.api.getSetByName(it)?.qrList ?? [])
678 .flat()
679 .find(it=>it.label == args.from)
680 ;
681 if (!qr) {
682 let [setName, ...qrNameParts] = args.from.split('.');
683 let qrName = qrNameParts.join('.');
684 let qrs = QuickReplySet.get(setName);
685 if (qrs) {
686 qr = qrs.qrList.find(it=>it.label == qrName);
687 }
688 }
689 if (qr) {
690 const parser = new SlashCommandParser();
691 const closure = parser.parse(qr.message, true, [], args._abortController, args._debugController);
692 if (args._debugController) {
693 closure.source = args.from;
694 }
695 const testCandidates = (executor)=>{
696 return (
697 executor.namedArgumentList.find(arg=>arg.name == 'key')
698 && executor.unnamedArgumentList.length > 0
699 && executor.unnamedArgumentList[0].value instanceof SlashCommandClosure
700 ) || (
701 !executor.namedArgumentList.find(arg=>arg.name == 'key')
702 && executor.unnamedArgumentList.length > 1
703 && executor.unnamedArgumentList[1].value instanceof SlashCommandClosure
704 );
705 };
706 const candidates = closure.executorList
707 .filter(executor=>['let', 'var'].includes(executor.command.name))
708 .filter(testCandidates)
709 .map(executor=>({
710 key: executor.namedArgumentList.find(arg=>arg.name == 'key')?.value ?? executor.unnamedArgumentList[0].value,
711 value: executor.unnamedArgumentList[executor.namedArgumentList.find(arg=>arg.name == 'key') ? 0 : 1].value,
712 }))
713 ;
714 for (let i = 0; i < value.length; i++) {
715 const srcName = value[i];
716 let dstName = srcName;
717 if (i + 2 < value.length && value[i + 1] == 'as') {
718 dstName = value[i + 2];
719 i += 2;
720 }
721 const pick = candidates.find(it=>it.key == srcName);
722 if (!pick) throw new Error(`No scoped closure named "${srcName}" found in "${args.from}"`);
723 if (args._scope.existsVariableInScope(dstName)) {
724 args._scope.setVariable(dstName, pick.value);
725 } else {
726 args._scope.letVariable(dstName, pick.value);
727 }
728 }
729 } else {
730 throw new Error(`No Quick Reply found for "${name}".`);
731 }
732 return '';
733 },
734 namedArgumentList: [
735 SlashCommandNamedArgument.fromProps({ name: 'from',
736 description: 'Quick Reply to import from (QRSet.QRLabel)',
737 typeList: ARGUMENT_TYPE.STRING,
738 isRequired: true,
739 }),
740 ],
741 unnamedArgumentList: [
742 SlashCommandArgument.fromProps({ description: 'what to import (x or x as y)',
743 acceptsMultiple: true,
744 typeList: ARGUMENT_TYPE.STRING,
745 isRequired: true,
746 }),
747 ],
748 splitUnnamedArgument: true,
749 helpString: `
750 <div>
751 Import one or more closures from another Quick Reply.
752 </div>
753 <div>
754 Only imports closures that are directly assigned a scoped variable via <code>/let</code> or <code>/var</code>.
755 </div>
756 <div>
757 <strong>Examples:</strong>
758 <ul>
759 <li><pre><code>/import from=LibraryQrSet.FooBar foo |\n/:foo</code></pre></li>
760 <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo\n\tbar\n|\n/:foo |\n/:bar</code></pre></li>
761 <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo as x\n\tbar as y\n|\n/:x |\n/:y</code></pre></li>
762 </ul>
763 </div>
764 `,
765 }));
536 }766 }
537767
538768
@@ -618,6 +848,8 @@ export class SlashCommandHandler {
618 args.set ?? '',848 args.set ?? '',
619 args.label ?? '',849 args.label ?? '',
620 {850 {
851 icon: args.icon,
852 showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
621 message: message ?? '',853 message: message ?? '',
622 title: args.title,854 title: args.title,
623 isHidden: isTrueBoolean(args.hidden),855 isHidden: isTrueBoolean(args.hidden),
@@ -633,12 +865,21 @@ export class SlashCommandHandler {
633 toastr.error(ex.message);865 toastr.error(ex.message);
634 }866 }
635 }867 }
868 getQuickReply(args) {
869 try {
870 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
871 } catch (ex) {
872 toastr.error(ex.message);
873 }
874 }
636 updateQuickReply(args, message) {875 updateQuickReply(args, message) {
637 try {876 try {
638 this.api.updateQuickReply(877 this.api.updateQuickReply(
639 args.set ?? '',878 args.set ?? '',
640 args.label ?? '',879 args.id !== undefined ? Number(args.id) : (args.label ?? ''),
641 {880 {
881 icon: args.icon,
882 showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
642 newLabel: args.newlabel,883 newLabel: args.newlabel,
643 message: (message ?? '').trim().length > 0 ? message : undefined,884 message: (message ?? '').trim().length > 0 ? message : undefined,
644 title: args.title,885 title: args.title,
@@ -657,7 +898,7 @@ export class SlashCommandHandler {
657 }898 }
658 deleteQuickReply(args, label) {899 deleteQuickReply(args, label) {
659 try {900 try {
660 this.api.deleteQuickReply(args.set, args.label ?? label);901 this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label));
661 } catch (ex) {902 } catch (ex) {
662 toastr.error(ex.message);903 toastr.error(ex.message);
663 }904 }
@@ -692,9 +933,9 @@ export class SlashCommandHandler {
692 }933 }
693934
694935
695 createSet(name, args) {936 async createSet(name, args) {
696 try {937 try {
697 this.api.createSet(938 await this.api.createSet(
698 args.name ?? name ?? '',939 args.name ?? name ?? '',
699 {940 {
700 disableSend: isTrueBoolean(args.nosend),941 disableSend: isTrueBoolean(args.nosend),
@@ -706,9 +947,9 @@ export class SlashCommandHandler {
706 toastr.error(ex.message);947 toastr.error(ex.message);
707 }948 }
708 }949 }
709 updateSet(name, args) {950 async updateSet(name, args) {
710 try {951 try {
711 this.api.updateSet(952 await this.api.updateSet(
712 args.name ?? name ?? '',953 args.name ?? name ?? '',
713 {954 {
714 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,955 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,
@@ -720,9 +961,9 @@ export class SlashCommandHandler {
720 toastr.error(ex.message);961 toastr.error(ex.message);
721 }962 }
722 }963 }
723 deleteSet(name) {964 async deleteSet(name) {
724 try {965 try {
725 this.api.deleteSet(name ?? '');966 await this.api.deleteSet(name ?? '');
726 } catch (ex) {967 } catch (ex) {
727 toastr.error(ex.message);968 toastr.error(ex.message);
728 }969 }
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js+14 -11
@@ -69,17 +69,20 @@ export class ButtonUi {
69 root.id = 'qr--bar';69 root.id = 'qr--bar';
70 root.classList.add('flex-container');70 root.classList.add('flex-container');
71 root.classList.add('flexGap5');71 root.classList.add('flexGap5');
72 const popout = document.createElement('div'); {72 if (this.settings.showPopoutButton) {
73 popout.id = 'qr--popoutTrigger';73 root.classList.add('popoutVisible');
74 popout.classList.add('menu_button');74 const popout = document.createElement('div'); {
75 popout.classList.add('fa-solid');75 popout.id = 'qr--popoutTrigger';
76 popout.classList.add('fa-window-restore');76 popout.classList.add('menu_button');
77 popout.addEventListener('click', ()=>{77 popout.classList.add('fa-solid');
78 this.settings.isPopout = true;78 popout.classList.add('fa-window-restore');
79 this.refresh();79 popout.addEventListener('click', ()=>{
80 this.settings.save();80 this.settings.isPopout = true;
81 });81 this.refresh();
82 root.append(popout);82 this.settings.save();
83 });
84 root.append(popout);
85 }
83 }86 }
84 if (this.settings.isCombined) {87 if (this.settings.isCombined) {
85 const buttons = document.createElement('div'); {88 const buttons = document.createElement('div'); {
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+64 -3
@@ -14,6 +14,7 @@ export class SettingsUi {
1414
15 /**@type {HTMLInputElement}*/ isEnabled;15 /**@type {HTMLInputElement}*/ isEnabled;
16 /**@type {HTMLInputElement}*/ isCombined;16 /**@type {HTMLInputElement}*/ isCombined;
17 /**@type {HTMLInputElement}*/ showPopoutButton;
1718
18 /**@type {HTMLElement}*/ globalSetList;19 /**@type {HTMLElement}*/ globalSetList;
1920
@@ -23,6 +24,8 @@ export class SettingsUi {
23 /**@type {HTMLInputElement}*/ disableSend;24 /**@type {HTMLInputElement}*/ disableSend;
24 /**@type {HTMLInputElement}*/ placeBeforeInput;25 /**@type {HTMLInputElement}*/ placeBeforeInput;
25 /**@type {HTMLInputElement}*/ injectInput;26 /**@type {HTMLInputElement}*/ injectInput;
27 /**@type {HTMLInputElement}*/ color;
28 /**@type {HTMLInputElement}*/ onlyBorderColor;
26 /**@type {HTMLSelectElement}*/ currentSet;29 /**@type {HTMLSelectElement}*/ currentSet;
2730
2831
@@ -77,6 +80,10 @@ export class SettingsUi {
77 this.isCombined = this.dom.querySelector('#qr--isCombined');80 this.isCombined = this.dom.querySelector('#qr--isCombined');
78 this.isCombined.checked = this.settings.isCombined;81 this.isCombined.checked = this.settings.isCombined;
79 this.isCombined.addEventListener('click', ()=>this.onIsCombined());82 this.isCombined.addEventListener('click', ()=>this.onIsCombined());
83
84 this.showPopoutButton = this.dom.querySelector('#qr--showPopoutButton');
85 this.showPopoutButton.checked = this.settings.showPopoutButton;
86 this.showPopoutButton.addEventListener('click', ()=>this.onShowPopoutButton());
80 }87 }
8188
82 prepareGlobalSetList() {89 prepareGlobalSetList() {
@@ -117,10 +124,29 @@ export class SettingsUi {
117 this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{124 this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
118 this.currentQrSet.addQuickReply();125 this.currentQrSet.addQuickReply();
119 });126 });
127 this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{
128 const text = await navigator.clipboard.readText();
129 this.currentQrSet.addQuickReplyFromText(text);
130 });
131 this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{
132 const inp = document.createElement('input'); {
133 inp.type = 'file';
134 inp.accept = '.json';
135 inp.addEventListener('change', async()=>{
136 if (inp.files.length > 0) {
137 for (const file of inp.files) {
138 const text = await file.text();
139 this.currentQrSet.addQuickReply(JSON.parse(text));
140 }
141 }
142 });
143 inp.click();
144 }
145 });
120 this.qrList = this.dom.querySelector('#qr--set-qrList');146 this.qrList = this.dom.querySelector('#qr--set-qrList');
121 this.currentSet = this.dom.querySelector('#qr--set');147 this.currentSet = this.dom.querySelector('#qr--set');
122 this.currentSet.addEventListener('change', ()=>this.onQrSetChange());148 this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
123 QuickReplySet.list.forEach(qrs=>{149 QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
124 const opt = document.createElement('option'); {150 const opt = document.createElement('option'); {
125 opt.value = qrs.name;151 opt.value = qrs.name;
126 opt.textContent = qrs.name;152 opt.textContent = qrs.name;
@@ -145,6 +171,34 @@ export class SettingsUi {
145 qrs.injectInput = this.injectInput.checked;171 qrs.injectInput = this.injectInput.checked;
146 qrs.save();172 qrs.save();
147 });173 });
174 let initialColorChange = true;
175 this.color = this.dom.querySelector('#qr--color');
176 this.color.color = this.currentQrSet?.color ?? 'transparent';
177 this.color.addEventListener('change', (evt)=>{
178 if (!this.dom.closest('body')) return;
179 const qrs = this.currentQrSet;
180 if (initialColorChange) {
181 initialColorChange = false;
182 this.color.color = qrs.color;
183 return;
184 }
185 qrs.color = evt.detail.rgb;
186 qrs.save();
187 this.currentQrSet.updateColor();
188 });
189 this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
190 const qrs = this.currentQrSet;
191 this.color.color = 'transparent';
192 qrs.save();
193 this.currentQrSet.updateColor();
194 });
195 this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor');
196 this.onlyBorderColor.addEventListener('click', ()=>{
197 const qrs = this.currentQrSet;
198 qrs.onlyBorderColor = this.onlyBorderColor.checked;
199 qrs.save();
200 this.currentQrSet.updateColor();
201 });
148 this.onQrSetChange();202 this.onQrSetChange();
149 }203 }
150 onQrSetChange() {204 onQrSetChange() {
@@ -152,6 +206,8 @@ export class SettingsUi {
152 this.disableSend.checked = this.currentQrSet.disableSend;206 this.disableSend.checked = this.currentQrSet.disableSend;
153 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;207 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
154 this.injectInput.checked = this.currentQrSet.injectInput;208 this.injectInput.checked = this.currentQrSet.injectInput;
209 this.color.color = this.currentQrSet.color ?? 'transparent';
210 this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
155 this.qrList.innerHTML = '';211 this.qrList.innerHTML = '';
156 const qrsDom = this.currentQrSet.renderSettings();212 const qrsDom = this.currentQrSet.renderSettings();
157 this.qrList.append(qrsDom);213 this.qrList.append(qrsDom);
@@ -184,6 +240,11 @@ export class SettingsUi {
184 this.settings.save();240 this.settings.save();
185 }241 }
186242
243 async onShowPopoutButton() {
244 this.settings.showPopoutButton = this.showPopoutButton.checked;
245 this.settings.save();
246 }
247
187 async onGlobalSetListSort() {248 async onGlobalSetListSort() {
188 this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{249 this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{
189 const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];250 const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
@@ -265,7 +326,7 @@ export class SettingsUi {
265 const qrs = new QuickReplySet();326 const qrs = new QuickReplySet();
266 qrs.name = name;327 qrs.name = name;
267 qrs.addQuickReply();328 qrs.addQuickReply();
268 const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);329 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
269 if (idx > -1) {330 if (idx > -1) {
270 QuickReplySet.list.splice(idx, 0, qrs);331 QuickReplySet.list.splice(idx, 0, qrs);
271 } else {332 } else {
@@ -321,7 +382,7 @@ export class SettingsUi {
321 this.prepareChatSetList();382 this.prepareChatSetList();
322 }383 }
323 } else {384 } else {
324 const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(qrs.name) == 1);385 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
325 if (idx > -1) {386 if (idx > -1) {
326 QuickReplySet.list.splice(idx, 0, qrs);387 QuickReplySet.list.splice(idx, 0, qrs);
327 } else {388 } else {
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js+5 -0
@@ -33,11 +33,14 @@ export class ContextMenu {
33 */33 */
34 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {34 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
35 const tree = {35 const tree = {
36 icon: qr.icon,
37 showLabel: qr.showLabel,
36 label: qr.label,38 label: qr.label,
37 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,39 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
38 children: [],40 children: [],
39 };41 };
40 qr.contextList.forEach((cl) => {42 qr.contextList.forEach((cl) => {
43 if (!cl.set) return;
41 if (!hierarchy.includes(cl.set)) {44 if (!hierarchy.includes(cl.set)) {
42 const nextHierarchy = [...hierarchy, cl.set];45 const nextHierarchy = [...hierarchy, cl.set];
43 const nextLabelHierarchy = [...labelHierarchy, tree.label];46 const nextLabelHierarchy = [...labelHierarchy, tree.label];
@@ -45,6 +48,8 @@ export class ContextMenu {
45 cl.set.qrList.forEach(subQr => {48 cl.set.qrList.forEach(subQr => {
46 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);49 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
47 tree.children.push(new MenuItem(50 tree.children.push(new MenuItem(
51 subTree.icon,
52 subTree.showLabel,
48 subTree.label,53 subTree.label,
49 subTree.message,54 subTree.message,
50 (evt) => {55 (evt) => {
public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js+1 -1
@@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js';
22
3export class MenuHeader extends MenuItem {3export class MenuHeader extends MenuItem {
4 constructor(/**@type {String}*/label) {4 constructor(/**@type {String}*/label) {
5 super(label, null, null);5 super(null, null, label, null, null);
6 }6 }
77
88
public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js+34 -7
@@ -1,21 +1,34 @@
1import { SubMenu } from './SubMenu.js';1import { SubMenu } from './SubMenu.js';
22
3export class MenuItem {3export class MenuItem {
4 /**@type {String}*/ label;4 /**@type {string}*/ icon;
5 /**@type {Object}*/ value;5 /**@type {boolean}*/ showLabel;
6 /**@type {Function}*/ callback;6 /**@type {string}*/ label;
7 /**@type {object}*/ value;
8 /**@type {function}*/ callback;
7 /**@type {MenuItem[]}*/ childList = [];9 /**@type {MenuItem[]}*/ childList = [];
8 /**@type {SubMenu}*/ subMenu;10 /**@type {SubMenu}*/ subMenu;
9 /**@type {Boolean}*/ isForceExpanded = false;11 /**@type {boolean}*/ isForceExpanded = false;
1012
11 /**@type {HTMLElement}*/ root;13 /**@type {HTMLElement}*/ root;
1214
13 /**@type {Function}*/ onExpand;15 /**@type {function}*/ onExpand;
1416
1517
1618
1719
18 constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) {20 /**
21 *
22 * @param {string} icon
23 * @param {boolean} showLabel
24 * @param {string} label
25 * @param {object} value
26 * @param {function} callback
27 * @param {MenuItem[]} children
28 */
29 constructor(icon, showLabel, label, value, callback, children = []) {
30 this.icon = icon;
31 this.showLabel = showLabel;
19 this.label = label;32 this.label = label;
20 this.value = value;33 this.value = value;
21 this.callback = callback;34 this.callback = callback;
@@ -33,7 +46,21 @@ export class MenuItem {
33 if (this.callback) {46 if (this.callback) {
34 item.addEventListener('click', (evt) => this.callback(evt, this));47 item.addEventListener('click', (evt) => this.callback(evt, this));
35 }48 }
36 item.append(this.label);49 const icon = document.createElement('div'); {
50 this.domIcon = icon;
51 icon.classList.add('qr--button-icon');
52 icon.classList.add('fa-solid');
53 if (!this.icon) icon.classList.add('qr--hidden');
54 else icon.classList.add(this.icon);
55 item.append(icon);
56 }
57 const lbl = document.createElement('div'); {
58 this.domLabel = lbl;
59 lbl.classList.add('qr--button-label');
60 if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
61 lbl.textContent = this.label;
62 item.append(lbl);
63 }
37 if (this.childList.length > 0) {64 if (this.childList.length > 0) {
38 item.classList.add('ctx-has-children');65 item.classList.add('ctx-has-children');
39 const sub = new SubMenu(this.childList);66 const sub = new SubMenu(this.childList);
public/scripts/extensions/quick-reply/style.css+604 -20
@@ -1,3 +1,20 @@
1@keyframes qr--success {
2 0%,
3 100% {
4 color: var(--SmartThemeBodyColor);
5 }
6 25%,
7 75% {
8 color: #51a351;
9 }
10}
11.qr--success {
12 animation-name: qr--success;
13 animation-duration: 3s;
14 animation-timing-function: linear;
15 animation-delay: 0s;
16 animation-iteration-count: 1;
17}
1#qr--bar {18#qr--bar {
2 outline: none;19 outline: none;
3 margin: 0;20 margin: 0;
@@ -10,7 +27,6 @@
10 max-width: 100%;27 max-width: 100%;
11 overflow-x: auto;28 overflow-x: auto;
12 order: 1;29 order: 1;
13 padding-right: 2.5em;
14 position: relative;30 position: relative;
15}31}
16#qr--bar > #qr--popoutTrigger {32#qr--bar > #qr--popoutTrigger {
@@ -18,6 +34,9 @@
18 right: 0.25em;34 right: 0.25em;
19 top: 0;35 top: 0;
20}36}
37#qr--bar.popoutVisible {
38 padding-right: 2.5em;
39}
21#qr--popout {40#qr--popout {
22 display: flex;41 display: flex;
23 flex-direction: column;42 flex-direction: column;
@@ -41,6 +60,7 @@
41}60}
42#qr--bar > .qr--buttons,61#qr--bar > .qr--buttons,
43#qr--popout > .qr--body > .qr--buttons {62#qr--popout > .qr--body > .qr--buttons {
63 --qr--color: transparent;
44 margin: 0;64 margin: 0;
45 padding: 0;65 padding: 0;
46 display: flex;66 display: flex;
@@ -49,10 +69,44 @@
49 gap: 5px;69 gap: 5px;
50 width: 100%;70 width: 100%;
51}71}
72#qr--bar > .qr--buttons.qr--color,
73#qr--popout > .qr--body > .qr--buttons.qr--color {
74 background-color: var(--qr--color);
75}
76#qr--bar > .qr--buttons.qr--borderColor,
77#qr--popout > .qr--body > .qr--buttons.qr--borderColor {
78 background-color: transparent;
79 border-left: 5px solid var(--qr--color);
80 border-right: 5px solid var(--qr--color);
81}
82#qr--bar > .qr--buttons:has(.qr--buttons.qr--color),
83#qr--popout > .qr--body > .qr--buttons:has(.qr--buttons.qr--color) {
84 margin: 5px;
85}
52#qr--bar > .qr--buttons > .qr--buttons,86#qr--bar > .qr--buttons > .qr--buttons,
53#qr--popout > .qr--body > .qr--buttons > .qr--buttons {87#qr--popout > .qr--body > .qr--buttons > .qr--buttons {
54 display: contents;88 display: contents;
55}89}
90#qr--bar > .qr--buttons > .qr--buttons.qr--color .qr--button:before,
91#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color .qr--button:before {
92 content: '';
93 background-color: var(--qr--color);
94 position: absolute;
95 inset: -5px;
96 z-index: -1;
97}
98#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before,
99#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before {
100 display: none;
101}
102#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
103#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
104#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after,
105#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after {
106 content: '';
107 width: 5px;
108 background-color: var(--qr--color);
109}
56#qr--bar > .qr--buttons .qr--button,110#qr--bar > .qr--buttons .qr--button,
57#qr--popout > .qr--body > .qr--buttons .qr--button {111#qr--popout > .qr--body > .qr--buttons .qr--button {
58 color: var(--SmartThemeBodyColor);112 color: var(--SmartThemeBodyColor);
@@ -66,11 +120,19 @@
66 align-items: center;120 align-items: center;
67 justify-content: center;121 justify-content: center;
68 text-align: center;122 text-align: center;
123 position: relative;
69}124}
70#qr--bar > .qr--buttons .qr--button:hover,125#qr--bar > .qr--buttons .qr--button:hover,
71#qr--popout > .qr--body > .qr--buttons .qr--button:hover {126#qr--popout > .qr--body > .qr--buttons .qr--button:hover {
72 opacity: 1;127 background-color: #4d4d4d;
73 filter: brightness(1.2);128}
129#qr--bar > .qr--buttons .qr--button .qr--hidden,
130#qr--popout > .qr--body > .qr--buttons .qr--button .qr--hidden {
131 display: none;
132}
133#qr--bar > .qr--buttons .qr--button .qr--button-icon,
134#qr--popout > .qr--body > .qr--buttons .qr--button .qr--button-icon {
135 margin: 0 0.5em;
74}136}
75#qr--bar > .qr--buttons .qr--button > .qr--button-expander,137#qr--bar > .qr--buttons .qr--button > .qr--button-expander,
76#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {138#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {
@@ -170,36 +232,80 @@
170#qr--settings #qr--set-qrList .qr--set-qrListContents {232#qr--settings #qr--set-qrList .qr--set-qrListContents {
171 padding: 0 0.5em;233 padding: 0 0.5em;
172}234}
173#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {235#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder {
236 display: flex;
237 align-items: center;
238 opacity: 0;
239 transition: 100ms;
240 margin: -2px 0 -11px 0;
241 position: relative;
242}
243#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions {
244 display: flex;
245 gap: 0.25em;
246 flex: 0 0 auto;
247}
248#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions .qr--action {
249 margin: 0;
250}
251#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:before,
252#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:after {
253 content: "";
254 display: block;
255 flex: 1 1 auto;
256 border: 1px solid;
257 margin: 0 1em;
258 height: 0;
259}
260#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:hover,
261#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:focus-within {
262 opacity: 1;
263}
264#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content {
174 display: flex;265 display: flex;
175 flex-direction: row;266 flex-direction: row;
176 gap: 0.5em;267 gap: 0.5em;
177 align-items: baseline;268 align-items: baseline;
178 padding: 0.25em 0;269 padding: 0.25em 0;
179}270}
180#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {271#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
181 flex: 0 0 auto;272 flex: 0 0 auto;
182}273}
183#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {274#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
184 flex: 1 1 25%;275 flex: 1 1 25%;
185}276}
186#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {277#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) {
187 flex: 0 0 auto;278 flex: 0 0 auto;
188}279}
189#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {280#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) {
190 flex: 1 1 75%;281 flex: 1 1 75%;
191}282}
192#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {283#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) {
193 flex: 0 0 auto;284 flex: 0 1 auto;
285 display: flex;
286 gap: 0.25em;
287 justify-content: flex-end;
288 flex-wrap: wrap;
194}289}
195#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle {290#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle {
196 padding: 0.75em;291 padding: 0.75em;
197}292}
198#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,293#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer {
199#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {294 display: flex;
295 align-items: center;
296 gap: 0.5em;
297}
298#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) {
299 display: none;
300}
301#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemLabel {
302 min-width: 24px;
303}
304#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,
305#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--action {
200 margin: 0;306 margin: 0;
201}307}
202#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {308#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage {
203 font-size: smaller;309 font-size: smaller;
204}310}
205#qr--settings .qr--set-qrListActions {311#qr--settings .qr--set-qrListActions {
@@ -212,6 +318,7 @@
212#qr--qrOptions {318#qr--qrOptions {
213 display: flex;319 display: flex;
214 flex-direction: column;320 flex-direction: column;
321 padding-right: 1px;
215}322}
216#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {323#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
217 display: flex;324 display: flex;
@@ -219,6 +326,12 @@
219 gap: 0.5em;326 gap: 0.5em;
220 align-items: baseline;327 align-items: baseline;
221}328}
329#qr--qrOptions > #qr--autoExec .checkbox_label {
330 text-wrap: nowrap;
331}
332#qr--qrOptions > #qr--autoExec .checkbox_label .fa-fw {
333 margin-right: 2px;
334}
222@media screen and (max-width: 750px) {335@media screen and (max-width: 750px) {
223 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {336 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
224 flex-direction: column;337 flex-direction: column;
@@ -238,6 +351,72 @@
238.popup:has(#qr--modalEditor) {351.popup:has(#qr--modalEditor) {
239 aspect-ratio: unset;352 aspect-ratio: unset;
240}353}
354.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {
355 min-width: unset;
356 min-height: unset;
357 height: auto !important;
358 width: min-content !important;
359 position: absolute;
360 right: 1em;
361 top: 1em;
362 left: unset;
363 bottom: unset;
364 margin: unset;
365 padding: 0;
366}
367.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized)::backdrop {
368 backdrop-filter: unset;
369 background-color: transparent;
370}
371.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-body {
372 flex: 0 0 auto;
373 height: min-content;
374 width: min-content;
375}
376.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content {
377 flex: 0 0 auto;
378 margin-top: 0;
379}
380.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor {
381 max-height: 50vh;
382}
383.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--main,
384.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--resizeHandle,
385.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > h3,
386.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeButtons,
387.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeProgress {
388 display: none;
389}
390.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--qrOptions {
391 width: auto;
392}
393.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
394 display: flex;
395}
396.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
397 display: none;
398}
399.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugState {
400 padding-top: 0;
401}
402.popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls {
403 display: none;
404}
405.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight {
406 position: absolute;
407 z-index: 50000;
408 pointer-events: none;
409 background-color: rgba(47, 150, 180, 0.5);
410}
411.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight.qr--unresolved {
412 background-color: rgba(255, 255, 0, 0.5);
413}
414.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary {
415 position: absolute;
416 z-index: 50000;
417 pointer-events: none;
418 border: 3px solid red;
419}
241.popup:has(#qr--modalEditor) .popup-content {420.popup:has(#qr--modalEditor) .popup-content {
242 display: flex;421 display: flex;
243 flex-direction: column;422 flex-direction: column;
@@ -249,6 +428,67 @@
249 gap: 1em;428 gap: 1em;
250 overflow: hidden;429 overflow: hidden;
251}430}
431.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > h3:first-child,
432.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--labels,
433.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
434.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > h3:first-child,
435.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > #qr--ctxEditor,
436.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions,
437.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3,
438.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3 + div {
439 display: none;
440}
441.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
442 visibility: hidden;
443}
444.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons {
445 display: flex;
446}
447.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
448 cursor: not-allowed;
449 opacity: 0.5;
450 pointer-events: none;
451 transition: 200ms;
452 border-color: transparent;
453}
454.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
455 cursor: pointer;
456 opacity: 1;
457 pointer-events: all;
458}
459.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume {
460 animation-name: qr--debugPulse;
461 animation-duration: 1500ms;
462 animation-timing-function: ease-in-out;
463 animation-delay: 0s;
464 animation-iteration-count: infinite;
465}
466.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume {
467 border-color: #51a351;
468}
469.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-step {
470 border-color: var(--SmartThemeQuoteColor);
471}
472.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepInto {
473 border-color: var(--SmartThemeQuoteColor);
474}
475.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepOut {
476 border-color: var(--SmartThemeQuoteColor);
477}
478.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle {
479 width: 6px;
480 background-color: var(--SmartThemeBorderColor);
481 border: 2px solid var(--SmartThemeBlurTintColor);
482 transition: border-color 200ms, background-color 200ms;
483 cursor: w-resize;
484}
485.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle:hover {
486 background-color: var(--SmartThemeQuoteColor);
487 border-color: var(--SmartThemeQuoteColor);
488}
489.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions {
490 width: var(--width, auto);
491}
252.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {492.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
253 flex: 1 1 auto;493 flex: 1 1 auto;
254 display: flex;494 display: flex;
@@ -260,21 +500,115 @@
260 display: flex;500 display: flex;
261 flex-direction: row;501 flex-direction: row;
262 gap: 0.5em;502 gap: 0.5em;
503 padding: 1px;
263}504}
264.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {505.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label,
506.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label {
265 flex: 1 1 1px;507 flex: 1 1 1px;
266 display: flex;508 display: flex;
267 flex-direction: column;509 flex-direction: column;
510 position: relative;
268}511}
269.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {512.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label.qr--fit,
513.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label.qr--fit {
514 flex: 0 0 auto;
515 justify-content: center;
516}
517.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup,
518.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup {
519 display: flex;
520 align-items: baseline;
521 gap: 0.5em;
522}
523.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup input,
524.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup input {
270 flex: 1 1 auto;525 flex: 1 1 auto;
271}526}
272.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {527.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--labelText,
528.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelText {
273 flex: 1 1 auto;529 flex: 1 1 auto;
274}530}
275.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {531.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--labelHint,
532.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelHint {
533 flex: 1 1 auto;
534}
535.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label input,
536.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label input {
276 flex: 0 0 auto;537 flex: 0 0 auto;
277}538}
539.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList,
540.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList {
541 background-color: var(--stcdx--bgColor);
542 border: 1px solid var(--SmartThemeBorderColor);
543 backdrop-filter: blur(var(--SmartThemeBlurStrength));
544 border-radius: 10px;
545 font-size: smaller;
546 position: absolute;
547 top: 100%;
548 left: 0;
549 right: 0;
550 overflow: auto;
551 margin: 0;
552 padding: 0.5em;
553 max-height: 50vh;
554 list-style: none;
555 z-index: 40000;
556 max-width: 100%;
557}
558.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem,
559.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem {
560 display: flex;
561 gap: 1em;
562 text-align: left;
563 opacity: 0.75;
564 transition: 200ms;
565 cursor: pointer;
566}
567.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem:hover,
568.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem:hover {
569 opacity: 1;
570}
571.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current,
572.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current {
573 opacity: 1;
574}
575.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
576.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
577.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id,
578.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id {
579 font-weight: bold;
580}
581.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label,
582.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label {
583 white-space: nowrap;
584}
585.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label .menu_button,
586.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label .menu_button {
587 display: inline-block;
588 height: min-content;
589 width: min-content;
590 margin: 0 0.5em 0 0;
591}
592.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id,
593.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id {
594 opacity: 0.5;
595}
596.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:before,
597.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:before {
598 content: "[";
599}
600.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:after,
601.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:after {
602 content: "]";
603}
604.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--message,
605.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--message {
606 height: 1lh;
607 overflow: hidden;
608 text-overflow: ellipsis;
609 white-space: nowrap;
610 opacity: 0.5;
611}
278.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {612.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
279 flex: 1 1 auto;613 flex: 1 1 auto;
280 display: flex;614 display: flex;
@@ -283,8 +617,9 @@
283}617}
284.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {618.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
285 display: flex;619 display: flex;
620 flex-wrap: wrap;
286 flex-direction: row;621 flex-direction: row;
287 gap: 1em;622 column-gap: 1em;
288 color: var(--grey70);623 color: var(--grey70);
289 font-size: smaller;624 font-size: smaller;
290 align-items: baseline;625 align-items: baseline;
@@ -308,6 +643,11 @@
308 background-color: var(--ac-style-color-background);643 background-color: var(--ac-style-color-background);
309 color: var(--ac-style-color-text);644 color: var(--ac-style-color-text);
310}645}
646.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar,
647.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar-thumb {
648 visibility: visible;
649 cursor: unset;
650}
311.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {651.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
312 color: unset;652 color: unset;
313 background-color: rgba(108 171 251 / 0.25);653 background-color: rgba(108 171 251 / 0.25);
@@ -357,11 +697,15 @@
357 font-family: var(--monoFontFamily);697 font-family: var(--monoFontFamily);
358 padding: 0.75em;698 padding: 0.75em;
359 margin: 0;699 margin: 0;
360 border: none;
361 resize: none;700 resize: none;
362 line-height: 1.2;701 line-height: 1.2;
363 border: 1px solid var(--SmartThemeBorderColor);702 border: 1px solid var(--SmartThemeBorderColor);
364 border-radius: 5px;703 border-radius: 5px;
704 position: relative;
705}
706.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-icon {
707 height: 100%;
708 aspect-ratio: 1 / 1;
365}709}
366.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {710.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {
367 display: flex;711 display: flex;
@@ -410,6 +754,46 @@
410.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {754.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
411 border-color: #d78872;755 border-color: #d78872;
412}756}
757.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons {
758 display: none;
759 gap: 1em;
760}
761.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton {
762 aspect-ratio: 1.25 / 1;
763 width: 2.25em;
764 position: relative;
765}
766.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid) {
767 border-width: 1px;
768 border-style: solid;
769}
770.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid):after {
771 content: '';
772 position: absolute;
773 inset: 3px;
774 background-color: var(--SmartThemeBodyColor);
775 mask-size: contain;
776 mask-position: center;
777 mask-repeat: no-repeat;
778}
779.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-resume:after {
780 mask-image: url('/img/step-resume.svg');
781}
782.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-step:after {
783 mask-image: url('/img/step-over.svg');
784}
785.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepInto:after {
786 mask-image: url('/img/step-into.svg');
787}
788.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepOut:after {
789 mask-image: url('/img/step-out.svg');
790}
791.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
792 display: none;
793}
794.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-send_textarea {
795 flex: 0 0 auto;
796}
413.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {797.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {
414 --prog: 0;798 --prog: 0;
415 --progColor: #92befc;799 --progColor: #92befc;
@@ -417,6 +801,7 @@
417 --progSuccessColor: #51a351;801 --progSuccessColor: #51a351;
418 --progErrorColor: #bd362f;802 --progErrorColor: #bd362f;
419 --progAbortedColor: #d78872;803 --progAbortedColor: #d78872;
804 flex: 0 0 auto;
420 height: 0.5em;805 height: 0.5em;
421 background-color: var(--black50a);806 background-color: var(--black50a);
422 position: relative;807 position: relative;
@@ -469,6 +854,7 @@
469 overflow: auto;854 overflow: auto;
470 min-width: 100%;855 min-width: 100%;
471 width: 0;856 width: 0;
857 white-space: pre-wrap;
472}858}
473.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {859.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
474 display: block;860 display: block;
@@ -476,6 +862,150 @@
476.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {862.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {
477 content: 'Result: ';863 content: 'Result: ';
478}864}
865.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState {
866 display: none;
867 text-align: left;
868 font-size: smaller;
869 font-family: var(--monoFontFamily);
870 color: white;
871 padding: 0.5em 0;
872 overflow: auto;
873 min-width: 100%;
874 width: 0;
875 white-space: pre-wrap;
876}
877.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState.qr--active {
878 display: block;
879}
880.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope {
881 display: grid;
882 grid-template-columns: 0fr 1fr 1fr;
883 column-gap: 0em;
884}
885.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title {
886 grid-column: 1 / 4;
887 font-weight: bold;
888 font-family: var(--mainFontFamily);
889 background-color: var(--black50a);
890 padding: 0.25em;
891 margin-top: 0.5em;
892}
893.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var,
894.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro,
895.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe {
896 display: contents;
897}
898.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--key,
899.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--key,
900.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--key,
901.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val,
902.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val,
903.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val {
904 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
905}
906.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:nth-child(2n),
907.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:nth-child(2n),
908.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:nth-child(2n) {
909 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
910}
911.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:hover,
912.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:hover,
913.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:hover {
914 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
915}
916.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:nth-child(2n),
917.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:nth-child(2n),
918.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:nth-child(2n) {
919 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
920}
921.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:hover,
922.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:hover,
923.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:hover {
924 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
925}
926.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--key,
927.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key,
928.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key,
929.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--val,
930.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--val,
931.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--val {
932 opacity: 0.5;
933}
934.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val,
935.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val,
936.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val {
937 grid-column: 2 / 4;
938}
939.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--singleCol,
940.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--singleCol,
941.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--singleCol {
942 grid-column: unset;
943}
944.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:before,
945.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:before,
946.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:before,
947.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:after,
948.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:after,
949.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:after {
950 content: '"';
951 color: var(--SmartThemeQuoteColor);
952}
953.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--unresolved:after,
954.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--unresolved:after,
955.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--unresolved:after {
956 content: '-UNRESOLVED-';
957 font-style: italic;
958 color: var(--SmartThemeQuoteColor);
959}
960.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key {
961 margin-left: 0.5em;
962 padding-right: 1em;
963}
964.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key:after {
965 content: ": ";
966}
967.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:before,
968.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:before {
969 content: "{{";
970}
971.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:after,
972.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:after {
973 content: "}}: ";
974}
975.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope {
976 display: contents;
977}
978.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--key,
979.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val {
980 opacity: 0.5;
981}
982.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack {
983 display: grid;
984 grid-template-columns: 1fr 0fr;
985}
986.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title {
987 grid-column: 1 / 3;
988 font-weight: bold;
989 font-family: var(--mainFontFamily);
990 background-color: var(--black50a);
991 padding: 0.25em;
992 margin-top: 1em;
993}
994.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item {
995 display: contents;
996}
997.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--name,
998.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--source {
999 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
1000}
1001.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--name {
1002 margin-left: 0.5em;
1003}
1004.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source {
1005 opacity: 0.5;
1006 text-align: right;
1007 white-space: nowrap;
1008}
479@keyframes qr--progressPulse {1009@keyframes qr--progressPulse {
480 0%,1010 0%,
481 100% {1011 100% {
@@ -485,9 +1015,63 @@
485 background-color: var(--progFlashColor);1015 background-color: var(--progFlashColor);
486 }1016 }
487}1017}
1018@keyframes qr--debugPulse {
1019 0%,
1020 100% {
1021 border-color: #51a351;
1022 }
1023 50% {
1024 border-color: #92befc;
1025 }
1026}
488.popup.qr--hide {1027.popup.qr--hide {
489 opacity: 0 !important;1028 opacity: 0 !important;
490}1029}
491.popup.qr--hide::backdrop {1030.popup.qr--hide::backdrop {
492 opacity: 0 !important;1031 opacity: 0 !important;
493}1032}
1033.popup.qr--hide::backdrop {
1034 opacity: 0 !important;
1035}
1036.popup:has(.qr--transferModal) .popup-button-ok {
1037 display: flex;
1038 align-items: center;
1039 flex-direction: column;
1040 white-space: pre;
1041 font-weight: normal;
1042 box-shadow: 0 0 0;
1043 transition: 200ms;
1044}
1045.popup:has(.qr--transferModal) .popup-button-ok:after {
1046 content: 'Transfer';
1047 height: 0;
1048 overflow: hidden;
1049 font-weight: bold;
1050}
1051.popup:has(.qr--transferModal) .qr--copy {
1052 display: flex;
1053 align-items: center;
1054 flex-direction: column;
1055 white-space: pre;
1056 font-weight: normal;
1057 box-shadow: 0 0 0;
1058 transition: 200ms;
1059}
1060.popup:has(.qr--transferModal) .qr--copy:after {
1061 content: 'Copy';
1062 height: 0;
1063 overflow: hidden;
1064 font-weight: bold;
1065}
1066.popup:has(.qr--transferModal):has(.qr--transferSelect:focus) .popup-button-ok {
1067 font-weight: bold;
1068 box-shadow: 0 0 10px;
1069}
1070.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .popup-button-ok {
1071 font-weight: normal;
1072 box-shadow: 0 0 0;
1073}
1074.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .qr--copy {
1075 font-weight: bold;
1076 box-shadow: 0 0 10px;
1077}
public/scripts/extensions/quick-reply/style.less+666 -126
@@ -1,3 +1,18 @@
1@keyframes qr--success {
2 0%, 100% {
3 color: var(--SmartThemeBodyColor);
4 }
5 25%, 75% {
6 color: rgb(81, 163, 81);
7 }
8}
9&.qr--success {
10 animation-name: qr--success;
11 animation-duration: 3s;
12 animation-timing-function: linear;
13 animation-delay: 0s;
14 animation-iteration-count: 1;
15}
1#qr--bar {16#qr--bar {
2 outline: none;17 outline: none;
3 margin: 0;18 margin: 0;
@@ -10,7 +25,6 @@
10 max-width: 100%;25 max-width: 100%;
11 overflow-x: auto;26 overflow-x: auto;
12 order: 1;27 order: 1;
13 padding-right: 2.5em;
14 position: relative;28 position: relative;
1529
16 >#qr--popoutTrigger {30 >#qr--popoutTrigger {
@@ -19,6 +33,9 @@
19 top: 0;33 top: 0;
20 }34 }
21}35}
36#qr--bar.popoutVisible {
37 padding-right: 2.5em;
38}
2239
23#qr--popout {40#qr--popout {
24 display: flex;41 display: flex;
@@ -50,6 +67,18 @@
50#qr--bar,67#qr--bar,
51#qr--popout>.qr--body {68#qr--popout>.qr--body {
52 >.qr--buttons {69 >.qr--buttons {
70 --qr--color: transparent;
71 &.qr--color {
72 background-color: var(--qr--color);
73 }
74 &.qr--borderColor {
75 background-color: transparent;
76 border-left: 5px solid var(--qr--color);
77 border-right: 5px solid var(--qr--color);
78 }
79 &:has(.qr--buttons.qr--color) {
80 margin: 5px;
81 }
53 margin: 0;82 margin: 0;
54 padding: 0;83 padding: 0;
55 display: flex;84 display: flex;
@@ -60,6 +89,25 @@
6089
61 >.qr--buttons {90 >.qr--buttons {
62 display: contents;91 display: contents;
92 &.qr--color {
93 .qr--button:before {
94 content: '';
95 background-color: var(--qr--color);
96 position: absolute;
97 inset: -5px;
98 z-index: -1;
99 }
100 &.qr--borderColor {
101 .qr--button:before {
102 display: none;
103 }
104 &:before, &:after {
105 content: '';
106 width: 5px;
107 background-color: var(--qr--color);
108 }
109 }
110 }
63 }111 }
64112
65 .qr--button {113 .qr--button {
@@ -75,10 +123,17 @@
75 align-items: center;123 align-items: center;
76 justify-content: center;124 justify-content: center;
77 text-align: center;125 text-align: center;
126 position: relative;
78127
79 &:hover {128 &:hover {
80 opacity: 1;129 background-color: rgb(30% 30% 30%);
81 filter: brightness(1.2);130 }
131
132 .qr--hidden {
133 display: none;
134 }
135 .qr--button-icon {
136 margin: 0 0.5em;
82 }137 }
83138
84 >.qr--button-expander {139 >.qr--button-expander {
@@ -211,14 +266,41 @@
211 .qr--set-qrListContents> {266 .qr--set-qrListContents> {
212 padding: 0 0.5em;267 padding: 0 0.5em;
213268
214 >.qr--set-item {269 >.qr--set-item .qr--set-itemAdder {
270 display: flex;
271 align-items: center;
272 opacity: 0;
273 transition: 100ms;
274 margin: -2px 0 -11px 0;
275 position: relative;
276 .qr--actions {
277 display: flex;
278 gap: 0.25em;
279 flex: 0 0 auto;
280 .qr--action {
281 margin: 0;
282 }
283 }
284 &:before, &:after {
285 content: "";
286 display: block;
287 flex: 1 1 auto;
288 border: 1px solid;
289 margin: 0 1em;
290 height: 0;
291 }
292 &:hover, &:focus-within {
293 opacity: 1;
294 }
295 }
296 >.qr--set-item .qr--content {
215 display: flex;297 display: flex;
216 flex-direction: row;298 flex-direction: row;
217 gap: 0.5em;299 gap: 0.5em;
218 align-items: baseline;300 align-items: baseline;
219 padding: 0.25em 0;301 padding: 0.25em 0;
220302
221 > :nth-child(1) {303 > :nth-child(2) {
222 flex: 0 0 auto;304 flex: 0 0 auto;
223 }305 }
224306
@@ -235,13 +317,29 @@
235 }317 }
236318
237 > :nth-child(5) {319 > :nth-child(5) {
238 flex: 0 0 auto;320 flex: 0 1 auto;
321 display: flex;
322 gap: 0.25em;
323 justify-content: flex-end;
324 flex-wrap: wrap;
239 }325 }
240326
241 >.drag-handle {327 >.drag-handle {
242 padding: 0.75em;328 padding: 0.75em;
243 }329 }
244330
331 .qr--set-itemLabelContainer {
332 display: flex;
333 align-items: center;
334 gap: 0.5em;
335 .qr--set-itemIcon:not(.fa-solid) {
336 display: none;
337 }
338 .qr--set-itemLabel {
339 min-width: 24px;
340 }
341 }
342
245 .qr--set-itemLabel,343 .qr--set-itemLabel,
246 .qr--action {344 .qr--action {
247 margin: 0;345 margin: 0;
@@ -251,6 +349,8 @@
251 font-size: smaller;349 font-size: smaller;
252 }350 }
253 }351 }
352
353
254 }354 }
255 }355 }
256356
@@ -270,6 +370,7 @@
270#qr--qrOptions {370#qr--qrOptions {
271 display: flex;371 display: flex;
272 flex-direction: column;372 flex-direction: column;
373 padding-right: 1px;
273374
274 >#qr--ctxEditor {375 >#qr--ctxEditor {
275 .qr--ctxItem {376 .qr--ctxItem {
@@ -279,6 +380,15 @@
279 align-items: baseline;380 align-items: baseline;
280 }381 }
281 }382 }
383 >#qr--autoExec {
384 .checkbox_label {
385 text-wrap: nowrap;
386
387 .fa-fw {
388 margin-right: 2px;
389 }
390 }
391 }
282}392}
283393
284394
@@ -306,6 +416,78 @@
306.popup:has(#qr--modalEditor) {416.popup:has(#qr--modalEditor) {
307 aspect-ratio: unset;417 aspect-ratio: unset;
308418
419 &:has(.qr--isExecuting.qr--minimized) {
420 min-width: unset;
421 min-height: unset;
422 height: auto !important;
423 width: min-content !important;
424 position: absolute;
425 right: 1em;
426 top: 1em;
427 left: unset;
428 bottom: unset;
429 margin: unset;
430 padding: 0;
431 &::backdrop {
432 backdrop-filter: unset;
433 background-color: transparent;
434 }
435 .popup-body {
436 flex: 0 0 auto;
437 height: min-content;
438 width: min-content;
439 }
440 .popup-content {
441 flex: 0 0 auto;
442 margin-top: 0;
443
444 > #qr--modalEditor {
445 max-height: 50vh;
446 > #qr--main,
447 > #qr--resizeHandle,
448 > #qr--qrOptions > h3,
449 > #qr--qrOptions > #qr--modal-executeButtons,
450 > #qr--qrOptions > #qr--modal-executeProgress
451 {
452 display: none;
453 }
454 #qr--qrOptions {
455 width: auto;
456 }
457 #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
458 display: flex;
459 }
460 #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
461 display: none;
462 }
463 #qr--modal-debugState {
464 padding-top: 0;
465 }
466 }
467 }
468 }
469 &:has(.qr--isExecuting) {
470 .popup-controls {
471 display: none;
472 }
473
474 .qr--highlight {
475 position: absolute;
476 z-index: 50000;
477 pointer-events: none;
478 background-color: rgb(47 150 180 / 0.5);
479 &.qr--unresolved {
480 background-color: rgb(255 255 0 / 0.5);
481 }
482 }
483 .qr--highlight-secondary {
484 position: absolute;
485 z-index: 50000;
486 pointer-events: none;
487 border: 3px solid red;
488 }
489 }
490
309 .popup-content {491 .popup-content {
310 display: flex;492 display: flex;
311 flex-direction: column;493 flex-direction: column;
@@ -317,140 +499,262 @@
317 gap: 1em;499 gap: 1em;
318 overflow: hidden;500 overflow: hidden;
319501
320 >#qr--main {502 &.qr--isExecuting {
321 flex: 1 1 auto;503 #qr--main > h3:first-child,
322 display: flex;504 #qr--main > .qr--labels,
323 flex-direction: column;505 #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
324 overflow: hidden;506 #qr--qrOptions > h3:first-child,
325507 #qr--qrOptions > #qr--ctxEditor,
326 >.qr--labels {508 #qr--qrOptions > .qr--ctxEditorActions,
327 flex: 0 0 auto;509 #qr--qrOptions > .qr--ctxEditorActions + h3,
328 display: flex;510 #qr--qrOptions > .qr--ctxEditorActions + h3 + div
329 flex-direction: row;511 {
330 gap: 0.5em;512 display: none;
331513 }
332 >label {514 #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
333 flex: 1 1 1px;515 visibility: hidden;
334 display: flex;516 }
335 flex-direction: column;517 #qr--modal-debugButtons {
336518 display: flex;
337 >.qr--labelText {519 .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
338 flex: 1 1 auto;520 cursor: not-allowed;
521 opacity: 0.5;
522 pointer-events: none;
523 transition: 200ms;
524 border-color: transparent;
525 }
526 }
527 &.qr--isPaused #qr--modal-debugButtons {
528 .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
529 cursor: pointer;
530 opacity: 1;
531 pointer-events: all;
532 &#qr--modal-resume {
533 animation-name: qr--debugPulse;
534 animation-duration: 1500ms;
535 animation-timing-function: ease-in-out;
536 animation-delay: 0s;
537 animation-iteration-count: infinite;
339 }538 }
340539 &#qr--modal-resume {
341 >.qr--labelHint {540 border-color: rgb(81, 163, 81);
342 flex: 1 1 auto;
343 }541 }
344542 &#qr--modal-step {
345 >input {543 border-color: var(--SmartThemeQuoteColor);
346 flex: 0 0 auto;544 }
545 &#qr--modal-stepInto {
546 border-color: var(--SmartThemeQuoteColor);
547 }
548 &#qr--modal-stepOut {
549 border-color: var(--SmartThemeQuoteColor);
347 }550 }
348 }551 }
349 }552 }
350553 #qr--resizeHandle {
351 >.qr--modal-messageContainer {554 width: 6px;
352 flex: 1 1 auto;555 background-color: var(--SmartThemeBorderColor);
353 display: flex;556 border: 2px solid var(--SmartThemeBlurTintColor);
354 flex-direction: column;557 transition: border-color 200ms, background-color 200ms;
355 overflow: hidden;558 cursor: w-resize;
356559 &:hover {
357 >.qr--modal-editorSettings {560 background-color: var(--SmartThemeQuoteColor);
358 display: flex;561 border-color: var(--SmartThemeQuoteColor);
359 flex-direction: row;562 }
360 gap: 1em;563 }
361 color: var(--grey70);564 #qr--qrOptions {
362 font-size: smaller;565 width: var(--width, auto);
363 align-items: baseline;566 }
364567 }
365 >.checkbox_label {568
366 white-space: nowrap;569 > #qr--main {
367570 flex: 1 1 auto;
368 >input {571 display: flex;
369 font-size: inherit;572 flex-direction: column;
370 }573 overflow: hidden;
574 > .qr--labels {
575 flex: 0 0 auto;
576 display: flex;
577 flex-direction: row;
578 gap: 0.5em;
579 padding: 1px;
580 > label, > .label {
581 flex: 1 1 1px;
582 display: flex;
583 flex-direction: column;
584 position: relative;
585 &.qr--fit {
586 flex: 0 0 auto;
587 justify-content: center;
371 }588 }
372 }589 .qr--inputGroup {
373590 display: flex;
374 >#qr--modal-messageHolder {591 align-items: baseline;
375 flex: 1 1 auto;592 gap: 0.5em;
376 display: grid;593 input {
377 text-align: left;594 flex: 1 1 auto;
378 overflow: hidden;
379
380 &.qr--noSyntax {
381 >#qr--modal-messageSyntax {
382 display: none;
383 }595 }
384596 }
385 >#qr--modal-message {597 .qr--labelText {
386 background-color: var(--ac-style-color-background);598 flex: 1 1 auto;
387 color: var(--ac-style-color-text);599 }
388600 .qr--labelHint {
389 &::selection {601 flex: 1 1 auto;
390 color: unset;602 }
391 background-color: rgba(108 171 251 / 0.25);603 input {
392604 flex: 0 0 auto;
393 @supports (color: rgb(from white r g b / 0.25)) {605 }
394 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);606 .qr--modal-switcherList {
607 background-color: var(--stcdx--bgColor);
608 border: 1px solid var(--SmartThemeBorderColor);
609 backdrop-filter: blur(var(--SmartThemeBlurStrength));
610 border-radius: 10px;
611 font-size: smaller;
612 position: absolute;
613 top: 100%;
614 left: 0;
615 right: 0;
616 overflow: auto;
617 margin: 0;
618 padding: 0.5em;
619 max-height: 50vh;
620 list-style: none;
621 z-index: 40000;
622 max-width: 100%;
623 .qr--modal-switcherItem {
624 display: flex;
625 gap: 1em;
626 text-align: left;
627 opacity: 0.75;
628 transition: 200ms;
629 cursor: pointer;
630 &:hover {
631 opacity: 1;
632 }
633 &.qr--current {
634 opacity: 1;
635 .qr--label, .qr--id {
636 font-weight: bold;
395 }637 }
396 }638 }
397 }639 }
398 }640 .qr--label {
399641 white-space: nowrap;
400 >#qr--modal-messageSyntax {642 .menu_button {
401 grid-column: 1;643 display: inline-block;
402 grid-row: 1;644 height: min-content;
403 padding: 0;645 width: min-content;
404 margin: 0;646 margin: 0 0.5em 0 0;
405 border: none;647 }
406 overflow: hidden;
407 min-width: 100%;
408 width: 0;
409
410 >#qr--modal-messageSyntaxInner {
411 height: 100%;
412 }648 }
413 }649 .qr--id {
414650 opacity: 0.5;
415 >#qr--modal-message {651 &:before { content: "["; }
416 background-color: transparent;652 &:after { content: "]"; }
417 color: transparent;
418 grid-column: 1;
419 grid-row: 1;
420 caret-color: var(--ac-style-color-text);
421 overflow: auto;
422
423 &::-webkit-scrollbar,
424 &::-webkit-scrollbar-thumb {
425 visibility: hidden;
426 cursor: default;
427 }653 }
428654 .qr--message {
429 &::selection {655 height: 1lh;
430 color: transparent;656 overflow: hidden;
431 background-color: rgba(108 171 251 / 0.25);657 text-overflow: ellipsis;
432658 white-space: nowrap;
433 @supports (color: rgb(from white r g b / 0.25)) {659 opacity: 0.5;
434 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
435 }
436 }660 }
437 }661 }
438662 }
439 #qr--modal-message,663 }
440 #qr--modal-messageSyntaxInner {664 > .qr--modal-messageContainer {
441 font-family: var(--monoFontFamily);665 flex: 1 1 auto;
442 padding: 0.75em;666 display: flex;
443 margin: 0;667 flex-direction: column;
444 border: none;668 overflow: hidden;
445 resize: none;669 > .qr--modal-editorSettings {
446 line-height: 1.2;670 display: flex;
447 border: 1px solid var(--SmartThemeBorderColor);671 flex-wrap: wrap;
448 border-radius: 5px;672 flex-direction: row;
449 }673 column-gap: 1em;
450 }674 color: var(--grey70);
451 }675 font-size: smaller;
676 align-items: baseline;
677 > .checkbox_label {
678 white-space: nowrap;
679 > input {
680 font-size: inherit;
681 }
682 }
683 }
684 > #qr--modal-messageHolder {
685 flex: 1 1 auto;
686 display: grid;
687 text-align: left;
688 overflow: hidden;
689 &.qr--noSyntax {
690 > #qr--modal-messageSyntax {
691 display: none;
692 }
693 > #qr--modal-message {
694 background-color: var(--ac-style-color-background);
695 color: var(--ac-style-color-text);
696 &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
697 visibility: visible;
698 cursor: unset;
699 }
700 &::selection {
701 color: unset;
702 background-color: rgba(108 171 251 / 0.25);
703 @supports (color: rgb(from white r g b / 0.25)) {
704 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
705 }
706 }
707 }
708 }
709 > #qr--modal-messageSyntax {
710 grid-column: 1;
711 grid-row: 1;
712 padding: 0;
713 margin: 0;
714 border: none;
715 overflow: hidden;
716 min-width: 100%;
717 width: 0;
718 > #qr--modal-messageSyntaxInner {
719 height: 100%;
720 }
721 }
722 > #qr--modal-message {
723 background-color: transparent;
724 color: transparent;
725 grid-column: 1;
726 grid-row: 1;
727 caret-color: var(--ac-style-color-text);
728 overflow: auto;
729 &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
730 visibility: hidden;
731 cursor: default;
732 }
733 &::selection {
734 color: transparent;
735 background-color: rgba(108 171 251 / 0.25);
736 @supports (color: rgb(from white r g b / 0.25)) {
737 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
738 }
739 }
740 }
741 #qr--modal-message, #qr--modal-messageSyntaxInner {
742 font-family: var(--monoFontFamily);
743 padding: 0.75em;
744 margin: 0;
745 resize: none;
746 line-height: 1.2;
747 border: 1px solid var(--SmartThemeBorderColor);
748 border-radius: 5px;
749 position: relative;
750 }
751 }
752 }
753 }
754 #qr--modal-icon {
755 height: 100%;
756 aspect-ratio: 1 / 1;
452 }757 }
453
454 #qr--modal-executeButtons {758 #qr--modal-executeButtons {
455 display: flex;759 display: flex;
456 gap: 1em;760 gap: 1em;
@@ -510,6 +814,47 @@
510 border-color: rgb(215, 136, 114);814 border-color: rgb(215, 136, 114);
511 }815 }
512 }816 }
817 #qr--modal-debugButtons {
818 display: none;
819 gap: 1em;
820 .qr--modal-debugButton {
821 aspect-ratio: 1.25 / 1;
822 width: 2.25em;
823 position: relative;
824 &:not(.fa-solid) {
825 border-width: 1px;
826 border-style: solid;
827 &:after {
828 content: '';
829 position: absolute;
830 inset: 3px;
831 background-color: var(--SmartThemeBodyColor);
832 mask-size: contain;
833 mask-position: center;
834 mask-repeat: no-repeat;
835 }
836 }
837 &#qr--modal-resume:after {
838 mask-image: url('/img/step-resume.svg');
839 }
840 &#qr--modal-step:after {
841 mask-image: url('/img/step-over.svg');
842 }
843 &#qr--modal-stepInto:after {
844 mask-image: url('/img/step-into.svg');
845 }
846 &#qr--modal-stepOut:after {
847 mask-image: url('/img/step-out.svg');
848 }
849 &#qr--modal-maximize {
850 display: none;
851 }
852 }
853 }
854
855 #qr--modal-send_textarea {
856 flex: 0 0 auto;
857 }
513858
514 #qr--modal-executeProgress {859 #qr--modal-executeProgress {
515 --prog: 0;860 --prog: 0;
@@ -518,6 +863,7 @@
518 --progSuccessColor: rgb(81, 163, 81);863 --progSuccessColor: rgb(81, 163, 81);
519 --progErrorColor: rgb(189, 54, 47);864 --progErrorColor: rgb(189, 54, 47);
520 --progAbortedColor: rgb(215, 136, 114);865 --progAbortedColor: rgb(215, 136, 114);
866 flex: 0 0 auto;
521 height: 0.5em;867 height: 0.5em;
522 background-color: var(--black50a);868 background-color: var(--black50a);
523 position: relative;869 position: relative;
@@ -588,6 +934,135 @@
588 overflow: auto;934 overflow: auto;
589 min-width: 100%;935 min-width: 100%;
590 width: 0;936 width: 0;
937 white-space: pre-wrap;
938 }
939 #qr--modal-debugState {
940 display: none;
941 &.qr--active {
942 display: block;
943 }
944 text-align: left;
945 font-size: smaller;
946 font-family: var(--monoFontFamily);
947 // background-color: rgb(146, 190, 252);
948 color: white;
949 padding: 0.5em 0;
950 overflow: auto;
951 min-width: 100%;
952 width: 0;
953 white-space: pre-wrap;
954
955 .qr--scope {
956 display: grid;
957 grid-template-columns: 0fr 1fr 1fr;
958 column-gap: 0em;
959 .qr--title {
960 grid-column: 1 / 4;
961 font-weight: bold;
962 font-family: var(--mainFontFamily);
963 background-color: var(--black50a);
964 padding: 0.25em;
965 margin-top: 0.5em;
966 }
967 .qr--var, .qr--macro, .qr--pipe {
968 display: contents;
969 &:nth-child(2n + 1) {
970 .qr--key, .qr--val {
971 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
972 }
973 .qr--val {
974 &:nth-child(2n) {
975 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
976 }
977 &:hover {
978 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
979 }
980 }
981 }
982 &:nth-child(2n) {
983 .qr--val {
984 &:nth-child(2n) {
985 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
986 }
987 &:hover {
988 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
989 }
990 }
991 }
992 &.qr--isHidden {
993 .qr--key, .qr--val {
994 opacity: 0.5;
995 }
996 }
997 .qr--val {
998 grid-column: 2 / 4;
999 &.qr--singleCol {
1000 grid-column: unset;
1001 }
1002 &.qr--simple {
1003 &:before, &:after {
1004 content: '"';
1005 color: var(--SmartThemeQuoteColor);
1006 }
1007 }
1008 &.qr--unresolved {
1009 &:after {
1010 content: '-UNRESOLVED-';
1011 font-style: italic;
1012 color: var(--SmartThemeQuoteColor);
1013 }
1014 }
1015 }
1016 }
1017 .qr--key {
1018 margin-left: 0.5em;
1019 padding-right: 1em;
1020 &:after { content: ": "; }
1021 }
1022 .qr--pipe, .qr--macro {
1023 > .qr--key {
1024 &:before { content: "{{"; }
1025 &:after { content: "}}: "; }
1026 }
1027 }
1028 .qr--scope {
1029 display: contents;
1030 .qr--pipe {
1031 .qr--key, .qr--val {
1032 opacity: 0.5;
1033 }
1034 }
1035 }
1036 }
1037
1038 .qr--stack {
1039 display: grid;
1040 grid-template-columns: 1fr 0fr;
1041 .qr--title {
1042 grid-column: 1 / 3;
1043 font-weight: bold;
1044 font-family: var(--mainFontFamily);
1045 background-color: var(--black50a);
1046 padding: 0.25em;
1047 margin-top: 1em;
1048 }
1049 .qr--item {
1050 display: contents;
1051 &:nth-child(2n + 1) {
1052 .qr--name, .qr--source {
1053 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
1054 }
1055 }
1056 .qr--name {
1057 margin-left: 0.5em;
1058 }
1059 .qr--source {
1060 opacity: 0.5;
1061 text-align: right;
1062 white-space: nowrap;
1063 }
1064 }
1065 }
591 }1066 }
592 }1067 }
593 }1068 }
@@ -605,10 +1080,75 @@
605 }1080 }
606}1081}
6071082
1083@keyframes qr--debugPulse {
1084 0%,
1085 100% {
1086 border-color: rgb(81, 163, 81);
1087 }
1088
1089 50% {
1090 border-color: rgb(146, 190, 252);
1091 }
1092}
1093
608.popup.qr--hide {1094.popup.qr--hide {
609 opacity: 0 !important;1095 opacity: 0 !important;
1096 &::backdrop {
1097 opacity: 0 !important;
1098 }
610}1099}
6111100
612.popup.qr--hide::backdrop {1101.popup.qr--hide::backdrop {
613 opacity: 0 !important;1102 opacity: 0 !important;
614}1103}
1104
1105
1106
1107.popup:has(.qr--transferModal) {
1108 .popup-button-ok {
1109 &:after {
1110 content: 'Transfer';
1111 height: 0;
1112 overflow: hidden;
1113 font-weight: bold;
1114 }
1115 display: flex;
1116 align-items: center;
1117 flex-direction: column;
1118 white-space: pre;
1119 font-weight: normal;
1120 box-shadow: 0 0 0;
1121 transition: 200ms;
1122 }
1123 .qr--copy {
1124 &:after {
1125 content: 'Copy';
1126 height: 0;
1127 overflow: hidden;
1128 font-weight: bold;
1129 }
1130 display: flex;
1131 align-items: center;
1132 flex-direction: column;
1133 white-space: pre;
1134 font-weight: normal;
1135 box-shadow: 0 0 0;
1136 transition: 200ms;
1137 }
1138 &:has(.qr--transferSelect:focus) {
1139 .popup-button-ok {
1140 font-weight: bold;
1141 box-shadow: 0 0 10px;
1142 }
1143 &.qr--isCopy {
1144 .popup-button-ok {
1145 font-weight: normal;
1146 box-shadow: 0 0 0;
1147 }
1148 .qr--copy {
1149 font-weight: bold;
1150 box-shadow: 0 0 10px;
1151 }
1152 }
1153 }
1154}
public/scripts/extensions/regex/editor.html+15 -22
@@ -54,12 +54,7 @@
54 <small data-i18n="Replace With">Replace With</small>54 <small data-i18n="Replace With">Replace With</small>
55 </label>55 </label>
56 <div>56 <div>
57 <textarea57 <textarea class="regex_replace_string text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_replace_string_placeholder" placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups." rows="2"></textarea>
58 class="regex_replace_string text_pole wide100p textarea_compact"
59 data-i18n="[placeholder]ext_regex_replace_string_placeholder"
60 placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
61 rows="2"
62 ></textarea>
63 </div>58 </div>
64 </div>59 </div>
65 <div class="flex1">60 <div class="flex1">
@@ -67,11 +62,7 @@
67 <small data-i18n="Trim Out">Trim Out</small>62 <small data-i18n="Trim Out">Trim Out</small>
68 </label>63 </label>
69 <div>64 <div>
70 <textarea65 <textarea class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder" placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter." rows="3"></textarea>
71 class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder"
72 placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter."
73 rows="3"
74 ></textarea>
75 </div>66 </div>
76 </div>67 </div>
77 </div>68 </div>
@@ -126,17 +117,6 @@
126 <input type="checkbox" name="disabled" />117 <input type="checkbox" name="disabled" />
127 <span data-i18n="Disabled">Disabled</span>118 <span data-i18n="Disabled">Disabled</span>
128 </label>119 </label>
129 <label class="checkbox flex-container" title="Chat history won't change, only the message rendered in the UI.">
130 <input type="checkbox" name="only_format_display" />
131 <span data-i18n="Only Format Display">Only Format Display</span>
132 </label>
133 <label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_prompt_desc" title="Chat history won't change, only the prompt as the request is sent (on generation).">
134 <input type="checkbox" name="only_format_prompt"/>
135 <span>
136 <span data-i18n="Only Format Prompt (?)">Only Format Prompt</span>
137 <span class="fa-solid fa-circle-question note-link-span"></span>
138 </span>
139 </label>
140 <label class="checkbox flex-container">120 <label class="checkbox flex-container">
141 <input type="checkbox" name="run_on_edit" />121 <input type="checkbox" name="run_on_edit" />
142 <span data-i18n="Run On Edit">Run On Edit</span>122 <span data-i18n="Run On Edit">Run On Edit</span>
@@ -148,6 +128,19 @@
148 <span class="fa-solid fa-circle-question note-link-span"></span>128 <span class="fa-solid fa-circle-question note-link-span"></span>
149 </span>129 </span>
150 </label>130 </label>
131 <span>
132 <small data-i18n="ext_regex_other_options" data-i18n="Ephemerality">Ephemerality</small>
133 <span class="fa-solid fa-circle-question note-link-span" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
134 </span>
135 <label class="checkbox flex-container" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
136 <input type="checkbox" name="only_format_display" />
137 <span data-i18n="Only Format Display">Alter Chat Display</span>
138 </label>
139 <label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_prompt_desc" title="Chat history file contents won't change, but regex will be applied to the outgoing prompt before it is sent to the LLM.">
140 <input type="checkbox" name="only_format_prompt" />
141 <span data-i18n="Only Format Prompt (?)">Alter Outgoing Prompt</span>
142 </label>
143
151 </div>144 </div>
152 </div>145 </div>
153 </div>146 </div>
public/scripts/extensions/shared.js+89 -2
@@ -1,5 +1,5 @@
1import { getRequestHeaders } from '../../script.js';1import { getRequestHeaders } from '../../script.js';
2import { extension_settings } from '../extensions.js';2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
3import { oai_settings } from '../openai.js';3import { oai_settings } from '../openai.js';
4import { SECRET_KEYS, secret_state } from '../secrets.js';4import { SECRET_KEYS, secret_state } from '../secrets.js';
5import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';5import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@@ -136,8 +136,12 @@ function throwIfInvalidModel(useReverseProxy) {
136 throw new Error('Anthropic (Claude) API key is not set.');136 throw new Error('Anthropic (Claude) API key is not set.');
137 }137 }
138138
139 if (extension_settings.caption.multimodal_api === 'zerooneai' && !secret_state[SECRET_KEYS.ZEROONEAI]) {
140 throw new Error('01.AI API key is not set.');
141 }
142
139 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {143 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
140 throw new Error('MakerSuite API key is not set.');144 throw new Error('Google AI Studio API key is not set.');
141 }145 }
142146
143 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {147 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
@@ -172,3 +176,86 @@ function throwIfInvalidModel(useReverseProxy) {
172 throw new Error('Custom API URL is not set.');176 throw new Error('Custom API URL is not set.');
173 }177 }
174}178}
179
180/**
181 * Check if the WebLLM extension is installed and supported.
182 * @returns {boolean} Whether the extension is installed and supported
183 */
184export function isWebLlmSupported() {
185 if (!('gpu' in navigator)) {
186 const warningKey = 'webllm_browser_warning_shown';
187 if (!sessionStorage.getItem(warningKey)) {
188 toastr.error('Your browser does not support the WebGPU API. Please use a different browser.', 'WebLLM', {
189 preventDuplicates: true,
190 timeOut: 0,
191 extendedTimeOut: 0,
192 });
193 sessionStorage.setItem(warningKey, '1');
194 }
195 return false;
196 }
197
198 if (!('llm' in SillyTavern)) {
199 const warningKey = 'webllm_extension_warning_shown';
200 if (!sessionStorage.getItem(warningKey)) {
201 toastr.error('WebLLM extension is not installed. Click here to install it.', 'WebLLM', {
202 timeOut: 0,
203 extendedTimeOut: 0,
204 preventDuplicates: true,
205 onclick: () => openThirdPartyExtensionMenu('https://github.com/SillyTavern/Extension-WebLLM'),
206 });
207 sessionStorage.setItem(warningKey, '1');
208 }
209 return false;
210 }
211
212 return true;
213}
214
215/**
216 * Generates text in response to a chat prompt using WebLLM.
217 * @param {any[]} messages Messages to use for generating
218 * @param {object} params Additional parameters
219 * @returns {Promise<string>} Generated response
220 */
221export async function generateWebLlmChatPrompt(messages, params = {}) {
222 if (!isWebLlmSupported()) {
223 throw new Error('WebLLM extension is not installed.');
224 }
225
226 console.debug('WebLLM chat completion request:', messages, params);
227 const engine = SillyTavern.llm;
228 const response = await engine.generateChatPrompt(messages, params);
229 console.debug('WebLLM chat completion response:', response);
230 return response;
231}
232
233/**
234 * Counts the number of tokens in the provided text using WebLLM's default model.
235 * @param {string} text Text to count tokens in
236 * @returns {Promise<number>} Number of tokens in the text
237 */
238export async function countWebLlmTokens(text) {
239 if (!isWebLlmSupported()) {
240 throw new Error('WebLLM extension is not installed.');
241 }
242
243 const engine = SillyTavern.llm;
244 const response = await engine.countTokens(text);
245 return response;
246}
247
248/**
249 * Gets the size of the context in the WebLLM's default model.
250 * @returns {Promise<number>} Size of the context in the WebLLM model
251 */
252export async function getWebLlmContextSize() {
253 if (!isWebLlmSupported()) {
254 throw new Error('WebLLM extension is not installed.');
255 }
256
257 const engine = SillyTavern.llm;
258 await engine.loadModel();
259 const model = await engine.getCurrentModelInfo();
260 return model?.context_size;
261}
public/scripts/extensions/stable-diffusion/button.html+6 -2
@@ -1,4 +1,8 @@
1<div id="sd_gen" class="list-group-item flex-container flexGap5">1<div id="sd_gen" class="list-group-item flex-container flexGap5">
2 <div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion" /></div>2 <div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion"></div>
3 Generate Image3 <span>Generate Image</span>
4</div>
5<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
6 <div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task" data-i18n="[title]Abort current image generation task"></div>
7 <span>Stop Image Generation</span>
4</div>8</div>
public/scripts/extensions/stable-diffusion/index.js+297 -68
@@ -30,13 +30,14 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
30import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';30import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
31import { debounce_timeout } from '../../constants.js';31import { debounce_timeout } from '../../constants.js';
32import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';32import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
33import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';33import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
34export { MODULE_NAME };34export { MODULE_NAME };
3535
36const MODULE_NAME = 'sd';36const MODULE_NAME = 'sd';
37const UPDATE_INTERVAL = 1000;37const UPDATE_INTERVAL = 1000;
38// This is a 1x1 transparent PNG38// This is a 1x1 transparent PNG
39const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';39const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
40const CUSTOM_STOP_EVENT = 'sd_stop_generation';
4041
41const sources = {42const sources = {
42 extras: 'extras',43 extras: 'extras',
@@ -50,6 +51,8 @@ const sources = {
50 drawthings: 'drawthings',51 drawthings: 'drawthings',
51 pollinations: 'pollinations',52 pollinations: 'pollinations',
52 stability: 'stability',53 stability: 'stability',
54 blockentropy: 'blockentropy',
55 huggingface: 'huggingface',
53};56};
5457
55const initiators = {58const initiators = {
@@ -57,6 +60,7 @@ const initiators = {
57 action: 'action',60 action: 'action',
58 interactive: 'interactive',61 interactive: 'interactive',
59 wand: 'wand',62 wand: 'wand',
63 swipe: 'swipe',
60};64};
6165
62const generationMode = {66const generationMode = {
@@ -451,6 +455,7 @@ async function loadSettings() {
451 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);455 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
452 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);456 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
453 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);457 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
458 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
454459
455 for (const style of extension_settings.sd.styles) {460 for (const style of extension_settings.sd.styles) {
456 const option = document.createElement('option');461 const option = document.createElement('option');
@@ -718,29 +723,29 @@ function onChatChanged() {
718 adjustElementScrollHeight();723 adjustElementScrollHeight();
719}724}
720725
721function adjustElementScrollHeight() {726async function adjustElementScrollHeight() {
722 if (!$('.sd_settings').is(':visible')) {727 if (!$('.sd_settings').is(':visible')) {
723 return;728 return;
724 }729 }
725730
726 resetScrollHeight($('#sd_prompt_prefix'));731 await resetScrollHeight($('#sd_prompt_prefix'));
727 resetScrollHeight($('#sd_negative_prompt'));732 await resetScrollHeight($('#sd_negative_prompt'));
728 resetScrollHeight($('#sd_character_prompt'));733 await resetScrollHeight($('#sd_character_prompt'));
729 resetScrollHeight($('#sd_character_negative_prompt'));734 await resetScrollHeight($('#sd_character_negative_prompt'));
730}735}
731736
732function onCharacterPromptInput() {737async function onCharacterPromptInput() {
733 const key = getCharaFilename(this_chid);738 const key = getCharaFilename(this_chid);
734 extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val();739 extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val();
735 resetScrollHeight($(this));740 await resetScrollHeight($(this));
736 saveSettingsDebounced();741 saveSettingsDebounced();
737 writePromptFieldsDebounced(this_chid);742 writePromptFieldsDebounced(this_chid);
738}743}
739744
740function onCharacterNegativePromptInput() {745async function onCharacterNegativePromptInput() {
741 const key = getCharaFilename(this_chid);746 const key = getCharaFilename(this_chid);
742 extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val();747 extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val();
743 resetScrollHeight($(this));748 await resetScrollHeight($(this));
744 saveSettingsDebounced();749 saveSettingsDebounced();
745 writePromptFieldsDebounced(this_chid);750 writePromptFieldsDebounced(this_chid);
746}751}
@@ -849,15 +854,15 @@ function onStepsInput() {
849 saveSettingsDebounced();854 saveSettingsDebounced();
850}855}
851856
852function onPromptPrefixInput() {857async function onPromptPrefixInput() {
853 extension_settings.sd.prompt_prefix = $('#sd_prompt_prefix').val();858 extension_settings.sd.prompt_prefix = $('#sd_prompt_prefix').val();
854 resetScrollHeight($(this));859 await resetScrollHeight($(this));
855 saveSettingsDebounced();860 saveSettingsDebounced();
856}861}
857862
858function onNegativePromptInput() {863async function onNegativePromptInput() {
859 extension_settings.sd.negative_prompt = $('#sd_negative_prompt').val();864 extension_settings.sd.negative_prompt = $('#sd_negative_prompt').val();
860 resetScrollHeight($(this));865 await resetScrollHeight($(this));
861 saveSettingsDebounced();866 saveSettingsDebounced();
862}867}
863868
@@ -1088,6 +1093,11 @@ function onComfyUrlInput() {
1088 saveSettingsDebounced();1093 saveSettingsDebounced();
1089}1094}
10901095
1096function onHFModelInput() {
1097 extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();
1098 saveSettingsDebounced();
1099}
1100
1091function onComfyWorkflowChange() {1101function onComfyWorkflowChange() {
1092 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();1102 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();
1093 saveSettingsDebounced();1103 saveSettingsDebounced();
@@ -1095,7 +1105,18 @@ function onComfyWorkflowChange() {
10951105
1096async function onStabilityKeyClick() {1106async function onStabilityKeyClick() {
1097 const popupText = 'Stability AI API Key:';1107 const popupText = 'Stability AI API Key:';
1098 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT);1108 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {
1109 customButtons: [{
1110 text: 'Remove Key',
1111 appendAtEnd: true,
1112 result: POPUP_RESULT.NEGATIVE,
1113 action: async () => {
1114 await writeSecret(SECRET_KEYS.STABILITY, '');
1115 toastr.success('API Key removed');
1116 await loadSettingOptions();
1117 },
1118 }],
1119 });
10991120
1100 if (!key) {1121 if (!key) {
1101 return;1122 return;
@@ -1221,7 +1242,16 @@ async function onModelChange() {
1221 extension_settings.sd.model = $('#sd_model').find(':selected').val();1242 extension_settings.sd.model = $('#sd_model').find(':selected').val();
1222 saveSettingsDebounced();1243 saveSettingsDebounced();
12231244
1224 const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai, sources.pollinations, sources.stability];1245 const cloudSources = [
1246 sources.horde,
1247 sources.novel,
1248 sources.openai,
1249 sources.togetherai,
1250 sources.pollinations,
1251 sources.stability,
1252 sources.blockentropy,
1253 sources.huggingface,
1254 ];
12251255
1226 if (cloudSources.includes(extension_settings.sd.source)) {1256 if (cloudSources.includes(extension_settings.sd.source)) {
1227 return;1257 return;
@@ -1433,6 +1463,12 @@ async function loadSamplers() {
1433 case sources.stability:1463 case sources.stability:
1434 samplers = ['N/A'];1464 samplers = ['N/A'];
1435 break;1465 break;
1466 case sources.blockentropy:
1467 samplers = ['N/A'];
1468 break;
1469 case sources.huggingface:
1470 samplers = ['N/A'];
1471 break;
1436 }1472 }
14371473
1438 for (const sampler of samplers) {1474 for (const sampler of samplers) {
@@ -1619,6 +1655,12 @@ async function loadModels() {
1619 case sources.stability:1655 case sources.stability:
1620 models = await loadStabilityModels();1656 models = await loadStabilityModels();
1621 break;1657 break;
1658 case sources.blockentropy:
1659 models = await loadBlockEntropyModels();
1660 break;
1661 case sources.huggingface:
1662 models = [{ value: '', text: '<Enter Model ID above>' }];
1663 break;
1622 }1664 }
16231665
1624 for (const model of models) {1666 for (const model of models) {
@@ -1648,49 +1690,13 @@ async function loadStabilityModels() {
1648async function loadPollinationsModels() {1690async function loadPollinationsModels() {
1649 return [1691 return [
1650 {1692 {
1651 value: 'pixart',1693 value: 'flux',
1652 text: 'PixArt-αlpha',1694 text: 'FLUX.1 [schnell]',
1653 },
1654 {
1655 value: 'playground',
1656 text: 'Playground v2',
1657 },
1658 {
1659 value: 'dalle3xl',
1660 text: 'DALL•E 3 XL',
1661 },
1662 {
1663 value: 'formulaxl',
1664 text: 'FormulaXL',
1665 },
1666 {
1667 value: 'dreamshaper',
1668 text: 'DreamShaper',
1669 },
1670 {
1671 value: 'deliberate',
1672 text: 'Deliberate',
1673 },
1674 {
1675 value: 'dpo',
1676 text: 'SDXL-DPO',
1677 },
1678 {
1679 value: 'swizz8',
1680 text: 'Swizz8',
1681 },
1682 {
1683 value: 'juggernaut',
1684 text: 'Juggernaut',
1685 },1695 },
1686 {1696 {
1687 value: 'turbo',1697 value: 'turbo',
1688 text: 'SDXL Turbo',1698 text: 'SDXL Turbo',
1689 },1699 },
1690 {
1691 value: 'realvis',
1692 text: 'Realistic Vision',
1693 },
1694 ];1700 ];
1695}1701}
16961702
@@ -1713,6 +1719,26 @@ async function loadTogetherAIModels() {
1713 return [];1719 return [];
1714}1720}
17151721
1722async function loadBlockEntropyModels() {
1723 if (!secret_state[SECRET_KEYS.BLOCKENTROPY]) {
1724 console.debug('Block Entropy API key is not set.');
1725 return [];
1726 }
1727
1728 const result = await fetch('/api/sd/blockentropy/models', {
1729 method: 'POST',
1730 headers: getRequestHeaders(),
1731 });
1732 console.log(result);
1733 if (result.ok) {
1734 const data = await result.json();
1735 console.log(data);
1736 return data;
1737 }
1738
1739 return [];
1740}
1741
1716async function loadHordeModels() {1742async function loadHordeModels() {
1717 const result = await fetch('/api/horde/sd-models', {1743 const result = await fetch('/api/horde/sd-models', {
1718 method: 'POST',1744 method: 'POST',
@@ -1979,6 +2005,12 @@ async function loadSchedulers() {
1979 case sources.stability:2005 case sources.stability:
1980 schedulers = ['N/A'];2006 schedulers = ['N/A'];
1981 break;2007 break;
2008 case sources.blockentropy:
2009 schedulers = ['N/A'];
2010 break;
2011 case sources.huggingface:
2012 schedulers = ['N/A'];
2013 break;
1982 }2014 }
19832015
1984 for (const scheduler of schedulers) {2016 for (const scheduler of schedulers) {
@@ -2055,6 +2087,12 @@ async function loadVaes() {
2055 case sources.stability:2087 case sources.stability:
2056 vaes = ['N/A'];2088 vaes = ['N/A'];
2057 break;2089 break;
2090 case sources.blockentropy:
2091 vaes = ['N/A'];
2092 break;
2093 case sources.huggingface:
2094 vaes = ['N/A'];
2095 break;
2058 }2096 }
20592097
2060 for (const vae of vaes) {2098 for (const vae of vaes) {
@@ -2266,9 +2304,9 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2266 const quietPrompt = getQuietPrompt(generationType, trigger);2304 const quietPrompt = getQuietPrompt(generationType, trigger);
2267 const context = getContext();2305 const context = getContext();
22682306
2269 // if context.characterId is not null, then we get context.characters[context.characterId].avatar, else we get groupId and context.groups[groupId].id2307 const characterName = context.groupId
2270 // sadly, groups is not an array, but is a dict with keys being index numbers, so we have to filter it2308 ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
2271 const characterName = context.characterId ? context.characters[context.characterId].name : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString();2309 : context.characters[context.characterId]?.name;
22722310
2273 if (generationType == generationMode.BACKGROUND) {2311 if (generationType == generationMode.BACKGROUND) {
2274 const callbackOriginal = callback;2312 const callbackOriginal = callback;
@@ -2290,6 +2328,7 @@ async function generatePicture(initiator, args, trigger, message, callback) {
22902328
2291 const dimensions = setTypeSpecificDimensions(generationType);2329 const dimensions = setTypeSpecificDimensions(generationType);
2292 const abortController = new AbortController();2330 const abortController = new AbortController();
2331 const stopButton = document.getElementById('sd_stop_gen');
2293 let negativePromptPrefix = args?.negative || '';2332 let negativePromptPrefix = args?.negative || '';
2294 let imagePath = '';2333 let imagePath = '';
22952334
@@ -2300,9 +2339,8 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2300 const prompt = await getPrompt(generationType, message, trigger, quietPrompt, combineNegatives);2339 const prompt = await getPrompt(generationType, message, trigger, quietPrompt, combineNegatives);
2301 console.log('Processed image prompt:', prompt);2340 console.log('Processed image prompt:', prompt);
23022341
2303 eventSource.once(event_types.GENERATION_STOPPED, stopListener);2342 $(stopButton).show();
2304 context.deactivateSendButtons();2343 eventSource.once(CUSTOM_STOP_EVENT, stopListener);
2305 hideSwipeButtons();
23062344
2307 if (typeof args?._abortController?.addEventListener === 'function') {2345 if (typeof args?._abortController?.addEventListener === 'function') {
2308 args._abortController.addEventListener('abort', stopListener);2346 args._abortController.addEventListener('abort', stopListener);
@@ -2311,13 +2349,13 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2311 imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal);2349 imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal);
2312 } catch (err) {2350 } catch (err) {
2313 console.trace(err);2351 console.trace(err);
2314 throw new Error('SD prompt text generation failed.');2352 toastr.error('SD prompt text generation failed. Reason: ' + err, 'Image Generation');
2353 throw new Error('SD prompt text generation failed. Reason: ' + err);
2315 }2354 }
2316 finally {2355 finally {
2356 $(stopButton).hide();
2317 restoreOriginalDimensions(dimensions);2357 restoreOriginalDimensions(dimensions);
2318 eventSource.removeListener(event_types.GENERATION_STOPPED, stopListener);2358 eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
2319 context.activateSendButtons();
2320 showSwipeButtons();
2321 }2359 }
23222360
2323 return imagePath;2361 return imagePath;
@@ -2583,6 +2621,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2583 case sources.stability:2621 case sources.stability:
2584 result = await generateStabilityImage(prefixedPrompt, negativePrompt, signal);2622 result = await generateStabilityImage(prefixedPrompt, negativePrompt, signal);
2585 break;2623 break;
2624 case sources.blockentropy:
2625 result = await generateBlockEntropyImage(prefixedPrompt, negativePrompt, signal);
2626 break;
2627 case sources.huggingface:
2628 result = await generateHuggingFaceImage(prefixedPrompt, signal);
2629 break;
2586 }2630 }
25872631
2588 if (!result.data) {2632 if (!result.data) {
@@ -2638,6 +2682,40 @@ async function generateTogetherAIImage(prompt, negativePrompt, signal) {
2638 }2682 }
2639}2683}
26402684
2685async function generateBlockEntropyImage(prompt, negativePrompt, signal) {
2686 const result = await fetch('/api/sd/blockentropy/generate', {
2687 method: 'POST',
2688 headers: getRequestHeaders(),
2689 signal: signal,
2690 body: JSON.stringify({
2691 prompt: prompt,
2692 negative_prompt: negativePrompt,
2693 model: extension_settings.sd.model,
2694 steps: extension_settings.sd.steps,
2695 width: extension_settings.sd.width,
2696 height: extension_settings.sd.height,
2697 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
2698 }),
2699 });
2700
2701 if (result.ok) {
2702 const data = await result.json();
2703
2704 // Default format is 'jpg'
2705 let format = 'jpg';
2706
2707 // Check if a format is specified in the result
2708 if (data.format) {
2709 format = data.format.toLowerCase();
2710 }
2711
2712 return { format: format, data: data.images[0] };
2713 } else {
2714 const text = await result.text();
2715 throw new Error(text);
2716 }
2717}
2718
2641/**2719/**
2642 * Generates an image using the Pollinations API.2720 * Generates an image using the Pollinations API.
2643 * @param {string} prompt - The main instruction used to guide the image generation.2721 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3182,6 +3260,34 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3182 return { format: 'png', data: await promptResult.text() };3260 return { format: 'png', data: await promptResult.text() };
3183}3261}
31843262
3263
3264/**
3265 * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected).
3266 * @param {string} prompt - The main instruction used to guide the image generation.
3267 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3268 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3269 */
3270async function generateHuggingFaceImage(prompt, signal) {
3271 const result = await fetch('/api/sd/huggingface/generate', {
3272 method: 'POST',
3273 headers: getRequestHeaders(),
3274 signal: signal,
3275 body: JSON.stringify({
3276 model: extension_settings.sd.huggingface_model_id,
3277 prompt: prompt,
3278 }),
3279 });
3280
3281 if (result.ok) {
3282 const data = await result.json();
3283 return { format: 'jpg', data: data.image };
3284 } else {
3285 const text = await result.text();
3286 throw new Error(text);
3287 }
3288}
3289
3290
3185async function onComfyOpenWorkflowEditorClick() {3291async function onComfyOpenWorkflowEditorClick() {
3186 let workflow = await (await fetch('/api/sd/comfy/workflow', {3292 let workflow = await (await fetch('/api/sd/comfy/workflow', {
3187 method: 'POST',3293 method: 'POST',
@@ -3347,11 +3453,15 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
3347 generationType: generationType,3453 generationType: generationType,
3348 negative: additionalNegativePrefix,3454 negative: additionalNegativePrefix,
3349 inline_image: false,3455 inline_image: false,
3456 image_swipes: [image],
3350 },3457 },
3351 };3458 };
3352 context.chat.push(message);3459 context.chat.push(message);
3460 const messageId = context.chat.length - 1;
3461 await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
3353 context.addOneMessage(message);3462 context.addOneMessage(message);
3354 context.saveChat();3463 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
3464 await context.saveChat();
3355}3465}
33563466
3357/**3467/**
@@ -3395,7 +3505,7 @@ async function addSDGenButtons() {
3395 $(document).on('click touchend', function (e) {3505 $(document).on('click touchend', function (e) {
3396 const target = $(e.target);3506 const target = $(e.target);
3397 if (target.is(dropdown) || target.closest(dropdown).length) return;3507 if (target.is(dropdown) || target.closest(dropdown).length) return;
3398 if (target.is(button) && !dropdown.is(':visible') && $('#send_but').is(':visible')) {3508 if ((target.is(button) || target.closest(button).length) && !dropdown.is(':visible')) {
3399 e.preventDefault();3509 e.preventDefault();
34003510
3401 dropdown.fadeIn(animation_duration);3511 dropdown.fadeIn(animation_duration);
@@ -3425,6 +3535,10 @@ async function addSDGenButtons() {
3425 generatePicture(initiators.wand, {}, param);3535 generatePicture(initiators.wand, {}, param);
3426 }3536 }
3427 });3537 });
3538
3539 const stopGenButton = $('#sd_stop_gen');
3540 stopGenButton.hide();
3541 stopGenButton.on('click', () => eventSource.emit(CUSTOM_STOP_EVENT));
3428}3542}
34293543
3430function isValidState() {3544function isValidState() {
@@ -3451,6 +3565,10 @@ function isValidState() {
3451 return true;3565 return true;
3452 case sources.stability:3566 case sources.stability:
3453 return secret_state[SECRET_KEYS.STABILITY];3567 return secret_state[SECRET_KEYS.STABILITY];
3568 case sources.blockentropy:
3569 return secret_state[SECRET_KEYS.BLOCKENTROPY];
3570 case sources.huggingface:
3571 return secret_state[SECRET_KEYS.HUGGINGFACE];
3454 }3572 }
3455}3573}
34563574
@@ -3480,7 +3598,9 @@ async function sdMessageButton(e) {
3480 const $mes = $icon.closest('.mes');3598 const $mes = $icon.closest('.mes');
3481 const message_id = $mes.attr('mesid');3599 const message_id = $mes.attr('mesid');
3482 const message = context.chat[message_id];3600 const message = context.chat[message_id];
3483 const characterFileName = context.characterId ? context.characters[context.characterId].name : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString();3601 const characterFileName = context.groupId
3602 ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
3603 : context.characters[context.characterId]?.name;
3484 const messageText = message?.mes;3604 const messageText = message?.mes;
3485 const hasSavedImage = message?.extra?.image && message?.extra?.title;3605 const hasSavedImage = message?.extra?.image && message?.extra?.title;
3486 const hasSavedNegative = message?.extra?.negative;3606 const hasSavedNegative = message?.extra?.negative;
@@ -3524,10 +3644,23 @@ async function sdMessageButton(e) {
35243644
3525 function saveGeneratedImage(prompt, image, generationType, negative) {3645 function saveGeneratedImage(prompt, image, generationType, negative) {
3526 // Some message sources may not create the extra object3646 // Some message sources may not create the extra object
3527 if (typeof message.extra !== 'object') {3647 if (typeof message.extra !== 'object' || message.extra === null) {
3528 message.extra = {};3648 message.extra = {};
3529 }3649 }
35303650
3651 // Add image to the swipe list if it's not already there
3652 if (!Array.isArray(message.extra.image_swipes)) {
3653 message.extra.image_swipes = [];
3654 }
3655
3656 const swipes = message.extra.image_swipes;
3657
3658 if (message.extra.image && !swipes.includes(message.extra.image)) {
3659 swipes.push(message.extra.image);
3660 }
3661
3662 swipes.push(image);
3663
3531 // If already contains an image and it's not inline - leave it as is3664 // If already contains an image and it's not inline - leave it as is
3532 message.extra.inline_image = message.extra.image && !message.extra.inline_image ? false : true;3665 message.extra.inline_image = message.extra.image && !message.extra.inline_image ? false : true;
3533 message.extra.image = image;3666 message.extra.image = image;
@@ -3566,6 +3699,99 @@ async function writePromptFields(characterId) {
3566 await writeExtensionField(characterId, 'sd_character_prompt', promptObject);3699 await writeExtensionField(characterId, 'sd_character_prompt', promptObject);
3567}3700}
35683701
3702/**
3703 * Switches an image to the next or previous one in the swipe list.
3704 * @param {object} args Event arguments
3705 * @param {any} args.message Message object
3706 * @param {JQuery<HTMLElement>} args.element Message element
3707 * @param {string} args.direction Swipe direction
3708 * @returns {Promise<void>}
3709 */
3710async function onImageSwiped({ message, element, direction }) {
3711 const context = getContext();
3712 const animationClass = 'fa-fade';
3713 const messageImg = element.find('.mes_img');
3714
3715 // Current image is already animating
3716 if (messageImg.hasClass(animationClass)) {
3717 return;
3718 }
3719
3720 const swipes = message?.extra?.image_swipes;
3721
3722 if (!Array.isArray(swipes)) {
3723 console.warn('No image swipes found in the message');
3724 return;
3725 }
3726
3727 const currentIndex = swipes.indexOf(message.extra.image);
3728
3729 if (currentIndex === -1) {
3730 console.warn('Current image not found in the swipes');
3731 return;
3732 }
3733
3734 // Switch to previous image or wrap around if at the beginning
3735 if (direction === 'left') {
3736 const newIndex = currentIndex === 0 ? swipes.length - 1 : currentIndex - 1;
3737 message.extra.image = swipes[newIndex];
3738
3739 // Update the image in the message
3740 appendMediaToMessage(message, element, false);
3741 }
3742
3743 // Switch to next image or generate a new one if at the end
3744 if (direction === 'right') {
3745 const newIndex = currentIndex === swipes.length - 1 ? swipes.length : currentIndex + 1;
3746
3747 if (newIndex === swipes.length) {
3748 const abortController = new AbortController();
3749 const swipeControls = element.find('.mes_img_swipes');
3750 const stopButton = document.getElementById('sd_stop_gen');
3751 const stopListener = () => abortController.abort('Aborted by user');
3752 const generationType = message?.extra?.generationType ?? generationMode.FREE;
3753 const dimensions = setTypeSpecificDimensions(generationType);
3754 const originalSeed = extension_settings.sd.seed;
3755 extension_settings.sd.seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
3756 let imagePath = '';
3757
3758 try {
3759 $(stopButton).show();
3760 eventSource.once(CUSTOM_STOP_EVENT, stopListener);
3761 const callback = () => { };
3762 const hasNegative = message.extra.negative;
3763 const prompt = await refinePrompt(message.extra.title, false, false);
3764 const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, false, true) : '';
3765 const characterName = context.groupId
3766 ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
3767 : context.characters[context.characterId]?.name;
3768
3769 messageImg.addClass(animationClass);
3770 swipeControls.hide();
3771 imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiators.swipe, abortController.signal);
3772 } finally {
3773 $(stopButton).hide();
3774 messageImg.removeClass(animationClass);
3775 swipeControls.show();
3776 eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
3777 restoreOriginalDimensions(dimensions);
3778 extension_settings.sd.seed = originalSeed;
3779 }
3780
3781 if (!imagePath) {
3782 return;
3783 }
3784
3785 swipes.push(imagePath);
3786 }
3787
3788 message.extra.image = swipes[newIndex];
3789 appendMediaToMessage(message, element, false);
3790 }
3791
3792 await context.saveChat();
3793}
3794
3569jQuery(async () => {3795jQuery(async () => {
3570 await addSDGenButtons();3796 await addSDGenButtons();
35713797
@@ -3683,6 +3909,7 @@ jQuery(async () => {
3683 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);3909 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
3684 $('#sd_stability_key').on('click', onStabilityKeyClick);3910 $('#sd_stability_key').on('click', onStabilityKeyClick);
3685 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);3911 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
3912 $('#sd_huggingface_model_id').on('input', onHFModelInput);
36863913
3687 $('.sd_settings .inline-drawer-toggle').on('click', function () {3914 $('.sd_settings .inline-drawer-toggle').on('click', function () {
3688 initScrollHeight($('#sd_prompt_prefix'));3915 initScrollHeight($('#sd_prompt_prefix'));
@@ -3704,6 +3931,8 @@ jQuery(async () => {
3704 }3931 }
3705 });3932 });
37063933
3934 eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped);
3935
3707 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);3936 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
37083937
3709 await loadSettings();3938 await loadSettings();
public/scripts/extensions/stable-diffusion/settings.html+10 -2
@@ -29,7 +29,8 @@
29 </label>29 </label>
30 <label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">30 <label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">
31 <input id="sd_expand" type="checkbox" />31 <input id="sd_expand" type="checkbox" />
32 <span data-i18n="sd_expand_txt">Auto-enhance prompts</span>32 <span data-i18n="sd_expand_txt">Auto-extend prompts</span>
33 <span class="right_menu_button fa-solid fa-triangle-exclamation" data-i18n="[title]sd_expand_warning" title="May produce unexpected results. Manual prompt editing is recommended."></span>
33 </label>34 </label>
34 <label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">35 <label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
35 <input id="sd_snap" type="checkbox" />36 <input id="sd_snap" type="checkbox" />
@@ -37,9 +38,11 @@
37 </label>38 </label>
38 <label for="sd_source" data-i18n="Source">Source</label>39 <label for="sd_source" data-i18n="Source">Source</label>
39 <select id="sd_source">40 <select id="sd_source">
41 <option value="blockentropy">Block Entropy</option>
40 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
41 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
42 <option value="extras">Extras API (local / remote)</option>44 <option value="extras">Extras API (local / remote)</option>
45 <option value="huggingface">HuggingFace Inference API (serverless)</option>
43 <option value="novel">NovelAI Diffusion</option>46 <option value="novel">NovelAI Diffusion</option>
44 <option value="openai">OpenAI (DALL-E)</option>47 <option value="openai">OpenAI (DALL-E)</option>
45 <option value="pollinations">Pollinations</option>48 <option value="pollinations">Pollinations</option>
@@ -81,6 +84,11 @@
81 <!-- (Original Text)<b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine. -->84 <!-- (Original Text)<b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine. -->
82 <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_drawthings_auth_txt"> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i>85 <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_drawthings_auth_txt"> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i>
83 </div>86 </div>
87 <div data-sd-source="huggingface">
88 <i>Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.</i>
89 <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
90 <input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" />
91 </div>
84 <div data-sd-source="vlad">92 <div data-sd-source="vlad">
85 <label for="sd_vlad_url">SD.Next API URL</label>93 <label for="sd_vlad_url">SD.Next API URL</label>
86 <div class="flex-container flexnowrap">94 <div class="flex-container flexnowrap">
@@ -378,7 +386,7 @@
378 </label>386 </label>
379 </div>387 </div>
380388
381 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability" class="marginTop5">389 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,blockentropy" class="marginTop5">
382 <label for="sd_seed">390 <label for="sd_seed">
383 <span data-i18n="Seed">Seed</span>391 <span data-i18n="Seed">Seed</span>
384 <small data-i18n="(-1 for random)">(-1 for random)</small>392 <small data-i18n="(-1 for random)">(-1 for random)</small>
public/scripts/extensions/token-counter/index.js+4 -3
@@ -59,8 +59,8 @@ async function doTokenCounter() {
59 $('#tokenized_chunks_display').text('—');59 $('#tokenized_chunks_display').text('—');
60 }60 }
6161
62 resetScrollHeight($('#token_counter_textarea'));62 await resetScrollHeight($('#token_counter_textarea'));
63 resetScrollHeight($('#token_counter_ids'));63 await resetScrollHeight($('#token_counter_ids'));
64 }, debounce_timeout.relaxed);64 }, debounce_timeout.relaxed);
65 dialog.find('#token_counter_textarea').on('input', () => countDebounced());65 dialog.find('#token_counter_textarea').on('input', () => countDebounced());
6666
@@ -134,7 +134,8 @@ jQuery(() => {
134 </div>`;134 </div>`;
135 $('#token_counter_wand_container').append(buttonHtml);135 $('#token_counter_wand_container').append(buttonHtml);
136 $('#token_counter').on('click', doTokenCounter);136 $('#token_counter').on('click', doTokenCounter);
137 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count',137 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
138 name: 'count',
138 callback: async () => String(await doCount()),139 callback: async () => String(await doCount()),
139 returns: 'number of tokens',140 returns: 'number of tokens',
140 helpString: 'Counts the number of tokens in the current chat.',141 helpString: 'Counts the number of tokens in the current chat.',
public/scripts/extensions/translate/buttons.html+3 -3
@@ -1,8 +1,8 @@
1<div id="translate_chat" class="list-group-item flex-container flexGap5">1<div id="translate_chat" class="list-group-item flex-container flexGap5">
2 <div class="fa-solid fa-language extensionsMenuExtensionButton" /></div>2 <div class="fa-solid fa-language extensionsMenuExtensionButton"></div>
3 <span data-i18n="ext_translate_btn_chat">Translate Chat</span>3 <span data-i18n="ext_translate_btn_chat">Translate Chat</span>
4</div>4</div>
5<div id="translate_input_message" class="list-group-item flex-container flexGap5">5<div id="translate_input_message" class="list-group-item flex-container flexGap5">
6 <div class="fa-solid fa-keyboard extensionsMenuExtensionButton" /></div>6 <div class="fa-solid fa-keyboard extensionsMenuExtensionButton"></div>
7 <span data-i18n="ext_translate_btn_input">Translate Input</span>7 <span data-i18n="ext_translate_btn_input">Translate Input</span>
8</div>
8 \ No newline at end of file \ No newline at end of file
8</div>
public/scripts/extensions/translate/index.js+13 -2
@@ -10,7 +10,7 @@ import {
10 updateMessageBlock,10 updateMessageBlock,
11} from '../../../script.js';11} from '../../../script.js';
12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
13import { POPUP_TYPE, callGenericPopup } from '../../popup.js';13import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
14import { findSecret, secret_state, writeSecret } from '../../secrets.js';14import { findSecret, secret_state, writeSecret } from '../../secrets.js';
15import { SlashCommand } from '../../slash-commands/SlashCommand.js';15import { SlashCommand } from '../../slash-commands/SlashCommand.js';
16import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';16import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@@ -621,7 +621,18 @@ jQuery(async () => {
621 const secretKey = extension_settings.translate.provider + '_url';621 const secretKey = extension_settings.translate.provider + '_url';
622 const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';622 const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
623623
624 const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl);624 const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl,{
625 customButtons: [{
626 text: 'Remove URL',
627 appendAtEnd: true,
628 result: POPUP_RESULT.NEGATIVE,
629 action: async () => {
630 await writeSecret(secretKey, '');
631 toastr.success('API URL removed');
632 $('#translate_url_button').toggleClass('success', !!secret_state[secretKey]);
633 },
634 }],
635 });
625636
626 if (url == false || url == '') {637 if (url == false || url == '') {
627 return;638 return;
public/scripts/extensions/tts/azure.js+0 -0
@@ -1,5 +1,5 @@
1import { getRequestHeaders } from '../../../script.js';1import { getRequestHeaders } from '../../../script.js';
public/scripts/extensions/tts/index.js+0 -0
public/scripts/extensions/tts/openai-compatible.js+0 -0
public/scripts/extensions/tts/system.js+0 -0
public/scripts/extensions/vectors/index.js+0 -0
public/scripts/extensions/vectors/settings.html+0 -0
public/scripts/instruct-mode.js+0 -0
public/scripts/openai.js+0 -0
public/scripts/popup.js+0 -0
public/scripts/power-user.js+0 -0
public/scripts/samplerSelect.js+0 -0
public/scripts/secrets.js+0 -0
public/scripts/showdown-underscore.js+0 -0
public/scripts/slash-commands.js+0 -0
public/scripts/slash-commands/SlashCommand.js+0 -0
public/scripts/slash-commands/SlashCommandArgument.js+0 -0
public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js+0 -0
public/scripts/slash-commands/SlashCommandBreak.js+0 -0
public/scripts/slash-commands/SlashCommandBreakController.js+0 -0
public/scripts/slash-commands/SlashCommandBreakPoint.js+0 -0
public/scripts/slash-commands/SlashCommandClosure.js+0 -0
public/scripts/slash-commands/SlashCommandClosureExecutor.js+0 -0
public/scripts/slash-commands/SlashCommandClosureResult.js+0 -0
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+0 -0
public/scripts/slash-commands/SlashCommandDebugController.js+0 -0
public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js+0 -0
public/scripts/slash-commands/SlashCommandEnumValue.js+0 -0
public/scripts/slash-commands/SlashCommandExecutionError.js+0 -0
public/scripts/slash-commands/SlashCommandExecutor.js+0 -0
public/scripts/slash-commands/SlashCommandParser.js+0 -0
public/scripts/slash-commands/SlashCommandScope.js+0 -0
public/scripts/tags.js+0 -0
public/scripts/templates/installExtension.html+0 -0
public/scripts/templates/worldInfoKeywordHeaders.html+0 -0
public/scripts/textgen-models.js+0 -0
public/scripts/textgen-settings.js+0 -0
public/scripts/tokenizers.js+0 -0
public/scripts/utils.js+0 -0
public/scripts/variables.js+0 -0
public/scripts/world-info.js+0 -0
public/style.css+0 -0
server.js+0 -0
src/additional-headers.js+0 -0
src/constants.js+0 -0
src/endpoints/anthropic.js+0 -0
src/endpoints/backends/chat-completions.js+0 -0
src/endpoints/backends/text-completions.js+0 -0
src/endpoints/content-manager.js+0 -0
src/endpoints/google.js+0 -0
src/endpoints/images.js+0 -0
src/endpoints/openai.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/tokenizers.js+0 -0
src/endpoints/translate.js+0 -0
src/prompt-converters.js+0 -0
src/tokenizers/gemma.model+0 -0

Binary file

src/transformers.mjs+0 -0
src/users.js+0 -0
src/util.js+0 -0
src/vectors/makersuite-vectors.js+0 -0
Diff truncated