Merge branch 'staging' into enable-autoselect-inputs

321f0500e02756743bd8e54991d5ad83b068b625

Wolfsblvt <wolfsblvt@gmail.com>

149 files changed, +5785 -844Showing whitespace changes
.eslintrc.js+1 -0
@@ -55,6 +55,7 @@ module.exports = {
5555 isProbablyReaderable: 'readonly',
5656 ePub: 'readonly',
5757 diff_match_patch: 'readonly',
58+ SillyTavern: 'readonly',
5859 },
5960 },
6061 ],
.github/workflows/close-stale-issues.yml+11 -11
@@ -16,21 +16,21 @@ jobs:
1616 uses: actions/stale@v4
1717 with:
1818 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
1919 days-before-stale: 360183
2020 days-before-close: 57
2121 operations-per-run: 30
2222 remove-stale-when-updated: true
2323 enable-statistics: true
2424 stale-issue-message: >
2525 This issue has gone 36 months without an update. To keep the ticket open, please indicate that it is still relevant in a comment below.
2626 Otherwise it will be closed in 5 working7 days.
2727 stale-pr-message: >
2828 This PR is stale because it has been open 6 weeksmonths with no activity. Either remove the stale label or comment below with a short update,
2929 otherwise this PR will be closed in 57 days.
3030 close-issue-message: >
3131 This issue was automatically closed because it has been stalled for over 16 yearmonths with no activity.
3232 close-pr-message: >
3333 This pull request was automatically closed because it has been stalled for over 16 yearmonths with no activity.
3434 stale-issue-label: '⚰️ Stale'
3535 close-issue-label: '🕸️ Inactive'
3636 stale-pr-label: '⚰️ Stale'
@@ -44,8 +44,8 @@ jobs:
4444 uses: actions/stale@v4
4545 with:
4646 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
4747 days-before-stale: 57
4848 days-before-close: 37
4949 operations-per-run: 30
5050 remove-stale-when-updated: true
5151 stale-issue-message: >
@@ -65,7 +65,7 @@ jobs:
6565 with:
6666 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
6767 days-before-stale: 7
6868 days-before-close: 365183
6969 operations-per-run: 30
7070 remove-stale-when-updated: true
7171 stale-issue-message: Hey SillyTavern, - Don't forget to respond!
CONTRIBUTING.md+10 -1
@@ -29,4 +29,13 @@
2929 - Updating GitHub Actions.
3030 - Hotfixing a critical bug.
31314. Project maintainers will test and can change your code before merging.
32-5. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer.
32+5. Write at least somewhat meaningful PR descriptions. There's no "right" way to do it, but the following may help with outlining a general structure:
33+ - What is the reason for a change?
34+ - What did you do to achieve this?
35+ - How would a reviewer test the change?
36+6. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer.
37+
38+## Further reading
39+
40+1. [How to write UI extensions](https://docs.sillytavern.app/for-contributors/writing-extensions/)
41+2. [How to write server plugins](https://docs.sillytavern.app/for-contributors/server-plugins)
default/config.yaml+44 -5
@@ -4,8 +4,22 @@ dataRoot: ./data
44# -- SERVER CONFIGURATION --
55# Listen for incoming connections
66listen: false
7+# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
8+protocol:
9+ ipv4: true
10+ ipv6: false
11+# Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6
12+dnsPreferIPv6: false
13+# The hostname that autorun opens.
14+# - Use "auto" to let the server decide
15+# - Use options like 'localhost', 'st.example.com'
16+autorunHostname: "auto"
717# Server port
818port: 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.
22+autorunPortOverride: -1
923# -- SECURITY CONFIGURATION --
1024# Toggle whitelist mode
1125whitelistMode: true
@@ -13,6 +27,7 @@ whitelistMode: true
1327enableForwardedWhitelist: true
1428# Whitelist of allowed IP addresses
1529whitelist:
30+ - ::1
1631 - 127.0.0.1
1732# Toggle basic authentication for endpoints
1833basicAuthMode: false
@@ -26,6 +41,11 @@ enableCorsProxy: false
2641enableUserAccounts: false
2742# Enable discreet login mode: hides user list on the login screen
2843enableDiscreetLogin: 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
48+sessionTimeout: 86400
2949# Used to sign session cookies. Will be auto-generated if not set
3050cookieSecret: ''
3151# Disable CSRF protection - NOT RECOMMENDED
@@ -35,6 +55,9 @@ securityOverride: false
3555# -- ADVANCED CONFIGURATION --
3656# Open the browser automatically
3757autorun: true
58+# Avoids using 'localhost' for autorun in auto mode.
59+# use if you don't have 'localhost' in your hosts file
60+avoidLocalhost: false
3861# Disable thumbnail generation
3962disableThumbnails: false
4063# Thumbnail quality (0-100)
@@ -93,10 +116,26 @@ openai:
93116deepl:
94117 # Available options: default, more, less, prefer_more, prefer_less
95118 formality: default
119+# -- MISTRAL API CONFIGURATION --
120+mistral:
121+ # Enables prefilling of the reply with the last assistant message in the prompt
122+ # CAUTION: The prefix is echoed into the completion. You may want to use regex to trim it out.
123+ enablePrefix: false
124+# -- OLLAMA API CONFIGURATION --
125+ollama:
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+ # * 5m: Keep the model loaded for 5 minutes after the request. Accepts duration strings (e.g. 5h30m40s)
130+ keepAlive: -1
131+# -- ANTHROPIC CLAUDE API CONFIGURATION --
132+claude:
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
96140# -- SERVER PLUGIN CONFIGURATION --
97141enableServerPlugins: false
98-# User session timeout *in seconds* (defaults to 24 hours).
99-## Set to a positive number to expire session after a certain time of inactivity
100-## Set to 0 to expire session when the browser is closed
101-## Set to a negative number to disable session expiration
102-sessionTimeout: 86400
default/content/presets/openai/Default.json+4 -4
@@ -1,7 +1,7 @@
11{
22 "chat_completion_source": "openai",
33 "openai_model": "gpt-3.54-turbo",
44 "claude_model": "claude-instant3-v15-sonnet-20240620",
55 "windowai_model": "",
66 "openrouter_model": "OR_Website",
77 "openrouter_use_fallback": false,
@@ -9,7 +9,7 @@
99 "openrouter_group_models": false,
1010 "openrouter_sort_models": "alphabetically",
1111 "ai21_model": "j2-ultra",
1212 "mistralai_model": "mistral-mediumlarge-latest",
1313 "custom_model": "",
1414 "custom_url": "",
1515 "custom_include_body": "",
@@ -22,7 +22,7 @@
2222 "count_penalty": 0,
2323 "top_p": 1,
2424 "top_k": 0,
2525 "top_a": 10,
2626 "min_p": 0,
2727 "repetition_penalty": 1,
2828 "openai_max_context": 4095,
default/content/settings.json+3 -3
@@ -610,9 +610,9 @@
610610 }
611611 ]
612612 },
613- "wi_format": "[Details of the fictional world the RP is set in:\n{0}]\n",
613+ "wi_format": "{0}",
614614 "openai_model": "gpt-3.54-turbo",
615615 "claude_model": "claude-instant3-v15-sonnet-20240620",
616616 "ai21_model": "j2-ultra",
617617 "windowai_model": "",
618618 "openrouter_model": "OR_Website",
index.d.ts+5 -0
@@ -9,6 +9,11 @@ declare global {
99 };
1010 }
1111 }
12+
13+ /**
14+ * The root directory for user data.
15+ */
16+ var DATA_ROOT: string;
1217}
1318
1419declare module 'express-session' {
package-lock.json+29 -28
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.12.34",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.12.34",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
@@ -42,7 +42,7 @@
4242 "rate-limiter-flexible": "^5.0.0",
4343 "response-time": "^2.3.2",
4444 "sanitize-filename": "^1.6.3",
4545 "sillytavern-transformers": "^2.14.6",
4646 "simple-git": "^3.19.1",
4747 "tiktoken": "^1.0.15",
4848 "vectra": "^0.2.2",
@@ -58,7 +58,7 @@
5858 },
5959 "devDependencies": {
6060 "@types/jquery": "^3.5.29",
6161 "eslint": "^8.5557.0",
6262 "jquery": "^3.6.4"
6363 },
6464 "engines": {
@@ -166,9 +166,9 @@
166166 "license": "MIT"
167167 },
168168 "node_modules/@eslint/js": {
169169 "version": "8.5557.0",
170170 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.5557.0.tgz",
171171 "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLoYs+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/nvNMpJOaJATj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
172172 "dev": true,
173173 "license": "MIT",
174174 "engines": {
@@ -185,14 +185,15 @@
185185 }
186186 },
187187 "node_modules/@humanwhocodes/config-array": {
188188 "version": "0.11.1314",
189189 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.1314.tgz",
190190 "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
191+ "deprecated": "Use @eslint/config-array instead",
191192 "dev": true,
192193 "license": "Apache-2.0",
193194 "dependencies": {
194195 "@humanwhocodes/object-schema": "^2.0.12",
195196 "debug": "^4.13.1",
196197 "minimatch": "^3.0.5"
197198 },
198199 "engines": {
@@ -200,9 +201,9 @@
200201 }
201202 },
202203 "node_modules/@humanwhocodes/config-array/node_modules/debug": {
203204 "version": "4.3.46",
204205 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.46.tgz",
205206 "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7GO/vCNNhehwxfkQ09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
206207 "dev": true,
207208 "license": "MIT",
208209 "dependencies": {
@@ -239,9 +240,10 @@
239240 }
240241 },
241242 "node_modules/@humanwhocodes/object-schema": {
242243 "version": "2.0.13",
243244 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.13.tgz",
244245 "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+1GWiExi9QyzlD8x/yTvvLDHpoxLr0xxxeslWw08Zt7wIKcDcA==",
246+ "deprecated": "Use @eslint/object-schema instead",
245247 "dev": true,
246248 "license": "BSD-3-Clause"
247249 },
@@ -1391,12 +1393,11 @@
13911393 "license": "MIT"
13921394 },
13931395 "node_modules/axios": {
13941396 "version": "1.67.14",
13951397 "resolved": "https://registry.npmjs.org/axios/-/axios-1.67.14.tgz",
13961398 "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnSDukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/VG4I5yxig1pCEXE4gTWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
1397- "license": "MIT",
13981399 "dependencies": {
13991400 "follow-redirects": "^1.15.06",
14001401 "form-data": "^4.0.0",
14011402 "proxy-from-env": "^1.1.0"
14021403 }
@@ -2456,17 +2457,17 @@
24562457 }
24572458 },
24582459 "node_modules/eslint": {
24592460 "version": "8.5557.0",
24602461 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.5557.0.tgz",
24612462 "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdtdZ6+rCntju0xEH7teIABPwXpahftIaTdAmexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
24622463 "dev": true,
24632464 "license": "MIT",
24642465 "dependencies": {
24652466 "@eslint-community/eslint-utils": "^4.2.0",
24662467 "@eslint-community/regexpp": "^4.6.1",
24672468 "@eslint/eslintrc": "^2.1.4",
24682469 "@eslint/js": "8.5557.0",
24692470 "@humanwhocodes/config-array": "^0.11.1314",
24702471 "@humanwhocodes/module-importer": "^1.0.1",
24712472 "@nodelib/fs.walk": "^1.2.8",
24722473 "@ungap/structured-clone": "^1.2.0",
package.json+3 -3
@@ -32,7 +32,7 @@
3232 "rate-limiter-flexible": "^5.0.0",
3333 "response-time": "^2.3.2",
3434 "sanitize-filename": "^1.6.3",
3535 "sillytavern-transformers": "^2.14.6",
3636 "simple-git": "^3.19.1",
3737 "tiktoken": "^1.0.15",
3838 "vectra": "^0.2.2",
@@ -70,7 +70,7 @@
7070 "type": "git",
7171 "url": "https://github.com/SillyTavern/SillyTavern.git"
7272 },
7373 "version": "1.12.34",
7474 "scripts": {
7575 "start": "node server.js",
7676 "start:no-csrf": "node server.js --disableCsrf",
@@ -90,7 +90,7 @@
9090 "main": "server.js",
9191 "devDependencies": {
9292 "@types/jquery": "^3.5.29",
9393 "eslint": "^8.5557.0",
9494 "jquery": "^3.6.4"
9595 }
9696}
public/css/character-group-overlay.css+2 -2
@@ -89,7 +89,7 @@
8989 position: absolute;
9090 width: 100%;
9191 height: 100vh;
9292 height: 100svh100dvh;
9393 z-index: 9998;
9494 top: 0;
9595}
@@ -99,6 +99,6 @@
9999}
100100
101101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
102102 width: 100pxunset;
103103 padding: 0.25em;
104104}
public/css/extensions-panel.css+2 -1
@@ -7,7 +7,8 @@
77 background-color: green;
88}
99
1010.extensions_block input[type="checkbox"] {,
11+.extensions_block input[type="radio"] {
1112 margin-left: 10px;
1213 margin-right: 10px;
1314}
public/css/loader.css+2 -2
@@ -7,8 +7,8 @@
77 z-index: 999999;
88 width: 100vw;
99 height: 100vh;
1010 width: 100svw100dvw;
1111 height: 100svh100dvh;
1212 background-color: var(--SmartThemeBlurTintColor);
1313 color: var(--SmartThemeBodyColor);
1414 /*for some reason the full screen blur does not work on iOS*/
public/css/logprobs.css+3 -3
@@ -1,7 +1,7 @@
11#logprobsViewer {
22 overflow-y: auto;
33 max-width: 90svw90dvw;
44 max-height: 90svh90dvh;
55 min-width: 100px;
66 min-height: 50px;
77 border-radius: 10px;
@@ -16,7 +16,7 @@
1616 top: 0;
1717 margin: 0;
1818 right: unset;
1919 width: calc(((100svw100dvw - var(--sheldWidth)) / 2) - 1px);
2020}
2121
2222.logprobs_panel_header {
public/css/mobile-styles.css+23 -27
@@ -1,6 +1,8 @@
11/*will apply to anything 1000px or less. this catches ipads, horizontal phones, and vertical phones)*/
22@media screen and (max-width: 1000px) {
3- #send_form.compact #leftSendForm, #send_form.compact #rightSendForm {
3+
4+ #send_form.compact #leftSendForm,
5+ #send_form.compact #rightSendForm {
46 flex-wrap: nowrap;
57 width: unset;
68 }
@@ -34,9 +36,9 @@
3436 right: 0;
3537 width: fit-content;
3638 max-height: calc(60vh - 60px);
3739 max-height: calc(60svh60dvh - 60px);
3840 max-width: 90vw;
3941 max-width: 90svw90dvw;
4042 left: 50%;
4143 top: 50%;
4244 transform: translateX(-50%) translateY(-50%);
@@ -102,7 +104,7 @@
102104 min-width: unset;
103105 width: 100%;
104106 max-height: calc(100vh - 45px);
105107 max-height: calc(100svh100dvh - 45px);
106108 position: fixed;
107109 left: 0;
108110 top: 5px;
@@ -130,15 +132,15 @@
130132 #top-bar {
131133 position: fixed;
132134 width: 100vw;
133135 width: 100svw100dvw;
134136 }
135137
136138 #bg1,
137139 #bg_custom {
138140 height: 100vh !important;
139141 height: 100svh100dvh !important;
140142 width: 100vw !important;
141143 width: 100svw100dvw !important;
142144 background-position: center;
143145
144146 }
@@ -146,13 +148,7 @@
146148
147149 #sheld,
148150 #character_popup,
149151 .drawer-content {
150-
151- /* ,
152- #world_popup */
153- {
154- /*max-height: calc(100vh - 36px);
155- max-height: calc(100svh - 36px);*/
156152 width: 100% !important;
157153 margin: 0 auto;
158154 max-width: 100%;
@@ -224,9 +220,9 @@
224220 #cfgConfig,
225221 #logprobsViewer,
226222 #movingDivs>div {
227223 /* 100vh are fallback units for browsers that don't support svhdvh */
228224 height: calc(100vh - 45px);
229225 height: calc(100svh100dvh - 45px);
230226 min-width: 100% !important;
231227 width: 100% !important;
232228 max-width: 100% !important;
@@ -286,9 +282,9 @@
286282
287283 body.waifuMode #sheld {
288284 height: 40vh;
289285 height: 40svh40dvh;
290286 top: 60vh;
291287 top: 60svh60dvh;
292288 bottom: 0 !important;
293289 }
294290
@@ -325,16 +321,16 @@
325321 body.waifuMode .zoomed_avatar {
326322 width: fit-content;
327323 max-height: calc(60vh - 60px);
328324 max-height: calc(60svh60dvh - 60px);
329325 max-width: 90vw;
330326 max-width: 90svw90dvw;
331327 }
332328
333329 .scrollableInner {
334330 overflow-y: auto;
335331 overflow-x: hidden;
336332 max-height: calc(100vh - 90px);
337333 max-height: calc(100svh100dvh - 90px);
338334 }
339335
340336 .horde_multiple_hint {
@@ -370,9 +366,9 @@
370366
371367 body:not(.waifuMode) .zoomed_avatar {
372368 max-height: calc(60vh - 60px);
373369 max-height: calc(60svh60dvh - 60px);
374370 max-width: 90vw;
375371 max-width: 90svw90dvw;
376372 left: 50%;
377373 top: 50%;
378374 transform: translateX(-50%) translateY(-50%);
@@ -453,9 +449,9 @@
453449 min-height: unset;
454450 max-height: unset;
455451 width: 100vw;
456452 width: 100svw100dvw;
457453 height: calc(100vh - 36px);
458454 height: calc(100svh100dvh - 36px);
459455 padding-right: max(env(safe-area-inset-right), 0px);
460456 padding-left: max(env(safe-area-inset-left), 0px);
461457 padding-bottom: 0;
@@ -485,10 +481,10 @@
485481 top: 0;
486482 margin: 0 auto;
487483 height: calc(100vh - 70px);
488484 height: calc(100svh100dvh - 70px);
489485 width: calc(100% - 5px);
490486 max-height: calc(100vh - 70px);
491487 max-height: calc(100svh100dvh - 70px);
492488 max-width: calc(100% - 5px);
493489
494490 }
public/css/popup-safari-fix.css+1 -1
@@ -7,5 +7,5 @@ body.safari .popup.large_dialogue_popup .popup-body {
77body.safari .popup .popup-body {
88 height: fit-content;
99 max-height: 90vh;
1010 max-height: 90svh90dvh;
1111}
public/css/popup.css+4 -4
@@ -16,8 +16,8 @@ dialog {
1616 display: flex;
1717 flex-direction: column;
1818
1919 max-height: calc(100svh100dvh - 2em);
2020 max-width: calc(100svw100dvw - 2em);
2121 min-height: fit-content;
2222
2323 /* Overflow visible so elements (like toasts) can appear outside of the dialog. '.popup-body' is hiding overflow for the real content. */
@@ -103,7 +103,7 @@ body.no-blur .popup[open]::backdrop {
103103
104104.popup #toast-container {
105105 /* Fix toastr in dialogs by actually placing it at the top of the screen via transform */
106106 height: 100svh100dvh;
107107 top: calc(50% + var(--topBarBlockSize));
108108 left: 50%;
109109 transform: translate(-50%, -50%);
@@ -115,7 +115,7 @@ body.no-blur .popup[open]::backdrop {
115115.popup-crop-wrap {
116116 margin: 10px auto;
117117 max-height: 75vh;
118118 max-height: 75svh75dvh;
119119 max-width: 100%;
120120}
121121
public/css/tags.css+6 -6
@@ -209,20 +209,20 @@
209209 opacity: 1;
210210}
211211
212212.tag_as_folder.right_menu_button {
213213 filter: brightness(75%) saturate(0.6);
214214}
215215
216216.tag_as_folder.right_menu_button:hover,
217217.tag_as_folder.right_menu_button.flash {
218218 filter: brightness(150%) saturate(0.6) !important;
219219}
220220
221221.tag_as_folder.right_menu_button.no_folder {
222222 filter: brightness(25%) saturate(0.25);
223223}
224224
225225.tag_as_folder.right_menu_button .tag_folder_indicator {
226226 position: absolute;
227227 top: calc(var(--mainFontSize) * -0.5);
228228 right: calc(var(--mainFontSize) * -0.5);
public/css/toggle-dependent.css+5 -1
@@ -28,6 +28,10 @@ body.hideChatAvatars .mesAvatarWrapper .avatar {
2828 display: none !important;
2929}
3030
31+body.hideChatAvatars .last_mes {
32+ padding-bottom: 40px !important;
33+}
34+
3135body.square-avatars .avatar,
3236body.square-avatars .avatar img {
3337 border-radius: var(--avatar-base-border-radius) !important;
@@ -360,7 +364,7 @@ body.waifuMode #top-bar {
360364
361365body.waifuMode #sheld {
362366 height: 40vh;
363367 height: 40svh40dvh;
364368 top: calc(100% - 40vh);
365369 bottom: 0;
366370}
public/css/world-info.css+12 -0
@@ -120,6 +120,14 @@
120120 flex-wrap: wrap;
121121}
122122
123+.world_entry .inline-drawer-header {
124+ cursor: initial;
125+}
126+
127+.world_entry .killSwitch {
128+ cursor: pointer;
129+}
130+
123131.world_entry_form_control input[type=button] {
124132 cursor: pointer;
125133}
@@ -173,6 +181,10 @@
173181 width: 7em;
174182}
175183
184+.world_entry .killSwitch.fa-toggle-on {
185+ color: var(--SmartThemeQuoteColor);
186+}
187+
176188.wi-card-entry {
177189 border: 1px solid;
178190 border-color: var(--SmartThemeBorderColor);
public/global.d.ts+7 -0
@@ -14,8 +14,15 @@ declare var isProbablyReaderable;
1414declare var ePub;
1515declare var ai;
1616
17+declare var SillyTavern: {
18+ getContext(): any;
19+ llm: any;
20+};
21+
1722// Jquery plugins
1823interface JQuery {
24+ nanogallery2(options?: any): JQuery;
25+ nanogallery2(method: string, options?: any): JQuery;
1926 pagination(method: 'getCurrentPageNum'): number;
2027 pagination(method: string, options?: any): JQuery;
2128 pagination(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
60+eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61+SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62+AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
63+4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64+GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65+G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66+kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67+qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68+KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69+qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70+Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71+sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72+SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73+tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74+Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
75+0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76+WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77+UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78+Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80+i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81+W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82+HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83+O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84+Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85+ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86+AEA7BAsAaIdgAQDt/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
60+eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61+SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62+AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
63+4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64+GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65+G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66+kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67+qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68+KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69+qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70+Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71+sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72+SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73+tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74+Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
75+0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76+WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77+UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78+Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80+i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81+W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82+HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83+O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84+Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85+ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86+AEA7BAsAaIdgAQDt/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
60+eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61+SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62+AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
63+4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64+GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65+G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66+kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67+qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68+KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69+qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70+Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71+sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72+SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73+tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74+Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
75+0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76+WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77+UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78+Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80+i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81+W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82+HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83+O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84+Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85+ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86+AEA7BAsAaIdgAQDt/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
60+eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61+SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62+AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
63+4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64+GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65+G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66+kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67+qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68+KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69+qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70+Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71+sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72+SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73+tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74+Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
75+0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76+WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77+UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78+Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80+i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81+W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82+HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83+O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84+Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85+ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86+AEA7BAsAaIdgAQDt/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
95+eJzt3T9o3FgeB/Cv77aZBHInYZPD+JLF4iDxDl44ZNiAvVfNhC2vCEqxWQJpJn3MwlRXTeP0drNF
96+NtfIKa5cYnVxkQMLDoztbU6GgBkwDiPOEHuOK/aK7FM0Gs3M01gzkp6+HzBknPFEkSV93z/9NLO6
97+uvoLiIiIaKjfZL0BRERERcDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIi
98+ksDAJCIiksDAJCIiksDAJCIiksDAJCIikvBZ1htAlIZmswlN0wAAvu+j1WplvEX5YVkWOp0OHMfJ
99+elMoBYuLi8Gfj4+PM9yS8mFgEinMsiyYphm8ZmgSjY9DskSKioZlvV5HrVbLcIuoyEzTDEZxyoqB
100+SaSgaFgKDE0aR6PRCI6pMocmA5NIMYPCUmBoUhKNRgOGYQD4eOyUOTQZmEQKGRWWAkOTZPm+3/O6
101+zKHJwCRShGxYCgxNkmHbNlzX7fleWUNzqqtk5xaWMbuwHLx+d+Tg4vw0eH3txs2e19RPXBA1TQuW
102+lHc6nb5WYFj4ouj7ft/BT8UXF5ae50HX9Z7bbQD0XOTq9ToArp6l4WzbBoC+RWQA4Lru0OuPSqYa
103+mLMLy1i69yh4/f5kvycgv3nyI47evuwL0jKzLCuYPxjWmvM8D47jxIanOLDF+xiYahkUlltbW2g2
104+mz3fb7VaPfesAgxNksPQzOGQ7NK9R/jLgw3MhXqiZWSaJjY2NoJhj1FDH4ZhBCvZyjZMUmbDwnKQ
105+VqsV26ji8CyNUvbh2VwWLrh24ya+frCBs5N9uK+fl6q3aRgGarVa0Ksc5+ebzSZ2dnZK0+ors06n
106+0/N6VFgKcT1NVY8Vy7Lgui48z8t6Uwpr1HVpUE9T0zQ8ffp0rDAVo2F5GhHLXQ8zbG5hGd88+RG3
107+l2q4duNm1pszcaKXGHdQioPHtm3Yto2tra2hoViv15XsbY77f9I0DY1GQ7n94TgOdnZ2AMiHpRDu
108+acb1HFTQaDRgmmbP1AYlI86dUfsvrqfp+z42NzfHaowZhgHLsnI18pHLHmaUef+Z8r1NcVBGibnJ
109+uNZx+Hvi4AofrOJ7qhAnrWEYiU5CTdOCuTzLsmDbtlK9Kcdxxl7M1Wq1YJqmkmEZnf8Xv3v2NJMJ
110+r4GQfW+4IS9Cc9yeZp7m2H9769atv03rH5tbWMbcH78MXkcX94QXBEVdv3ETf/rzXwEAF+en+N9/
111+P0xuQ6csfEEXfN/HixcvgovhKL7vY3d3F7quY35+Pvi+rut97yvixdGyLFSrVQBApVJBtVrFwcEB
112+ut0uAGBtbQ2VSgUA0O12sbu7C6B/34r9U8R9MEy73Y79/qD9IvOzRddut1GtVoP/f6VSgWEYaLfb
113+V2owaZqGSqUSHHvTNu1h9MePH/e89jwPvu/3fIWvM4Zh4PLyEr7vB/uo2+3i4OAA8/PzfT876Cv6
114+mb7vZ36s5raHeXF+ig/np32Lf5buPcLnX9Thvn6Os5P9jLYuXdFe4FWetmHbNjqdTqJWYRG4rgvD
115+MIKLhZgbGdbTjGuIAPloqdLkxfVsxu1pip8LD0v6vo+9vb1SHU+Dhv3j6hYD/T3NJFMG0Tl2y7KC
116+sM5KbucwP5yf4s2r9dhhWLEo6OsHG4Wf24ybSBfLt8cVntdShed5fUOpwxYUDArLra0tDsmVSNwc
117+Wlz4DVOr1dBsNvver2ka6vV634W9jCaxejauMTzusG5acjske3F+indHDv5zdox//+sfuP67P+D3
118+c70HrBimFUO0RRymffjwYTBkBCS/oNdqteDADH/put43HAsUd0gWQDAkYxhGzzBbtVrtOYkqlQrW
119+1tb6fr6MYSkzJKs6MRw4zvCszDoAcQxOa99Oe0g2PFo17PpxeHjYNyUUNzwra9DvLTodM0257WFG
120+ua+f482r9dhFP+b9ZzDvPytcbzN6f6Xv+4kv6GIRTNyXigb1NEcpY1jSJ+P2NOMW4sXRNC1RWUJV
121+pd3THPZ7y6Knmds5zDhnJ/v46YfvcPerb/H5F/WegBS3oBSpUlD0BNvb28toS4pFhKbsScOwVEfc
122+MHsS0eNl2Jxm0kanqquNk0q7IpCYK15ZWQl+f4ZhBPt7mnOahelhhv38z78PXPRTpEpBKysrwZ99
123+3y/V4oGriutpxmFYqkWMyoz7Negz43qaSXswcVMgZZV2T9NxnL4Ohfi8aSpUDzPsQwF6kKOMuxI2
124+LMmqM9WM6mkyLEnWoIVjV/2MMptETzNq2o2UQgbm3a++HXjP5rsjB+7r51PeIsrKoNBkWKrpqsNv
125+g0It7ngRTwOSxeHYfmmFZtziK3HuT1OhAnNuYRl37z2KHW49O9nHz29fKnNv5iDiwPN9P6gjqlLV
126+mnFEQ5Nhqa6rjMokvdVIPPlHtucYretLH101NEXJ0LCkZSDTUpjANO8/w+2l+JqC7uvneHek9vyf
127+uH0kSnYuT3Xh1ibD8pNOp8OGFca7L9f3fdi2LbVSVpSwpHjjhmZcydCkBRDSlPvAvL1Ug3n/Wezf
128+qV5fVhhW+Fg8nUTV4tlJMCj7lXmOW7hKEQvP87CzszO0chYX7MlJGppiMVaYaMRkJbeBef3Xaj5x
129+w68X56dKlcYbRvZRX/V6PfOyUUR5k0bFJ8dx4LouS+OlIGloRhf1ZF08P7eBee3GzdhCBGVa1CNK
130+b8m+1zRNnrxEv0qzPKIYBhTzmWVqmIbncXVdly7mkITMU07ysDYht4EZVZZFPVdhGEZpA1MUZtc0
131+DcfHx8GCjbIr836JK2l31YtuWfZdWKfT6SleP6nbZ4aFpmEYmYclUJDALMOinjhJb8pVtRzeMHEr
132+6IBPcx15OMmywP2CYOW0OC/y0EMpItu2p1b0fFBo5mV9Rm6LrwMfe5VvXq3j/QR6lbOzs7i4uEj9
133+c9Ok63rwDEgZ4pmYZVGr1QYWxq5UKkGDI+n9dEXH/fJRt9uF53mYn5/H9va2MmE57eLr3W4X7XYb
134+mqZhZmam52ERk3CVgu2Tlsse5qQX9czOzuL777/H+vr6RD4/LUlP8DLdByY7v1uv16debzJL3C+9
135+srwFQSWe5020wSHzPM08yF0t2XdHDn764buJhuWTJ08m8tlpE08el1Wm+ctRj1wa971Fx/1CRTSJ
136+52lOwlR7mO9P9nH09mXwOloP9s2r9Yku6hFhOTs7O7F/I23RJ8YPMukWYN4kma8VC1/y1FKdFO4X
137+Kqq0a89OwlQD8+xkf2ggMiz7iUUawx5llVWZqKzkqcWZJ9wvVHR5D81czmGmrahhKXieh1arFRQx
138+0HUdmqbBdV24rluqnuW4sj7R8or7hfImz6GpfGAWPSzDHMcp1TzlIGJuV7ZHVZZQ4H4hVeQ1NHO3
139+6CdNKoUl9Yo+TDat9xYd9wupIo8LgZQNTIal2hzHkWpllq0wNvcLqWRQaGZVpEXJwGRYlsPm5ubQ
140+cMjiAbN5wP1CKomGpud5mVX+mWqln2lIEpZsYRdbt9vF7u4ufN9HpVIJqpC4rouDgwNsb2+Xcp6O
141++0Vt0670kweHh4fQdR2Xl5eZ3hEws7q6+ktm/3rKkvYs817ph4iI8kOZVbKi3B0REdEkKNXDJCIi
142+mhQlF/0QERGljYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQk
143+4bPFxcWst4GIiCj32MMkIiKSMHPnzh3WkiUiIhqBPUwiIiIJDEwiIiIJDEwiIiIJ/weH8+Ed/xCz
144+fAAAAABJRU5ErkJggg==
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+149 -48
@@ -383,7 +383,7 @@
383383 Max Response Length (tokens)
384384 </div>
385385 <div class="wide100p">
386386 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="501" max="800016384">
387387 </div>
388388 </div>
389389 <div class="range-block" data-source="openai,custom">
@@ -1273,7 +1273,7 @@
12731273 <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />
12741274 <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">
12751275 </div>
12761276 <div data-newbie-hidden data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" id="smoothingBlock" name="smoothingBlock" class="wide100p">
12771277 <h4 class="wide100p textAlignCenter">
12781278 <label data-i18n="Smooth Sampling">Smooth Sampling</label>
12791279 <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 @@
13581358 </div>
13591359 </div>
13601360 </div>
13611361 <div data-newbie-hidden data-tg-type="ooba,aphrodite,infermaticai,koboldcpp,llamacpp,mancer,ollama,tabby" id="mirostat_block_ooba" class="wide100p">
13621362 <h4 class="wide100p textAlignCenter">
13631363 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>
13641364 <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 @@
13871387 </div>
13881388 </div>
13891389 </div>
13901390 <div data-newbie-hidden data-tg-type="ooba, vllm" id="beamSearchBlock" name="beamSearchBlock" class="wide100p">
13911391 <h4 class="wide100p textAlignCenter">
13921392 <label>
13931393 <span data-i18n="Beam search">Beam Search</span>
@@ -1413,7 +1413,7 @@
14131413 </div>
14141414 </div>
14151415 </div>
14161416 <div data-tg-type="ooba" data-newbie-hidden id="contrastiveSearchBlock" name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
14171417 <h4 class="textAlignCenter" data-i18n="Contrastive search">Contrastive Search
14181418 <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>
14191419 </h4>
@@ -1544,7 +1544,7 @@
15441544 <h4 class="wide100p textAlignCenter">
15451545 <label>
15461546 <span data-i18n="Grammar String">Grammar String</span>
15471547 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which." title="GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which."></div>
15481548 <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">
15491549 <small>
15501550 <div class="fa-solid fa-up-right-from-square note-link-span"></div>
@@ -1554,13 +1554,13 @@
15541554 </h4>
15551555 <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>
15561556 </div>
15571557 <div id="sampler_order_blocksampler_order_block_kcpp" data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">
15581558 <hr class="wide100p">
15591559 <div class="range-block-title">
15601560 <span data-i18n="Samplers Order">Samplers Order</span>
15611561 </div>
15621562 <div class="toggle-description widthUnset" data-i18n="Samplers will be applied in a top-down order. Use with caution.">
15631563 kcpp only. Samplers will be applied in a top-down order.
15641564 Use with caution.
15651565 </div>
15661566 <div id="koboldcpp_order" class="prompt_order">
@@ -1597,10 +1597,10 @@
15971597 <span data-i18n="Load default order">Load default order</span>
15981598 </div>
15991599 </div>
16001600 <div id="sampler_order_block_lcpp" data-newbie-hidden data-tg-type="llamacpp" class="range-block flexFlowColumn wide100p">
16011601 <hr class="wide100p">
16021602 <h4 class="range-block-title justifyCenter">
16031603 <span data-i18n="SamplersSampler Order">SamplersSampler Order</span>
16041604 <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>
16051605 </h4>
16061606 <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 @@
16181618 <span data-i18n="Load default order">Load default order</span>
16191619 </div>
16201620 </div>
16211621 <div id="sampler_priority_block_ooba" data-newbie-hidden data-tg-type="ooba" class="range-block flexFlowColumn wide100p">
16221622 <hr class="wide100p">
16231623 <h4 class="range-block-title justifyCenter">
16241624 <span data-i18n="Sampler Priority">Sampler Priority</span>
@@ -1660,16 +1660,24 @@
16601660 </div>
16611661 <div class="inline-drawer-content">
16621662 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_none">
16631663 <input type="radio" id="character_names_none" name="character_names" value="0-1">
16641664 <span data-i18n="None">None</span>
16651665 <i class="right_menu_button fa-solid fa-circle-exclamation" title="Except forNever groupsadd andcharacter pastname personasprefixes. Otherwise, makeMay surebehave youpoorly providein namesgroups, inchoose thewith promptcaution." data-i18n="[title]character_names_none"></i>
16661666 <small class="flexBasis100p" data-i18n="Don'tNever add character names.">
16671667 Don'tNever add character names.
1668+ </small>
1669+ </label>
1670+ <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_default">
1671+ <input type="radio" id="character_names_default" name="character_names" value="0">
1672+ <span data-i18n="Default">Default</span>
1673+ <i class="right_menu_button fa-solid fa-circle-exclamation" title="Add prefixes for groups and past personas. Otherwise, make sure you provide names in the prompt." data-i18n="[title]character_names_default"></i>
1674+ <small class="flexBasis100p" data-i18n="Don't add character names unless necessary.">
1675+ Don't add character names unless necessary.
16681676 </small>
16691677 </label>
16701678 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_completion">
16711679 <input type="radio" id="character_names_completion" name="character_names" value="1">
16721680 <span data-i18n="Completion Object">Completion Object</span>
16731681 <i class="right_menu_button fa-solid fa-circle-exclamation" title="Restrictions apply: only Latin alphanumerics and underscores. Doesn't work for all sources, notably: Claude, MistralAI, Google." data-i18n="[title]character_names_completion"></i>
16741682 <small class="flexBasis100p" data-i18n="Add character names to completion objects.">
16751683 Add character names to completion objects.
@@ -1762,7 +1770,7 @@
17621770 </div>
17631771 </label>
17641772 </div>
17651773 <div class="range-block" data-source="openai,openrouter,makersuite,claude,custom,01ai">
17661774 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
17671775 <input id="openai_image_inlining" type="checkbox" />
17681776 <span data-i18n="Send inline images">Send inline images</span>
@@ -1815,10 +1823,16 @@
18151823 </div>
18161824 <div data-newbie-hidden class="range-block" data-source="claude">
18171825 <div class="wide100p">
1826+ <div class="flex-container alignItemsCenter">
18181827 <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
1819- <textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
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+ </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">
18201832 <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
1821- <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
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>
18221836 </div>
18231837 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
18241838 <input id="claude_use_sysprompt" type="checkbox" />
@@ -2419,6 +2433,7 @@
24192433 <optgroup>
24202434 <option value="01ai">01.AI (Yi)</option>
24212435 <option value="ai21">AI21</option>
2436+ <option value="blockentropy">Block Entropy</option>
24222437 <option value="claude">Claude</option>
24232438 <option value="cohere">Cohere</option>
24242439 <option value="groq">Groq</option>
@@ -2562,7 +2577,9 @@
25622577 </optgroup>
25632578 <optgroup label="GPT-4o">
25642579 <option value="gpt-4o">gpt-4o</option>
2580+ <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
25652581 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2582+ <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
25662583 </optgroup>
25672584 <optgroup label="gpt-4o-mini">
25682585 <option value="gpt-4o-mini">gpt-4o-mini</option>
@@ -2794,21 +2811,26 @@
27942811 <div>
27952812 <h4 data-i18n="Google Model">Google Model</h4>
27962813 <select id="model_google_select">
27972814 <optgroup label="LatestPrimary">
2798- <!-- Doesn't work without "latest". Maybe my key is scuffed? -->
2815+ <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
27992816 <option value="gemini-1.5-flash-latest">Gemini 1.5 Flash</option>
2800- <!-- Points to 1.0, no default 1.5 endpoint -->
2817+ <option value="gemini-1.0-pro">Gemini 1.0 Pro</option>
28012818 <option value="gemini-pro">Gemini Pro (1.0)</option>
28022819 <option value="gemini-pro-vision">Gemini Pro Vision (1.0)</option>
28032820 <option value="gemini-ultra">Gemini Ultra (1.0)</option>
2804- <option value="text-bison-001">Bison Text</option>
2805- <option value="chat-bison-001">Bison Chat</option>
2806- </optgroup>
2807- <optgroup label="Sub-versions">
2808- <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro</option>
2809- <option value="gemini-1.0-pro-latest">Gemini 1.0 Pro</option>
2810- <option value="gemini-1.0-pro-vision-latest">Gemini 1.0 Pro Vision</option>
28112821 <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>
28122834 </optgroup>
28132835 </select>
28142836 </div>
@@ -2845,6 +2867,7 @@
28452867 <option value="mistral-small-2402">mistral-small-2402</option>
28462868 <option value="mistral-medium-2312">mistral-medium-2312</option>
28472869 <option value="mistral-large-2402">mistral-large-2402</option>
2870+ <option value="mistral-large-2407">mistral-large-2407</option>
28482871 <option value="codestral-2405">codestral-2405</option>
28492872 <option value="codestral-mamba-2407">codestral-mamba-2407</option>
28502873 </optgroup>
@@ -2885,7 +2908,20 @@
28852908 </div>
28862909 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
28872910 <select id="model_perplexity_select">
28882911 <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">
28892925 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
28902926 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
28912927 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
@@ -2894,8 +2930,6 @@
28942930 <option value="sonar-small-online">sonar-small-online</option>
28952931 <option value="sonar-medium-chat">sonar-medium-chat</option>
28962932 <option value="sonar-medium-online">sonar-medium-online</option>
2897- </optgroup>
2898- <optgroup label="Open-Source Models">
28992933 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
29002934 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
29012935 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
@@ -2929,6 +2963,20 @@
29292963 </select>
29302964 </div>
29312965 </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>
29322980 <form id="custom_form" data-source="custom">
29332981 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
29342982 <div class="flex-container">
@@ -3060,7 +3108,11 @@
30603108 </div>
30613109 <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.">
30623110 <input id="context_use_stop_strings" type="checkbox" />
30633111 <small data-i18n="UseSeparators as Stop Strings">UseSeparators 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>
30643116 </label>
30653117 <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">
30663118 <input id="context_allow_jailbreak" type="checkbox" />
@@ -3288,6 +3340,24 @@
32883340 </div>
32893341 </div>
32903342 <div class="flex-container">
3343+ <div class="flex1" title="Inserted before the first User's message." data-i18n="[title]Inserted before the first User's message.">
3344+ <label for="instruct_first_input_sequence">
3345+ <small data-i18n="First User Prefix">First User Prefix</small>
3346+ </label>
3347+ <div>
3348+ <textarea id="instruct_first_input_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
3349+ </div>
3350+ </div>
3351+ <div class="flex1" title="Inserted before the last User's message." data-i18n="[title]instruct_last_input_sequence">
3352+ <label for="instruct_last_input_sequence">
3353+ <small data-i18n="Last User Prefix">Last User Prefix</small>
3354+ </label>
3355+ <div>
3356+ <textarea id="instruct_last_input_sequence" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
3357+ </div>
3358+ </div>
3359+ </div>
3360+ <div class="flex-container">
32913361 <div class="flex1" title="Will be inserted as a last prompt line when using system/neutral generation." data-i18n="[title]Will be inserted as a last prompt line when using system/neutral generation.">
32923362 <label for="instruct_last_system_sequence">
32933363 <small data-i18n="System Instruction Prefix">System Instruction Prefix</small>
@@ -3379,6 +3449,7 @@
33793449 <!-- Option #2 was a legacy GPT-2/3 tokenizer -->
33803450 <option value="3">Llama 1/2</option>
33813451 <option value="12">Llama 3</option>
3452+ <option value="13">Gemma / Gemini</option>
33823453 <option value="4">NerdStash (NovelAI Clio)</option>
33833454 <option value="5">NerdStash v2 (NovelAI Kayra)</option>
33843455 <option value="7">Mistral</option>
@@ -3635,8 +3706,8 @@
36353706 <div id="OpenAllWIEntries" class="menu_button fa-solid fa-expand" title="Open all Entries" data-i18n="[title]Open all Entries"></div>
36363707 <div id="CloseAllWIEntries" class="menu_button fa-solid fa-compress" title="Close all Entries" data-i18n="[title]Close all Entries"></div>
36373708 <div id="world_popup_new" class="menu_button fa-solid fa-plus" title="New Entry" data-i18n="[title]New Entry"></div>
36383709 <div id="world_backfill_memos" class="menu_button fa-solid fa-notes-medical" title="Fill empty Memo/Titles with Keywords" data-i18n="[title]Fill empty Memo/Titles with Keywords"></div><div id="world_apply_custom_sorting" class="menu_button fa-solid fa-solid fa-arrow-down-9-1"
36393710 <div id="world_apply_current_sorting" class="menu_button fa-solid fa-solid fa-arrow-down-9-1" title="Apply customcurrent sorting as Order" data-i18n="[title]Apply customcurrent sorting as Order"></div>
36403711 <div id="world_import_button" class="menu_button fa-solid fa-file-import" title="Import World Info" data-i18n="[title]Import World Info"></div>
36413712 <div id="world_popup_export" class="menu_button fa-solid fa-file-export" title="Export World Info" data-i18n="[title]Export World Info"></div>
36423713 <div id="world_duplicate" class="menu_button fa-solid fa-paste" title="Duplicate World Info" data-i18n="[title]Duplicate World Info"></div>
@@ -3999,7 +4070,7 @@
39994070 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">
40004071 <div class="slider_hint">
40014072 <span data-i18n="Slow">Slow</span>
40024073 <span data-i18n=""></span>
40034074 <span data-i18n="Fast">Fast</span>
40044075 </div>
40054076 </div>
@@ -4031,21 +4102,23 @@
40314102 <input id="restore_user_input" type="checkbox" />
40324103 <small data-i18n="Restore User Input">Restore User Input</small>
40334104 </label>
4105+ <div class="flex-container alignItemsCenter">
40344106 <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">
40354107 <input id="movingUImode" type="checkbox" />
40364108 <small data-i18n="Movable UI Panels">MovingUI&nbsp;<i class="fa-solid fa-desktop"></i></small>
40374109 </label>
4110+ <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>
4111+ </div>
40384112 <div data-newbie-hidden id="MovingUI-presets-block" class="flex-container alignitemscenter">
40394113 <div class="flex-container alignItemsFlexEnd">
40404114 <label for="movingUIPresets" title="MovingUI preset. Predefined/saved draggable positions." data-i18n="[title]MovingUI preset. Predefined/saved draggable positions">
40414115 <small data-i18n="MUI Preset">MUIMovingUI Preset:</small>
40424116 <div class="flex-container flexnowrap">
40434117 <select id="movingUIPresets" class="widthNatural flex1 margin0">
40444118 </select>
40454119 </div>
40464120 </label>
40474121 <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>
4048- <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>
40494122 </div>
40504123 </div>
40514124 <div data-newbie-hidden id="CustomCSS-block" class="flex-container flexFlowColumn">
@@ -4117,6 +4190,12 @@
41174190 Quick "Continue" button
41184191 </small>
41194192 </label>
4193+ <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">
4194+ <input id="quick_impersonate" type="checkbox" />
4195+ <small data-i18n="Quick 'Impersonate' button">
4196+ Quick "Impersonate" button
4197+ </small>
4198+ </label>
41204199 <div class="checkbox-container flex-container">
41214200 <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">
41224201 <input id="swipes-checkbox" type="checkbox" />
@@ -4237,6 +4316,16 @@
42374316 </div>
42384317 </div>
42394318 </div>
4319+ <div title="Determines which keys select an item from the AutoComplete suggestions">
4320+ <label data-i18n="Keyboard">
4321+ <small>Keyboard:</small>
4322+ </label>
4323+ <select id="stscript_autocomplete_select">
4324+ <option value="3" data-i18n="Select with Tab or Enter">Select with Tab or Enter</option>
4325+ <option value="1" data-i18n="Select with Tab">Select with Tab</option>
4326+ <option value="2" data-i18n="Select with Enter">Select with Enter</option>
4327+ </select>
4328+ </div>
42404329 <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">
42414330 <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>
42424331 <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">
@@ -4275,7 +4364,7 @@
42754364 <span class="fa-solid fa-circle-question note-link-span"></span>
42764365 </a>
42774366 </label>
42784367 <label class="checkbox_label" title="Prevents {{getvar::}} {{getglobalvar::}} macros from having literal macro-like values auto-evaluated.&NewLine;e.g. &quot;{{newline}}&quot; remains as literal string &quot;{{newline}}&quot;&NewLine;&NewLine;(This is done by internally replacing {{getvar::}} {{getglobalvar::}} macros with scoped variables.)" data-i18n="[title]Prevents {{getvar::}} {{getglobalvar::}} macros from having literal macro-like values auto-evaluated.&NewLine;e.g. &quot;{{newline}}&quot; remains as literal string &quot;{{newline}}&quot;&NewLine;&NewLine;(This is done by internally replacing {{getvar::}} {{getglobalvar::}} macros with scoped variables.)stscript_parser_flag_replace_getvar_label">
42794368 <input id="stscript_parser_flag_replace_getvar" type="checkbox" />
42804369 <span data-i18n="REPLACE_GETVAR"><small>REPLACE_GETVAR</small></span>
42814370 <a href="https://docs.sillytavern.app/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link">
@@ -4578,7 +4667,7 @@
45784667 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>
45794668 <input type="hidden" id="fav_checkbox" name="fav" />
45804669 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
45814670 <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]Character Loreworld_button_title"></div>
45824671 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
45834672 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
45844673 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
@@ -5235,21 +5324,22 @@
52355324 <div class="world_entry">
52365325 <form class="world_entry_form wi-card-entry">
52375326 <div class="inline-drawer wide100p">
52385327 <div class="inline-drawer-toggle inline-drawer-header gap5px padding0">
52395328 <span class="drag-handle">&#9776;</span>
52405329 <div class="gap5px world_entry_thin_controls wide100p alignitemscenter">
52415330 <div class="inline-drawer-toggle fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
5331+ <div class="fa-solid fa-toggle-on killSwitch" name="entryKillSwitch" title="Toggle entry's active state."></div>
52425332 <div class="flex-container alignitemscenter wide100p">
5333+
52435334 <div class="WIEntryTitleAndStatus flex-container flex1 alignitemscenter">
5335+
52445336 <div class="flex-container flex1">
52455337 <textarea class="text_pole" rows="1" name="comment" maxlength="5000" data-i18n="[placeholder]Entry Title/Memo" placeholder="Entry Title/Memo"></textarea>
52465338 </div>
5247- <!-- <span class="world_entry_form_position_value"></span> -->
52485339 <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">
52495340 <option value="constant" title="Constant" data-i18n="[title]WI_Entry_Status_Constant">🔵</option>
52505341 <option value="normal" title="Normal" data-i18n="[title]WI_Entry_Status_Normal">🟢</option>
52515342 <option value="vectorized" title="Vectorized" data-i18n="[title]WI_Entry_Status_Vectorized">🔗</option>
5252- <option value="disabled" title="Disabled" data-i18n="[title]WI_Entry_Status_Disabled">❌</option>
52535343 </select>
52545344 </div>
52555345 <div class="WIEnteryHeaderControls flex-container">
@@ -5722,6 +5812,11 @@
57225812 <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div>
57235813 <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div>
57245814 </div>
5815+ <div class="mes_img_swipes">
5816+ <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>
5817+ <div class="mes_img_swipe_counter">1/1</div>
5818+ <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>
5819+ </div>
57255820 <img class="mes_img" src="" />
57265821 </div>
57275822 <div class="mes_bias"></div>
@@ -5784,6 +5879,7 @@
57845879 Enable simple UI mode
57855880 </span>
57865881 </label>
5882+ <div class="expander"></div>
57875883 <div class="textAlignCenter">
57885884 <h3 data-i18n="Looking for AI characters?">
57895885 Looking for AI characters?
@@ -5802,6 +5898,7 @@
58025898 </span>
58035899 </span>
58045900 </div>
5901+ <div class="expander"></div>
58055902 <h3 data-i18n="Your Persona">
58065903 Your Persona
58075904 </h3>
@@ -6315,6 +6412,7 @@
63156412 <div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
63166413 <i class="fa-solid fa-circle-stop"></i>
63176414 </div>
6415+ <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>
63186416 <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>
63196417 <div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
63206418 </div>
@@ -6431,6 +6529,9 @@
64316529 <button class="menu_button set_default_persona" title="Select this as default persona for the new chats." data-i18n="[title]Select this as default persona for the new chats.">
64326530 <i class="fa-fw fa-solid fa-crown"></i>
64336531 </button>
6532+ <button class="menu_button duplicate_persona" title="Duplicate persona" data-i18n="[title]Duplicate persona">
6533+ <i class="fa-fw fa-solid fa-clone"></i>
6534+ </button>
64346535 <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona">
64356536 <i class="fa-fw fa-solid fa-trash-alt"></i>
64366537 </button>
public/locales/ar-sa.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "مخطط جيسون",
208208 "Type in the desired JSON schema": "اكتب مخطط JSON المطلوب",
209209 "Grammar String": "سلسلة القواعد",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "يعتمد GNBFGBNF أو ENBFEBNF على الواجهة الخلفية المستخدمة. إذا كنت تستخدم هذا يجب أن تعرف أي.",
211211 "Top P & Min P": "أعلى ع وأدنى ص",
212212 "Load default order": "تحميل الترتيب الافتراضي",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp فقط. تحديد ترتيب أخذ العينات. إذا لم يكن وضع Mirostat 0، فسيتم تجاهل ترتيب أخذ العينات.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "سلوك أسماء الشخصيات",
217217 "Helps the model to associate messages with characters.": "يساعد النموذج على ربط الرسائل بالأحرف.",
218218 "None": "لا شيء",
219219 "character_names_nonecharacter_names_default": "باستثناء المجموعات والشخصيات السابقة. بخلاف ذلك، تأكد من تقديم الأسماء في المطالبة.",
220220 "Don't add character names.": "لا تضيف أسماء الشخصيات.",
221221 "Completion": "كائن الإكمال",
222222 "character_names_completion": "تنطبق القيود: فقط الحروف الأبجدية اللاتينية والأرقام والشرطات السفلية. لا يعمل مع جميع المصادر، ولا سيما: Claude وMistralAI وGoogle.",
public/locales/de-de.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSON-Schema",
208208 "Type in the desired JSON schema": "Geben Sie das gewünschte JSON-Schema ein",
209209 "Grammar String": "Grammatikzeichenfolge",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF oder ENBFEBNF, hängt vom verwendeten Backend ab. Wenn Sie dieses verwenden, sollten Sie wissen, welches.",
211211 "Top P & Min P": "Top P und Min P",
212212 "Load default order": "Standardreihenfolge laden",
213213 "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.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Charakternamen Verhalten",
217217 "Helps the model to associate messages with characters.": "Hilft dem Modell, Nachrichten mit Zeichen zu verknüpfen.",
218218 "None": "Keins",
219219 "character_names_nonecharacter_names_default": "Außer für Gruppen und frühere Personas. Andernfalls stellen Sie sicher, dass Sie in der Eingabeaufforderung Namen angeben.",
220220 "Don't add character names.": "Fügen Sie keine Charakternamen hinzu.",
221221 "Completion": "Vervollständigungsobjekt",
222222 "character_names_completion": "Es gelten Einschränkungen: nur lateinische alphanumerische Zeichen und Unterstriche. Funktioniert nicht für alle Quellen, insbesondere: Claude, MistralAI, Google.",
public/locales/es-es.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Esquema JSON",
208208 "Type in the desired JSON schema": "Escriba el esquema JSON deseado",
209209 "Grammar String": "Cadena de gramática",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF o ENBFEBNF, depende del backend en uso. Si estás usando esto, debes saber cuál.",
211211 "Top P & Min P": "P superior y P mínima",
212212 "Load default order": "Cargar orden predeterminado",
213213 "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.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Comportamiento de los nombres de personajes",
217217 "Helps the model to associate messages with characters.": "Ayuda al modelo a asociar mensajes con personajes.",
218218 "None": "Ninguno",
219219 "character_names_nonecharacter_names_default": "Excepto grupos y personas pasadas. De lo contrario, asegúrese de proporcionar nombres en el mensaje.",
220220 "Don't add character names.": "No agregues nombres de personajes.",
221221 "Completion": "Objeto de finalización",
222222 "character_names_completion": "Aplican restricciones: solo caracteres alfanuméricos latinos y guiones bajos. No funciona para todas las fuentes, en particular: Claude, MistralAI, Google.",
public/locales/fr-fr.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Schéma JSON",
208208 "Type in the desired JSON schema": "Tapez le schéma JSON souhaité",
209209 "Grammar String": "Chaîne de grammaire",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF ou ENBFEBNF dépend du backend utilisé. Si vous l'utilisez, vous devez savoir lequel.",
211211 "Top P & Min P": "P supérieur et P minimal",
212212 "Load default order": "Charger l'ordre par défaut",
213213 "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é.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Comportement des noms de personnages",
217217 "Helps the model to associate messages with characters.": "Aide le modèle à associer des messages à des personnages.",
218218 "None": "Aucun",
219219 "character_names_nonecharacter_names_default": "Sauf pour les groupes et les personnages passés. Sinon, assurez-vous de fournir des noms dans l'invite.",
220220 "Don't add character names.": "N'ajoutez pas de noms de personnages.",
221221 "Completion": "Objet d'achèvement",
222222 "character_names_completion": "Des restrictions s'appliquent : uniquement les caractères alphanumériques latins et les traits de soulignement. Ne fonctionne pas pour toutes les sources, notamment : Claude, MistralAI, Google.",
public/locales/is-is.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSON kerfi",
208208 "Type in the desired JSON schema": "Sláðu inn æskilegt JSON skema",
209209 "Grammar String": "Málfræðistrengur",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF eða ENBFEBNF, fer eftir bakendanum sem er í notkun. Ef þú ert að nota þetta ættir þú að vita hvaða.",
211211 "Top P & Min P": "Efstu P & Min P",
212212 "Load default order": "Hlaða sjálfgefna röð",
213213 "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ð.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Hegðun persónunafna",
217217 "Helps the model to associate messages with characters.": "Hjálpar líkaninu að tengja skilaboð við stafi.",
218218 "None": "Enginn",
219219 "character_names_nonecharacter_names_default": "Nema hópar og fyrri persónur. Annars, vertu viss um að gefa upp nöfn í hvetjunni.",
220220 "Don't add character names.": "Ekki bæta við persónunöfnum.",
221221 "Completion": "Lokunarhlutur",
222222 "character_names_completion": "Takmarkanir gilda: aðeins latneskar tölustafir og undirstrik. Virkar ekki fyrir allar heimildir, sérstaklega: Claude, MistralAI, Google.",
public/locales/it-it.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Schema JSON",
208208 "Type in the desired JSON schema": "Digita lo schema JSON desiderato",
209209 "Grammar String": "Stringa grammaticale",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF o ENBFEBNF, dipende dal backend in uso. Se stai usando questo dovresti sapere quale.",
211211 "Top P & Min P": "P massimo e P minimo",
212212 "Load default order": "Carica ordine predefinito",
213213 "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.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Comportamento dei nomi dei personaggi",
217217 "Helps the model to associate messages with characters.": "Aiuta il modello ad associare i messaggi ai personaggi.",
218218 "None": "Nessuno",
219219 "character_names_nonecharacter_names_default": "Fatta eccezione per i gruppi e i personaggi passati. Altrimenti, assicurati di fornire i nomi nel prompt.",
220220 "Don't add character names.": "Non aggiungere nomi di personaggi.",
221221 "Completion": "Oggetto di completamento",
222222 "character_names_completion": "Si applicano restrizioni: solo caratteri alfanumerici latini e trattini bassi. Non funziona con tutte le fonti, in particolare: Claude, MistralAI, Google.",
public/locales/ja-jp.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSONスキーマ",
208208 "Type in the desired JSON schema": "希望するJSONスキーマを入力します",
209209 "Grammar String": "文法文字列",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF または ENBFEBNF は、使用するバックエンドによって異なります。これを使用する場合は、どちらであるかを知っておく必要があります。",
211211 "Top P & Min P": "トップPと最小P",
212212 "Load default order": "デフォルトの順序を読み込む",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp のみ。サンプラーの順序を決定します。Mirostat モードが 0 でない場合、サンプラーの順序は無視されます。",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "キャラクター名の動作",
217217 "Helps the model to associate messages with characters.": "モデルがメッセージをキャラクターに関連付けるのに役立ちます。",
218218 "None": "なし",
219219 "character_names_nonecharacter_names_default": "グループと過去のペルソナを除きます。それ以外の場合は、プロンプトに名前を必ず入力してください。",
220220 "Don't add character names.": "キャラクター名を追加しないでください。",
221221 "Completion": "完了オブジェクト",
222222 "character_names_completion": "制限事項: ラテン英数字とアンダースコアのみ。すべてのソースで機能するわけではありません。特に、Claude、MistralAI、Google では機能しません。",
public/locales/ko-kr.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSON 스키마",
208208 "Type in the desired JSON schema": "원하는 JSON 스키마를 입력하세요.",
209209 "Grammar String": "문법 문자열",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF 또는 ENBF는EBNF는 사용 중인 백엔드에 따라 다릅니다. 이것을 사용한다면 어느 것이 무엇인지 알아야합니다.",
211211 "Top P & Min P": "상위 P 및 최소 P",
212212 "Load default order": "기본 순서로 로드",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp만 가능합니다. 샘플러의 순서를 결정합니다. Mirostat 모드가 0이 아닌 경우 샘플러 순서는 무시됩니다.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "캐릭터 이름 행동",
217217 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",
218218 "None": "없음",
219219 "character_names_nonecharacter_names_default": "그룹 및 과거 페르소나는 제외됩니다. 그렇지 않으면 프롬프트에 이름을 제공해야 합니다.",
220220 "Don't add character names.": "캐릭터 이름을 추가하지 마세요.",
221221 "Completion": "완료 객체",
222222 "character_names_completion": "제한 사항이 적용됩니다. 라틴 영숫자 및 밑줄만 사용할 수 있습니다. 모든 소스, 특히 Claude, MistralAI, Google에서 작동하지 않습니다.",
public/locales/nl-nl.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSON-schema",
208208 "Type in the desired JSON schema": "Typ het gewenste JSON-schema",
209209 "Grammar String": "Grammaticareeks",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF of ENBFEBNF, hangt af van de gebruikte backend. Als u dit gebruikt, moet u weten welke.",
211211 "Top P & Min P": "Top P & Min P",
212212 "Load default order": "Standaardvolgorde laden",
213213 "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.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Karakternamen Gedrag",
217217 "Helps the model to associate messages with characters.": "Helpt het model berichten aan karakters te koppelen.",
218218 "None": "Geen",
219219 "character_names_nonecharacter_names_default": "Behalve voor groepen en vroegere persona's. Zorg er anders voor dat u namen opgeeft in de prompt.",
220220 "Don't add character names.": "Voeg geen namen van personages toe.",
221221 "Completion": "Voltooiingsobject",
222222 "character_names_completion": "Er zijn beperkingen van toepassing: alleen Latijnse alfanumerieke tekens en onderstrepingstekens. Werkt niet voor alle bronnen, met name: Claude, MistralAI, Google.",
public/locales/pt-pt.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Esquema JSON",
208208 "Type in the desired JSON schema": "Digite o esquema JSON desejado",
209209 "Grammar String": "Cadeia de Gramática",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF ou ENBFEBNF, depende do backend em uso. Se você estiver usando isso, você deve saber qual.",
211211 "Top P & Min P": "P superior e P mínimo",
212212 "Load default order": "Carregar ordem padrão",
213213 "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.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Comportamento dos nomes dos personagens",
217217 "Helps the model to associate messages with characters.": "Ajuda o modelo a associar mensagens a personagens.",
218218 "None": "Nenhum",
219219 "character_names_nonecharacter_names_default": "Exceto para grupos e personas passadas. Caso contrário, certifique-se de fornecer nomes no prompt.",
220220 "Don't add character names.": "Não adicione nomes de personagens.",
221221 "Completion": "Objeto de conclusão",
222222 "character_names_completion": "Aplicam-se restrições: apenas alfanuméricos latinos e sublinhados. Não funciona para todas as fontes, nomeadamente: Claude, MistralAI, Google.",
public/locales/ru-ru.json+2 -2
@@ -978,7 +978,7 @@
978978 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
979979 "char_import_7": ")",
980980 "Grammar String": "Грамматика",
981981 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF или ENBFEBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
982982 "Account": "Аккаунт",
983983 "Hi,": "Привет,",
984984 "To enable multi-account features, restart the SillyTavern server with": "Чтобы активировать систему аккаунтов, перезапустите SillyTavern, выставив",
@@ -1246,7 +1246,7 @@
12461246 "Top P & Min P": "Top P & Min P",
12471247 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.",
12481248 "Helps the model to associate messages with characters.": "Помогает модели связывать сообщения с персонажами.",
12491249 "character_names_nonecharacter_names_default": "Except for groups and past personas. Otherwise, make sure you provide names in the prompt.",
12501250 "Completion": "Completion Object",
12511251 "character_names_completion": "Только латинские буквы, цифры и знак подчёркивания. Работает не для всех бэкендов, в частности для Claude, MistralAI, Google.",
12521252 "Use AI21 Tokenizer": "Использовать токенайзер AI21",
public/locales/uk-ua.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Схема JSON",
208208 "Type in the desired JSON schema": "Введіть потрібну схему JSON",
209209 "Grammar String": "Граматичний рядок",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF або ENBFEBNF, залежить від серверної частини, яка використовується. Якщо ви використовуєте це, ви повинні знати, який.",
211211 "Top P & Min P": "Верхній P & Min P",
212212 "Load default order": "Завантажити типовий порядок",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "лише llama.cpp. Визначає порядок пробовідбірників. Якщо режим Mirostat не 0, порядок вибірки ігнорується.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Поведінка імен персонажів",
217217 "Helps the model to associate messages with characters.": "Допомагає моделі пов’язувати повідомлення з символами.",
218218 "None": "Немає",
219219 "character_names_nonecharacter_names_default": "За винятком груп і минулих персонажів. В іншому випадку переконайтеся, що ви вказали імена в підказці.",
220220 "Don't add character names.": "Не додавайте імена персонажів.",
221221 "Completion": "Об'єкт завершення",
222222 "character_names_completion": "Застосовуються обмеження: лише латинські букви та цифри підкреслення. Працює не для всіх джерел, зокрема: Claude, MistralAI, Google.",
public/locales/vi-vn.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Lược đồ JSON",
208208 "Type in the desired JSON schema": "Nhập lược đồ JSON mong muốn",
209209 "Grammar String": "Chuỗi ngữ pháp",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF hoặc ENBFEBNF, 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.",
211211 "Top P & Min P": "P & P tối thiểu hàng đầu",
212212 "Load default order": "Tải thứ tự mặc định",
213213 "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.",
@@ -216,7 +216,7 @@
216216 "Character Names Behavior": "Tên nhân vật Hành vi",
217217 "Helps the model to associate messages with characters.": "Giúp mô hình liên kết tin nhắn với các ký tự.",
218218 "None": "Không",
219219 "character_names_nonecharacter_names_default": "Ngoại trừ các nhóm và cá tính trong quá khứ. Nếu không, hãy đảm bảo bạn cung cấp tên trong lời nhắc.",
220220 "Don't add character names.": "Không thêm tên nhân vật.",
221221 "Completion": "Đối tượng hoàn thành",
222222 "character_names_completion": "Áp dụng hạn chế: chỉ chữ và số Latinh và dấu gạch dưới. Không hoạt động với tất cả các nguồn, đặc biệt là: Claude, MistralAI, Google.",
public/locales/zh-cn.json+89 -28
@@ -69,8 +69,8 @@
6969 "Top A": "Top A",
7070 "Quick Prompts Edit": "快速提示词编辑",
7171 "Main": "主要",
7272 "NSFWAuxiliary": "NSFW辅助的",
7373 "JailbreakPost-History Instructions": "越狱后续历史指令",
7474 "Utility Prompts": "实用提示词",
7575 "Impersonation prompt": "AI帮答提示词",
7676 "Restore default prompt": "恢复默认提示词",
@@ -208,19 +208,21 @@
208208 "JSON Schema": "JSON 结构",
209209 "Type in the desired JSON schema": "输入所需的 JSON 结构",
210210 "Grammar String": "语法字符串",
211211 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNFENBFEBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
212212 "Top P & Min P": "Top P 和 Min P",
213213 "Load default order": "加载默认顺序",
214+ "Sampler Order": "取样器顺序",
214215 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。",
215216 "Sampler Priority": "采样器优先级",
216217 "Ooba only. Determines the order of samplers.": "确定采样器的顺序(仅适用于Ooba)",
217218 "Character Names Behavior": "角色名称行为",
218219 "Helps the model to associate messages with characters.": "有助于模型将消息与角色关联起来。",
219220 "None": "无",
220221 "tag_import_nonecharacter_names_none": "不添加角色名称前缀。在群聊中可能导致错误行为,谨慎勾选。",
221222 "character_names_noneNever add character names.": "群聊和过去的角色除外。否则,请确保在提示词中提供了姓名不添加角色名称。",
222223 "Don't add character names.character_names_default": "不添加角色名称群聊和过去的角色除外否则,请确保在提示词中提供了姓名。",
223- "Completion": "补全对象",
224+ "Don't add character names unless necessary.": "如非必要,否则不添加角色名称。",
225+ "Completion Object": "补全对象",
224226 "character_names_completion": "适用限制:仅限拉丁字母数字和下划线。不适用于所有补全源,尤其是:Claude、MistralAI、Google。",
225227 "Add character names to completion objects.": "在补全对象中添加角色名称。",
226228 "Message Content": "消息内容",
@@ -318,6 +320,7 @@
318320 "View Remaining Credits": "查看剩余额度",
319321 "OpenRouter Model": "OpenRouter 模型",
320322 "Model Providers": "模型提供者",
323+ "Allow fallback providers": "允许后备提供者",
321324 "InfermaticAI API Key": "InfermaticAI API 密钥",
322325 "InfermaticAI Model": "InfermaticAI 模型",
323326 "DreamGen API key": "DreamGen API 密钥",
@@ -346,6 +349,7 @@
346349 "Ollama Model": "Ollama 模型",
347350 "Download": "下载",
348351 "Tabby API key": "Tabby API 密钥",
352+ "Tabby Model": "Tabby 模型",
349353 "koboldcpp API key (optional)": "koboldcpp API 密钥(可选)",
350354 "Example: 127.0.0.1:5001": "示例:127.0.0.1:5001",
351355 "Authorize": "授权",
@@ -363,13 +367,14 @@
363367 "This will show up as your saved preset.": "这将显示为您保存的预设。",
364368 "Proxy Server URL": "代理服务器 URL",
365369 "Alternative server URL (leave empty to use the default value).": "备用服务器 URL(留空以使用默认值)。",
366- "Remove your real OAI API Key from the API panel BEFORE typing anything into this box": "在键入任何内容之前,从 API 面板中删除您的真实 OAI API 密钥",
367- "We cannot provide support for problems encountered while using an unofficial OpenAI proxy": "我们无法为使用非官方 OpenAI 代理时遇到的问题提供支持",
368370 "Doesn't work? Try adding": "不起作用?尝试在最后添加",
369371 "at the end!": "!",
370372 "Proxy Password": "代理密码",
371373 "Will be used as a password for the proxy instead of API key.": "将用作代理的密码,而不是 API 密钥。",
372374 "Peek a password": "查看密码",
375+ "Using a proxy that you're not running yourself is a risk to your data privacy.": "使用您自己未运行的代理会对您的数据隐私造成风险。",
376+ "ANY support requests will be REFUSED if you are using a proxy.": "如果您使用代理,任何支持请求都将被拒绝。",
377+ "Do not proceed if you do not agree to this!": "如果您不同意,请不要继续!",
373378 "OpenAI API key": "OpenAI API 密钥",
374379 "View API Usage Metrics": "查看API使用情况",
375380 "Follow": "跟随",
@@ -384,14 +389,14 @@
384389 "Slack and Poe cookies will not work here, do not bother trying.": "Slack和Poe的cookie在这里不起作用,请不要尝试。",
385390 "Claude Model": "Claude 模型",
386391 "Window AI Model": "Window AI 模型",
392+ "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。",
393+ "Allow fallback models": "允许后备模型",
387394 "Model Order": "OpenRouter 模型顺序",
388395 "Alphabetically": "按字母顺序",
389396 "Price": "价格(最便宜)",
390397 "Context Size": "上下文大小",
391398 "Group by vendors": "按供应商分组",
392399 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",
393- "Allow fallback routes": "允许后备方案",
394- "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。",
395400 "openrouter_force_instruct": "此选项已过时,将来会被删除。要使用指令格式,请改用文本完成 API 下的 OpenRouter。",
396401 "LEGACY": "旧版",
397402 "Force Instruct Mode formatting": "强制指令模式格式化",
@@ -439,9 +444,11 @@
439444 "Example Separator": "示例分隔符",
440445 "Chat Start": "聊天开始",
441446 "Add Chat Start and Example Separator to a list of stopping strings.": "将聊天开始和示例分隔符添加到停止字符串列表中。",
442447 "UseSeparators as Stop Strings": "用作停止字符串分隔符作为终止字符串",
443- "context_allow_jailbreak": "如果在角色卡中定义并且启用了“首选角色越狱”,则在提示词末尾包含越狱。\n不建议在文本完成模型中使用此功能,否则会导致输出错误。",
448+ "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",
444449 "AllowNames Jailbreakas Stop Strings": "允许越狱名称作为终止字符串",
450+ "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。",
451+ "Allow Post-History Instructions": "允许后历史说明",
445452 "Context Order": "上下文顺序",
446453 "Summary": "总结",
447454 "Author's Note": "作者注释",
@@ -487,6 +494,10 @@
487494 "First Assistant Prefix": "第一个助理前缀",
488495 "instruct_last_output_sequence": "插入到最后一条助手消息之前或作为生成 AI 回复时的最后一行提示词(中立/系统角色除外)。",
489496 "Last Assistant Prefix": "最后一个助理前缀",
497+ "Inserted before the first User's message.": "插入在第一个用户的消息之前。",
498+ "First User Prefix": "第一个用户前缀",
499+ "instruct_last_input_sequence": "插入到最后一条用户消息之前。",
500+ "Last User Prefix": "上次用户前缀",
490501 "Will be inserted as a last prompt line when using system/neutral generation.": "当使用系统/中性生成时将作为最后的一行提示词插入。",
491502 "System Instruction Prefix": "系统指令前缀",
492503 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "如果生成了停止序列,则该序列之后的所有内容都将从输出中删除(包括在内)。",
@@ -534,6 +545,7 @@
534545 "Sorted Evenly": "均匀排序",
535546 "Character Lore First": "角色世界书优先",
536547 "Global Lore First": "全局世界书优先",
548+ "Include names with each message into the context for scanning": "将每条消息的名称纳入上下文中以供扫描",
537549 "Entries can activate other entries by mentioning their keywords": "条目可以通过提及它们的关键字来激活其他条目",
538550 "Recursive Scan": "递归扫描",
539551 "Lookup for the entry keys in the context will respect the case": "在上下文中查找条目键将保持大小写敏感",
@@ -552,6 +564,7 @@
552564 "Close all Entries": "关闭所有条目",
553565 "New Entry": "新条目",
554566 "Fill empty Memo/Titles with Keywords": "使用关键字填充空的备忘录/标题",
567+ "Apply current sorting as Order": "应用当前排序作为顺序",
555568 "Import World Info": "导入世界书",
556569 "Export World Info": "导出世界书",
557570 "Duplicate World Info": "复制世界书",
@@ -659,14 +672,15 @@
659672 "Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定义在导入卡片时应选择哪种操作来导入其列出的标签。“询问”将始终显示对话框。",
660673 "Import Card Tags": "导入卡片标签",
661674 "Ask": "询问",
675+ "tag_import_none": "无",
662676 "tag_import_all": "全部",
663677 "Existing": "现存的",
664678 "Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊匹配,在列表中通过所有数据字段搜索角色,而不仅仅是名称子字符串",
665679 "Advanced Character Search": "高级角色搜索",
666680 "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果角色卡包含提示词,则使用它替代系统提示词",
667681 "Prefer Character Card Prompt": "角色卡提示词优先",
668682 "If checked and the character card contains a jailbreak override (Post -History Instruction)Instructions override, use that instead": "如果角色卡包含越狱(后置历史记录指令)如果选中并且角色卡包含后历史指令覆盖则使用它替代系统越狱则使用它。",
669683 "Prefer Character Card JailbreakInstructions": "角色卡越狱优先首选角色卡说明",
670684 "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和调整导入的角色图像的大小。关闭时,裁剪/调整大小为 512x768。",
671685 "Never resize avatars": "永不调整头像大小",
672686 "Show actual file names on the disk, in the characters list display only": "在角色列表显示中,显示磁盘上实际的文件名。",
@@ -693,10 +707,10 @@
693707 "Restore User Input": "恢复用户输入",
694708 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",
695709 "Movable UI Panels": "可移动 UI 面板",
710+ "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
696711 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",
697712 "MUI Preset": "可移动 UI 预设",
698713 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",
699- "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
700714 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",
701715 "Custom CSS": "自定义 CSS",
702716 "Expand the editor": "展开编辑器",
@@ -716,6 +730,8 @@
716730 "Press Send to continue": "按发送键以继续",
717731 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",
718732 "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": "快速“模仿”按钮",
719735 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",
720736 "Swipes": "刷新回复按钮",
721737 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",
@@ -739,6 +755,8 @@
739755 "Log prompts to console": "将提示词记录到控制台",
740756 "Requests logprobs from the API for the Token Probabilities feature": "从API请求对数概率数据,用于实现词符概率功能。",
741757 "Request token probabilities": "请求词符概率",
758+ "In group chat, highlight the character(s) that are currently queued to generate responses and the order in which they will respond.": "在群聊中,突出显示当前排队等待生成响应的角色以及他们响应的顺序。",
759+ "Show group chat queue": "显示群聊队列",
742760 "Automatically reject and re-generate AI message based on configurable criteria": "根据可配置的条件自动拒绝并重新生成AI消息",
743761 "Auto-swipe": "自动滑动",
744762 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",
@@ -759,6 +777,10 @@
759777 "Autocomplete Style": "风格",
760778 "Follow Theme": "关注主题",
761779 "Dark": "黑暗的",
780+ "Keyboard": "键盘:",
781+ "Select with Tab or Enter": "使用 Tab 或 Enter 选择",
782+ "Select with Tab": "使用 Tab 选择",
783+ "Select with Enter": "按 Enter 键选择",
762784 "Sets the font size of the autocomplete.": "设置自动完成的字体大小。",
763785 "Sets the width of the autocomplete.": "设置自动完成的宽度。",
764786 "Autocomplete Width": "宽度",
@@ -770,7 +792,7 @@
770792 "Parser Flags": "解析器标志",
771793 "Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "切换到更严格的转义,允许所有分隔字符用反斜杠转义,并且反斜杠也可以转义。",
772794 "STRICT_ESCAPING": "严格转义",
773795 "Replacestscript_parser_flag_replace_getvar_label": all"防止 {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.": "用范围变量替换所有宏具有自动评估的文字宏类值。\n例如,“{{newline}}”保留为文字字符串“{{newline}}”\n\n(这是通过在内部用范围变量替换 {{getvar::}} {{getglobalvar::}} 宏,以避免双重宏替换宏来实现的",
774796 "REPLACE_GETVAR": "替换GETVAR",
775797 "Change Background Image": "更改背景图片",
776798 "Filter": "搜索",
@@ -921,7 +943,7 @@
921943 "Insert {{original}} into either box to include the respective default prompt from system settings.": "将{{original}}插入到任一框中,以包含系统设置中的相应默认提示词。",
922944 "Main Prompt": "主要提示词",
923945 "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此处的任何内容都将替换用于此角色的默认主提示词。(v2规范:system_prompt)",
924946 "Any contents here will replace the default JailbreakPost-History PromptInstructions used for this character. (v2 spec: post_history_instructions)": "此处的任何内容都将替换用于此角色的默认越狱提示词此处的任何内容都将替换此角色使用的默认后历史说明\nv2规范v2 规范:post_history_instructions)",
925947 "Creator's Metadata (Not sent with the AI prompt)": "创作者的元数据(不与AI提示词一起发送)",
926948 "Creator's Metadata": "创作者的元数据",
927949 "(Not sent with the AI Prompt)": "(不随 AI 提示词发送)",
@@ -957,9 +979,6 @@
957979 "Lock": "加锁",
958980 "Unlock": "解锁",
959981 "Delete background": "删除背景",
960- "Chat Scenario Override": "聊天场景覆盖",
961- "Remove": "移除",
962- "Type here...": "在此处输入...",
963982 "Chat Lorebook": "聊天知识书",
964983 "Chat Lorebook for": "聊天知识书",
965984 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
@@ -1047,6 +1066,7 @@
10471066 "Use Probability": "使用概率",
10481067 "Add Memo": "添加备忘录",
10491068 "Text or token ids": "文本或 [token ID]",
1069+ "Type here...": "在此处输入...",
10501070 "close": "关闭",
10511071 "prompt_manager_edit": "编辑",
10521072 "prompt_manager_name": "姓名",
@@ -1054,8 +1074,9 @@
10541074 "To whom this message will be attributed.": "此消息应归于谁。",
10551075 "AI Assistant": "AI助手",
10561076 "prompt_manager_position": "位置",
10571077 "Injection position. NextRelative (to other prompts (relativein prompt manager) or inIn-chat (absolute)@ Depth.": "注入位置。其他提示词旁边(相对)或在聊天中绝对相对于提示管理器中的其他提示或在聊天中@深度。",
10581078 "prompt_manager_relative": "相对",
1079+ "prompt_manager_in_chat": "聊天中",
10591080 "prompt_manager_depth": "深度",
10601081 "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最后一条消息之后,1 = 在最后一条消息之前,等等。",
10611082 "Prompt": "提示词",
@@ -1164,6 +1185,7 @@
11641185 "Pause script execution": "暂停执行脚本",
11651186 "Abort script execution": "中止执行脚本",
11661187 "Abort request": "中止请求",
1188+ "Ask AI to write your message for you": "让AI为您撰写消息",
11671189 "Continue the last message": "继续上一条消息",
11681190 "Send a message": "发送消息",
11691191 "Close chat": "关闭聊天",
@@ -1175,19 +1197,24 @@
11751197 "Manage chat files": "管理聊天文件",
11761198 "Delete messages": "删除消息",
11771199 "Regenerate": "重新生成",
1178- "Ask AI to write your message for you": "请求AI为您撰写消息",
11791200 "Impersonate": "AI 帮答",
11801201 "Continue": "继续",
11811202 "Bind user name to that avatar": "将用户名称绑定到该头像",
11821203 "Change persona image": "更改用户角色头像",
11831204 "Select this as default persona for the new chats.": "选择此项作为新聊天的默认用户角色。",
1205+ "Duplicate persona": "复制用户角色",
11841206 "Delete persona": "删除用户角色",
11851207 "These characters are the winners of character design contests and have outstandable quality.": "这些角色都是角色设计大赛的获奖者,品质非常出色。",
11861208 "Contest Winners": "比赛获胜者",
11871209 "These characters are the finalists of character design contests and have remarkable quality.": "这些角色都是角色设计大赛的入围作品,品质十分出色。",
11881210 "Featured Characters": "特色角色",
11891211 "Download Extensions & Assets": "下载扩展和资源菜单",
1212+ "Load a custom asset list or select": "加载自定义资产列表或选择",
1213+ "to install 3rd party extensions.": "安装第三方扩展。",
11901214 "Assets URL": "资产网址",
1215+ "load_asset_list_desc": "根据资产列表文件加载扩展和资产列表。\n\n此字段中的默认资产 URL 指向官方第一方扩展和资产列表。\n如果您有自定义资产列表,可以在此处插入。\n\n要安装单个第三方扩展,请使用右上角的“安装扩展”按钮。",
1216+ "Load an asset list": "加载资产列表",
1217+ "Load Asset List": "加载资产列表",
11911218 "Characters": "人物",
11921219 "Attach a File": "附加文件",
11931220 "Enter a URL or the ID of a Fandom wiki page to scrape:": "输入要抓取的 Fandom wiki 页面的 URL 或 ID:",
@@ -1271,6 +1298,7 @@
12711298 "Put images with expressions there. File names should follow the pattern:": "将带有表情的图像放在那里。文件名应遵循以下模式:",
12721299 "expression_label_pattern": "[表达式标签].[图像格式]",
12731300 "Sprite set:": "表情集:",
1301+ "Show Gallery": "展示图库",
12741302 "ext_sum_title": "总结",
12751303 "ext_sum_with": "总结如下:",
12761304 "ext_sum_main_api": "主要 API",
@@ -1315,9 +1343,14 @@
13151343 "ext_sum_injection_template": "插入模板",
13161344 "ext_sum_memory_template_placeholder": "{{summary}} 将解析当前摘要内容。",
13171345 "ext_sum_injection_position": "插入位置",
1346+ "ext_sum_include_wi_scan_desc": "在 WI 扫描中包括最新摘要。",
1347+ "ext_sum_include_wi_scan": "纳入世界信息扫描",
1348+ "None (not injected)": "无(未注入)",
1349+ "ext_sum_injection_position_none": "摘要不会被注入到提示中。您仍然可以通过 {{summary}} 宏访问它。",
13181350 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",
13191351 "Labels and Message": "标签和信息",
13201352 "Label": "标签",
1353+ "(label of the button, if no icon is chosen) ": "(如果没有选择图标,则为按钮的标签)",
13211354 "Title": "标题",
13221355 "(tooltip, leave empty to show message or /command)": "(工具提示,留空以显示消息或/命令)",
13231356 "Message / Command:": "消息/命令:",
@@ -1348,6 +1381,8 @@
13481381 "Inject user input automatically": "自动注入用户输入",
13491382 "(if disabled, use ": "(如果禁用,使用",
13501383 "macro for manual injection)": "宏用于手动注入)",
1384+ "Color": "颜色",
1385+ "Only apply color as accent": "仅应用颜色作为强调",
13511386 "ext_regex_title": "正则",
13521387 "ext_regex_new_global_script_desc": "新的全局正则表达式脚本",
13531388 "ext_regex_new_global_script": "新建全局正则",
@@ -1361,7 +1396,7 @@
13611396 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
13621397 "Regex Editor": "正则表达式编辑器",
13631398 "Test Mode": "测试模式",
13641399 "ext_regex_desc": "Regex 是一款使用正则表达式查找正则是一款使用正则表达式查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的 ?。",
13651400 "Input": "输入",
13661401 "ext_regex_test_input_placeholder": "在此输入...",
13671402 "Output": "输出",
@@ -1396,6 +1431,7 @@
13961431 "ext_regex_export_script": "导出脚本",
13971432 "ext_regex_delete_script": "删除脚本",
13981433 "Trigger Stable Diffusion": "触发Stable Diffusion",
1434+ "Abort current image generation task": "中止当前图像生成",
13991435 "sd_Yourself": "你自己",
14001436 "sd_Your_Face": "你的脸",
14011437 "sd_Me": "我",
@@ -1445,6 +1481,10 @@
14451481 "Delete workflow": "删除工作流",
14461482 "Enhance": "提高",
14471483 "Refine": "优化",
1484+ "API Key": "API 密钥",
1485+ "Click to set": "点击设置",
1486+ "You can find your API key in the Stability AI dashboard.": "您可以在 Stability AI 仪表板中找到您的 API 密钥。",
1487+ "Style Preset": "风格预设",
14481488 "Sampling method": "采样方法",
14491489 "Scheduler": "调度器",
14501490 "Resolution": "分辨率",
@@ -1545,6 +1585,10 @@
15451585 "Only used when Main API is selected.": "仅在选择主 API 时使用。",
15461586 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",
15471587 "View Stats": "查看统计数据",
1588+ "Title/Memo": "标题/备忘录",
1589+ "Status": "状态",
1590+ "Position": "位置",
1591+ "Trigger %": "触发率 %",
15481592 "Manager Users": "管理用户",
15491593 "New User": "新用户",
15501594 "Status:": "地位:",
@@ -1564,12 +1608,10 @@
15641608 "New Tags": "新标签",
15651609 "Folder Tags": "文件夹标签",
15661610 "The following tags will be auto-imported based on the currently selected folders": "根据当前选定的文件夹将自动导入以下标签",
1567- "Remember my choice": "记住我的选择",
1568- "Remember the chosen import option If anything besides 'Cancel' is selected, this dialog will not show up anymore. To change this, go to the settings and modify \"Tag Import Option\". If the \"Import\" option is chosen, the global setting will stay on \"Ask\".": "记住所选的导入选项\n如果选择了“取消”以外的任何选项,此对话框将不再显示。\n要更改此设置,请转到设置并修改“标签导入选项”。\n\n如果选择了“导入”选项,则全局设置将保留为“询问”。",
15691611 "Import None": "不导入",
15701612 "Import All": "全部导入",
15711613 "Import Existing": "导入现有",
15721614 "Import tags button": "导入",
15731615 "Include Body Parameters": "包括主体参数",
15741616 "custom_include_body_desc": "聊天完成请求主体中要包含的参数(YAML 对象)\n\n示例:\n- top_k:20\n- repetition_penalty:1.1",
15751617 "Exclude Body Parameters": "排除主体参数",
@@ -1671,6 +1713,9 @@
16711713 "char_import_8": "RisuRealm 角色(直链)",
16721714 "Supports importing multiple characters.": "支持导入多个角色。",
16731715 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
1716+ "Show Raw Prompt": "显示原始提示",
1717+ "Copy Prompt": "复制提示",
1718+ "Show Prompt Differences": "显示提示差异",
16741719 "System-wide Replacement Macros (in order of evaluation):": "系统范围的替换宏(按评估顺序):",
16751720 "help_macros_1": "仅适用于斜线命令批处理。替换为上一个命令的返回结果。",
16761721 "help_macros_2": "仅插入一个换行符。",
@@ -1687,6 +1732,7 @@
16871732 "help_macros_13": "角色对话示例",
16881733 "help_macros_14": "未格式化的对话示例",
16891734 "(only for Story String)": "(仅适用于故事字符串)",
1735+ "help_macros_summary": "“Summarize”扩展生成的最新聊天摘要(如果有)。",
16901736 "help_macros_15": "您当前的 Persona 用户名",
16911737 "help_macros_16": "角色的名字",
16921738 "help_macros_17": "角色的版本号",
@@ -1700,6 +1746,7 @@
17001746 "help_macros_22": "上下文中包含的第一条消息的 ID。要求在当前会话中至少运行一次生成。",
17011747 "help_macros_23": "最后一条聊天消息中当前滑动的 ID(以 1 为基数)。如果最后一条消息是用户或提示隐藏的,则为空字符串。",
17021748 "help_macros_24": "最后一条聊天消息中的滑动次数。如果最后一条消息是用户隐藏或提示隐藏的,则为空字符串。",
1749+ "help_macros_reverse": "反转宏的内容。",
17031750 "help_macros_25": "您可以在此处留言,宏将被替换为空白内容。AI 看不到。",
17041751 "help_macros_26": "当前时间",
17051752 "help_macros_27": "当前日期",
@@ -1738,6 +1785,8 @@
17381785 "help_macros_56": "指示系统指令前缀",
17391786 "help_macros_57": "指示第一个用户消息填充器",
17401787 "help_macros_58": "指示停止顺序",
1788+ "help_macros_first_user": "指示用户第一个输入序列",
1789+ "help_macros_last_user": "指示用户最后输入序列",
17411790 "Chat variables Macros:": "聊天变量宏:",
17421791 "Local variables = unique to the current chat": "局部变量 = 当前聊天所独有",
17431792 "Global variables = works in any chat for any character": "全局变量 = 适用于任何角色的任何聊天",
@@ -1769,10 +1818,22 @@
17691818 "prompt_manager_tokens": "词符",
17701819 "Are you sure you want to reset your settings to factory defaults?": "您确定要将设置重置为出厂默认设置吗?",
17711820 "Don't forget to save a snapshot of your settings before proceeding.": "在继续之前,不要忘记保存您的设置快照。",
1821+ "Chat Scenario Override": "聊天场景覆盖",
1822+ "Remove": "移除",
17721823 "Settings Snapshots": "设置快照",
17731824 "Record a snapshot of your current settings.": "记录当前设置的快照。",
17741825 "Make a Snapshot": "制作快照",
17751826 "Restore this snapshot": "恢复此快照",
1827+ "Download Model": "下载模型",
1828+ "Downloader Options": "下载器选项",
1829+ "Extra parameters for downloading/HuggingFace API": "下载/HuggingFace API 的额外参数。如果不确定,请将其留空。",
1830+ "Revision": "修订",
1831+ "Folder Name": "输出文件夹名称",
1832+ "HF Token": "HF代币",
1833+ "Include Patterns": "包含模式",
1834+ "Glob patterns of files to include in the download.": "要包含在下载中的文件的全局模式。每个模式用换行符分隔。",
1835+ "Exclude Patterns": "排除模式",
1836+ "Glob patterns of files to exclude in the download.": "下载中要排除的文件的 Glob 模式。每个模式用换行符分隔。",
17761837 "Hi,": "嘿,",
17771838 "To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器",
17781839 "set to true in the config.yaml file.": "在 config.yaml 文件中设置为 true。",
public/locales/zh-tw.json+2 -2
@@ -208,7 +208,7 @@
208208 "JSON Schema": "JSON 結構",
209209 "Type in the desired JSON schema": "輸入所需的 JSON 結構",
210210 "Grammar String": "語法字串",
211211 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNFENBFEBNF,取決於所使用的後端。如果您使用此功能,應該知道是哪一種",
212212 "Top P & Min P": "Top P 和 Min P",
213213 "Load default order": "載入預設順序",
214214 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。",
@@ -217,7 +217,7 @@
217217 "Character Names Behavior": "角色人物名稱行為",
218218 "Helps the model to associate messages with characters.": "幫助模型將訊息與角色人物關聯起來。",
219219 "None": "無",
220220 "character_names_nonecharacter_names_default": "除了團體和過去的玩家角色人物外。否則,請確保在提示中提供名字。",
221221 "Don't add character names.": "不要新增角色人物名稱",
222222 "Completion": "補充",
223223 "character_names_completion": "字元限制:僅限拉丁字母數字和底線。不適用於所有來源,特別是:Claude、MistralAI、Google。",
public/script.js+187 -173
@@ -156,6 +156,7 @@ import {
156156 ensureImageFormatSupported,
157157 flashHighlight,
158158 isTrueBoolean,
159+ toggleDrawer,
159160} from './scripts/utils.js';
160161import { debounce_timeout } from './scripts/constants.js';
161162
@@ -224,10 +225,10 @@ import {
224225import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
225226import { hideLoader, showLoader } from './scripts/loader.js';
226227import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
227228import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels } from './scripts/textgen-models.js';
228229import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
229230import { initPresetManager } from './scripts/preset-manager.js';
230231import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros.js';
231232import { currentUser, setUserControls } from './scripts/user.js';
232233import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
233234import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
@@ -424,6 +425,8 @@ export const event_types = {
424425 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
425426 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
426427 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',
427430 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
428431 WORLDINFO_UPDATED: 'worldinfo_updated',
429432 CHARACTER_EDITED: 'character_edited',
@@ -439,6 +442,7 @@ export const event_types = {
439442 GROUP_CHAT_CREATED: 'group_chat_created',
440443 GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
441444 GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
445+ GENERATE_AFTER_DATA: 'generate_after_data',
442446 GROUP_MEMBER_DRAFTED: 'group_member_drafted',
443447 WORLD_INFO_ACTIVATED: 'world_info_activated',
444448 TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
@@ -455,6 +459,7 @@ export const event_types = {
455459 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
456460 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
457461 ONLINE_STATUS_CHANGED: 'online_status_changed',
462+ IMAGE_SWIPED: 'image_swiped',
458463};
459464
460465export const eventSource = new EventEmitter();
@@ -570,6 +575,7 @@ export const system_message_types = {
570575 * @enum {number} Extension prompt types
571576 */
572577export const extension_prompt_types = {
578+ NONE: -1,
573579 IN_PROMPT: 0,
574580 IN_CHAT: 1,
575581 BEFORE_PROMPT: 2,
@@ -909,6 +915,7 @@ async function firstLoadInit() {
909915 await readSecretState();
910916 initLocales();
911917 initDefaultSlashCommands();
918+ initTextGenModels();
912919 await getSystemMessages();
913920 sendSystemMessage(system_message_types.WELCOME);
914921 await getSettings();
@@ -929,10 +936,17 @@ async function firstLoadInit() {
929936 initCfg();
930937 initLogprobs();
931938 doDailyExtensionUpdatesCheck();
932939 await hideLoader();
940+ await fixViewport();
933941 await eventSource.emit(event_types.APP_READY);
934942}
935943
944+async function fixViewport() {
945+ document.body.style.position = 'absolute';
946+ await delay(1);
947+ document.body.style.position = '';
948+}
949+
936950function cancelStatusCheck() {
937951 abortStatusCheck?.abort();
938952 abortStatusCheck = new AbortController();
@@ -1712,16 +1726,24 @@ export async function replaceCurrentChat() {
17121726}
17131727
17141728export function showMoreMessages() {
17151729 letconst messageIdfirstDisplayedMesId = Number($('#chat').children('.mes').first().attr('mesid'));
1730+ let messageId = Number(firstDisplayedMesId);
17161731 let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
17171732
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+
17181739 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
17191740 const prevHeight = $('#chat').prop('scrollHeight');
17201741
17211742 while (messageId > 0 && count > 0) {
1743+ let newMessageId = messageId - 1;
1744+ addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId });
17221745 count--;
17231746 messageId--;
1724- addOneMessage(chat[messageId], { insertBefore: messageId + 1, scroll: false, forceId: messageId });
17251747 }
17261748
17271749 if (messageId == 0) {
@@ -2091,6 +2113,7 @@ export function updateMessageBlock(messageId, message) {
20912113export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
20922114 // Add image to message
20932115 if (mes.extra?.image) {
2116+ const container = messageElement.find('.mes_img_container');
20942117 const chatHeight = $('#chat').prop('scrollHeight');
20952118 const image = messageElement.find('.mes_img');
20962119 const text = messageElement.find('.mes_text');
@@ -2106,9 +2129,27 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
21062129 });
21072130 image.attr('src', mes.extra?.image);
21082131 image.attr('title', mes.extra?.title || mes.title || '');
21092132 messageElement.find('.mes_img_container')container.addClass('img_extra');
21102133 image.toggleClass('img_inline', isInline);
21112134 text.toggleClass('displayNone', !isInline);
2135+
2136+ const imageSwipes = mes.extra.image_swipes;
2137+ if (Array.isArray(imageSwipes) && imageSwipes.length > 0) {
2138+ container.addClass('img_swipes');
2139+ const counter = container.find('.mes_img_swipe_counter');
2140+ const currentImage = imageSwipes.indexOf(mes.extra.image) + 1;
2141+ counter.text(`${currentImage}/${imageSwipes.length}`);
2142+
2143+ const swipeLeft = container.find('.mes_img_swipe_left');
2144+ swipeLeft.off('click').on('click', function () {
2145+ eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'left' });
2146+ });
2147+
2148+ const swipeRight = container.find('.mes_img_swipe_right');
2149+ swipeRight.off('click').on('click', function () {
2150+ eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' });
2151+ });
2152+ }
21122153 }
21132154
21142155 // Add file to message
@@ -2462,9 +2503,12 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
24622503 * @returns {string[]} Array of stopping strings
24632504 */
24642505export function getStoppingStrings(isImpersonate, isContinue) {
2506+ const result = [];
2507+
2508+ if (power_user.context.names_as_stop_strings) {
24652509 const charString = `\n${name2}:`;
24662510 const userString = `\n${name1}:`;
24672511 const result = .push(isImpersonate ? [charString] : [userString]);
24682512
24692513 result.push(userString);
24702514
@@ -2472,8 +2516,8 @@ export function getStoppingStrings(isImpersonate, isContinue) {
24722516 result.push(charString);
24732517 }
24742518
2475- // Add other group members as the stopping strings
2519+ // 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)
24762520 if (selected_group && (name2 || isImpersonate)) {
24772521 const group = groups.find(x => x.id === selected_group);
24782522
24792523 if (group && Array.isArray(group.members)) {
@@ -2484,6 +2528,7 @@ export function getStoppingStrings(isImpersonate, isContinue) {
24842528 result.push(...names);
24852529 }
24862530 }
2531+ }
24872532
24882533 result.push(...getInstructStoppingSequences());
24892534 result.push(...getCustomStoppingStrings());
@@ -2800,6 +2845,8 @@ class StreamingProcessor {
28002845 this.messageDom = null;
28012846 this.messageTextDom = null;
28022847 this.messageTimerDom = null;
2848+ this.messageTokenCounterDom = null;
2849+ /** @type {HTMLTextAreaElement} */
28032850 this.sendTextarea = document.querySelector('#send_textarea');
28042851 this.type = type;
28052852 this.force_name2 = force_name2;
@@ -2815,6 +2862,15 @@ class StreamingProcessor {
28152862 this.messageLogprobs = [];
28162863 }
28172864
2865+ #checkDomElements(messageId) {
2866+ if (this.messageDom === null || this.messageTextDom === null) {
2867+ this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
2868+ this.messageTextDom = this.messageDom?.querySelector('.mes_text');
2869+ this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
2870+ this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
2871+ }
2872+ }
2873+
28182874 showMessageButtons(messageId) {
28192875 if (messageId == -1) {
28202876 return;
@@ -2843,9 +2899,7 @@ class StreamingProcessor {
28432899 else {
28442900 await saveReply(this.type, text, true);
28452901 messageId = chat.length - 1;
2846- this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
2902+ this.#checkDomElements(messageId);
2847- this.messageTextDom = this.messageDom.querySelector('.mes_text');
2848- this.messageTimerDom = this.messageDom.querySelector('.mes_timer');
28492903 this.showMessageButtons(messageId);
28502904 }
28512905
@@ -2881,9 +2935,10 @@ class StreamingProcessor {
28812935 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
28822936 }
28832937 else {
2884- let currentTime = new Date();
2938+ this.#checkDomElements(messageId);
2939+ const currentTime = new Date();
28852940 // Don't waste time calculating token count for streaming
28862941 letconst currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
28872942 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
28882943 chat[messageId]['mes'] = processedText;
28892944 chat[messageId]['gen_started'] = this.timeStarted;
@@ -2895,8 +2950,9 @@ class StreamingProcessor {
28952950 }
28962951
28972952 chat[messageId]['extra']['token_count'] = currentTokenCount;
2898- const tokenCounter = $(`#chat .mes[mesid="${messageId}"] .tokenCounterDisplay`);
2953+ if (this.messageTokenCounterDom instanceof HTMLElement) {
28992954 tokenCounter this.text(messageTokenCounterDom.textContent = `${currentTokenCount}t`);
2955+ }
29002956 }
29012957
29022958 if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId]['swipes'])) {
@@ -2904,16 +2960,20 @@ class StreamingProcessor {
29042960 chat[messageId]['swipe_info'][chat[messageId]['swipe_id']] = { 'send_date': chat[messageId]['send_date'], 'gen_started': chat[messageId]['gen_started'], 'gen_finished': chat[messageId]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])) };
29052961 }
29062962
29072963 letconst formattedText = messageFormatting(
29082964 processedText,
29092965 chat[messageId].name,
29102966 chat[messageId].is_system,
29112967 chat[messageId].is_user,
29122968 messageId,
29132969 );
2970+ if (this.messageTextDom instanceof HTMLElement) {
29142971 this.messageTextDom.innerHTML = formattedText;
2972+ }
2973+ if (this.messageTimerDom instanceof HTMLElement) {
29152974 this.messageTimerDom.textContent = timePassed.timerValue;
29162975 this.messageTimerDom.title = timePassed.timerTitle;
2976+ }
29172977 this.setFirstSwipe(messageId);
29182978 }
29192979
@@ -3200,6 +3260,23 @@ function restoreResponseLength(api, responseLength) {
32003260}
32013261
32023262/**
3263+ * Removes last message from the chat DOM.
3264+ * @returns {Promise<void>} Resolves when the message is removed.
3265+ */
3266+function removeLastMessage() {
3267+ return new Promise((resolve) => {
3268+ const lastMes = $('#chat').children('.mes').last();
3269+ if (lastMes.length === 0) {
3270+ return resolve();
3271+ }
3272+ lastMes.hide(animation_duration, function () {
3273+ $(this).remove();
3274+ resolve();
3275+ });
3276+ });
3277+}
3278+
3279+/**
32033280 * Runs a generation using the current chat context.
32043281 * @param {string} type Generation type
32053282 * @param {GenerateOptions} options Generation options
@@ -3331,9 +3408,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
33313408 }
33323409 else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && chat.length) {
33333410 chat.length = chat.length - 1;
3334- $('#chat').children().last().hide(250, function () {
3411+ await removeLastMessage();
3335- $(this).remove();
3336- });
33373412 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
33383413 }
33393414 }
@@ -3581,6 +3656,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
35813656 let chat2 = [];
35823657 let continue_mag = '';
35833658 const userMessageIndices = [];
3659+ const lastUserMessageIndex = coreChat.findLastIndex(x => x.is_user);
35843660
35853661 for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) {
35863662 if (main_api == 'openai') {
@@ -3599,6 +3675,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
35993675 chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST);
36003676 }
36013677
3678+ if (lastUserMessageIndex >= 0 && j === lastUserMessageIndex && isInstruct) {
3679+ // Reformat with the last input sequence (if any)
3680+ chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST);
3681+ }
3682+
36023683 // Do not suffix the message for continuation
36033684 if (i === 0 && isContinue) {
36043685 if (isInstruct) {
@@ -3624,7 +3705,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
36243705 mes: power_user.instruct.user_alignment_message,
36253706 is_user: true,
36263707 };
36273708 userAlignmentMessage = formatMessageHistoryItem(alignmentMessage, isInstruct, falseforce_output_sequence.FIRST);
36283709 }
36293710
36303711 // Call combined AN into Generate
@@ -3886,7 +3967,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38863967
38873968 // Get instruct mode line
38883969 if (isInstruct && !isContinue) {
38893970 const name = (quiet_prompt && !quietToLoud && !isImpersonate) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
38903971 const isQuiet = quiet_prompt && type == 'quiet';
38913972 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);
38923973 }
@@ -4167,6 +4248,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
41674248 }
41684249 }
41694250
4251+ await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data);
4252+
41704253 if (dryRun) {
41714254 generatedPromptCache = '';
41724255 return Promise.resolve();
@@ -4698,6 +4781,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
46984781 await eventSource.emit(event_types.MESSAGE_SENT, chat_id);
46994782 addOneMessage(message);
47004783 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
4784+ await saveChatConditional();
47014785 }
47024786}
47034787
@@ -5029,7 +5113,7 @@ function setInContextMessages(lastmsg, type) {
50295113 * @param {object} data Generation data
50305114 * @returns {Promise<object>} Response data from the API
50315115 */
50325116export async function sendGenerationRequest(type, data) {
50335117 if (main_api === 'openai') {
50345118 return await sendOpenAIRequest(type, data.prompt, abortController.signal);
50355119 }
@@ -5061,7 +5145,7 @@ async function sendGenerationRequest(type, data) {
50615145 * @param {object} data Generation data
50625146 * @returns {Promise<any>} Streaming generator
50635147 */
50645148export async function sendStreamingRequest(type, data) {
50655149 if (abortController?.signal?.aborted) {
50665150 throw new Error('Generation was aborted.');
50675151 }
@@ -5342,9 +5426,11 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
53425426 getMessage = fixMarkdown(getMessage, false);
53435427 }
53445428
53455429 const nameToTrim2 = isImpersonate ? name1 : name2;
5430+ ? (!power_user.allow_name1_display ? name1 : '')
5431+ : (!power_user.allow_name2_display ? name2 : '');
53465432
53475433 if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) {
53485434 getMessage = getMessage.replace(nameToTrim2 + ':', '');
53495435 getMessage = getMessage.trimStart();
53505436 }
@@ -5565,6 +5651,7 @@ export function activateSendButtons() {
55655651 is_send_press = false;
55665652 $('#send_but').removeClass('displayNone');
55675653 $('#mes_continue').removeClass('displayNone');
5654+ $('#mes_impersonate').removeClass('displayNone');
55685655 $('.mes_buttons:last').show();
55695656 hideStopButton();
55705657}
@@ -5572,6 +5659,7 @@ export function activateSendButtons() {
55725659export function deactivateSendButtons() {
55735660 $('#send_but').addClass('displayNone');
55745661 $('#mes_continue').addClass('displayNone');
5662+ $('#mes_impersonate').addClass('displayNone');
55755663 showStopButton();
55765664}
55775665
@@ -6022,9 +6110,10 @@ async function getChatResult() {
60226110 const message = getFirstMessage();
60236111 if (message.mes) {
60246112 chat.push(message);
6025- await saveChatConditional();
60266113 freshChat = true;
60276114 }
6115+ // Make sure the chat appears on the server
6116+ await saveChatConditional();
60286117 }
60296118 await loadItemizedPrompts(getCurrentChatId());
60306119 await printMessages();
@@ -6076,7 +6165,7 @@ export async function openCharacterChat(file_name) {
60766165 chat_metadata = {};
60776166 await getChat();
60786167 $('#selected_chat_pole').val(file_name);
60796168 await createOrEditCharacter(new CustomEvent('newChat'));
60806169}
60816170
60826171////////// OPTIMZED MAIN API CHANGE FUNCTION ////////////
@@ -6361,7 +6450,7 @@ export async function getSettings() {
63616450 loadHordeSettings(settings);
63626451
63636452 // Load power user settings
63646453 await loadPowerUserSettings(settings, data);
63656454
63666455 // Load character tags
63676456 loadTagsSettings(settings);
@@ -6778,6 +6867,11 @@ export async function displayPastChats() {
67786867 const fileName = chat['file_name'];
67796868 const chatContent = rawChats[fileName];
67806869
6870+ // Make sure empty chats are displayed when there is no search query
6871+ if (Array.isArray(chatContent) && !chatContent.length && !searchQuery) {
6872+ return true;
6873+ }
6874+
67816875 // // Uncomment this to return to old behavior (classical full-substring search).
67826876 // return chatContent && Object.values(chatContent).some(message => message?.mes?.toLowerCase()?.includes(searchQuery.toLowerCase()));
67836877
@@ -7866,6 +7960,8 @@ window['SillyTavern'].getContext = function () {
78667960 eventTypes: event_types,
78677961 addOneMessage: addOneMessage,
78687962 generate: Generate,
7963+ sendStreamingRequest: sendStreamingRequest,
7964+ sendGenerationRequest: sendGenerationRequest,
78697965 stopGeneration: stopGeneration,
78707966 getTokenCount: getTokenCount,
78717967 extensionPrompts: extension_prompts,
@@ -8259,6 +8355,7 @@ const swipe_right = () => {
82598355};
82608356
82618357const CONNECT_API_MAP = {
8358+ // Default APIs not contined inside text gen / chat gen
82628359 'kobold': {
82638360 selected: 'kobold',
82648361 button: '#api_button',
@@ -8270,147 +8367,48 @@ const CONNECT_API_MAP = {
82708367 selected: 'novel',
82718368 button: '#api_button_novel',
82728369 },
8273- 'ooba': {
8370+ // KoboldCpp alias
8274- selected: 'textgenerationwebui',
8275- button: '#api_button_textgenerationwebui',
8276- type: textgen_types.OOBA,
8277- },
8278- 'tabby': {
8279- selected: 'textgenerationwebui',
8280- button: '#api_button_textgenerationwebui',
8281- type: textgen_types.TABBY,
8282- },
8283- 'llamacpp': {
8284- selected: 'textgenerationwebui',
8285- button: '#api_button_textgenerationwebui',
8286- type: textgen_types.LLAMACPP,
8287- },
8288- 'ollama': {
8289- selected: 'textgenerationwebui',
8290- button: '#api_button_textgenerationwebui',
8291- type: textgen_types.OLLAMA,
8292- },
8293- 'mancer': {
8294- selected: 'textgenerationwebui',
8295- button: '#api_button_textgenerationwebui',
8296- type: textgen_types.MANCER,
8297- },
8298- 'vllm': {
8299- selected: 'textgenerationwebui',
8300- button: '#api_button_textgenerationwebui',
8301- type: textgen_types.VLLM,
8302- },
8303- 'aphrodite': {
8304- selected: 'textgenerationwebui',
8305- button: '#api_button_textgenerationwebui',
8306- type: textgen_types.APHRODITE,
8307- },
8308- 'koboldcpp': {
8309- selected: 'textgenerationwebui',
8310- button: '#api_button_textgenerationwebui',
8311- type: textgen_types.KOBOLDCPP,
8312- },
83138371 'kcpp': {
83148372 selected: 'textgenerationwebui',
83158373 button: '#api_button_textgenerationwebui',
83168374 type: textgen_types.KOBOLDCPP,
83178375 },
8318- 'togetherai': {
8376+ // OpenAI alias
8319- selected: 'textgenerationwebui',
8320- button: '#api_button_textgenerationwebui',
8321- type: textgen_types.TOGETHERAI,
8322- },
8323- 'openai': {
8324- selected: 'openai',
8325- button: '#api_button_openai',
8326- source: chat_completion_sources.OPENAI,
8327- },
83288377 'oai': {
83298378 selected: 'openai',
83308379 button: '#api_button_openai',
83318380 source: chat_completion_sources.OPENAI,
83328381 },
8333- 'claude': {
8382+ // OpenRouter special naming, to differentiate between chat comp and text comp
8334- selected: 'openai',
8335- button: '#api_button_openai',
8336- source: chat_completion_sources.CLAUDE,
8337- },
8338- 'windowai': {
8339- selected: 'openai',
8340- button: '#api_button_openai',
8341- source: chat_completion_sources.WINDOWAI,
8342- },
83438383 'openrouter': {
83448384 selected: 'openai',
83458385 button: '#api_button_openai',
83468386 source: chat_completion_sources.OPENROUTER,
83478387 },
8348- 'scale': {
8349- selected: 'openai',
8350- button: '#api_button_openai',
8351- source: chat_completion_sources.SCALE,
8352- },
8353- 'ai21': {
8354- selected: 'openai',
8355- button: '#api_button_openai',
8356- source: chat_completion_sources.AI21,
8357- },
8358- 'makersuite': {
8359- selected: 'openai',
8360- button: '#api_button_openai',
8361- source: chat_completion_sources.MAKERSUITE,
8362- },
8363- 'mistralai': {
8364- selected: 'openai',
8365- button: '#api_button_openai',
8366- source: chat_completion_sources.MISTRALAI,
8367- },
8368- 'custom': {
8369- selected: 'openai',
8370- button: '#api_button_openai',
8371- source: chat_completion_sources.CUSTOM,
8372- },
8373- 'cohere': {
8374- selected: 'openai',
8375- button: '#api_button_openai',
8376- source: chat_completion_sources.COHERE,
8377- },
8378- 'perplexity': {
8379- selected: 'openai',
8380- button: '#api_button_openai',
8381- source: chat_completion_sources.PERPLEXITY,
8382- },
8383- 'groq': {
8384- selected: 'openai',
8385- button: '#api_button_openai',
8386- source: chat_completion_sources.GROQ,
8387- },
8388- '01ai': {
8389- selected: 'openai',
8390- button: '#api_button_openai',
8391- source: chat_completion_sources.ZEROONEAI,
8392- },
8393- 'infermaticai': {
8394- selected: 'textgenerationwebui',
8395- button: '#api_button_textgenerationwebui',
8396- type: textgen_types.INFERMATICAI,
8397- },
8398- 'dreamgen': {
8399- selected: 'textgenerationwebui',
8400- button: '#api_button_textgenerationwebui',
8401- type: textgen_types.DREAMGEN,
8402- },
84038388 'openrouter-text': {
84048389 selected: 'textgenerationwebui',
84058390 button: '#api_button_textgenerationwebui',
84068391 type: textgen_types.OPENROUTER,
84078392 },
8408- 'huggingface': {
8393+};
8394+
8395+// Fill connections map from textgen_types and chat_completion_sources
8396+for (const textGenType of Object.values(textgen_types)) {
8397+ if (CONNECT_API_MAP[textGenType]) continue;
8398+ CONNECT_API_MAP[textGenType] = {
84098399 selected: 'textgenerationwebui',
84108400 button: '#api_button_textgenerationwebui',
84118401 type: textgen_types.HUGGINGFACEtextGenType,
8412- },
84138402 };
8403+}
8404+for (const chatCompletionSource of Object.values(chat_completion_sources)) {
8405+ if (CONNECT_API_MAP[chatCompletionSource]) continue;
8406+ CONNECT_API_MAP[chatCompletionSource] = {
8407+ selected: 'openai',
8408+ button: '#api_button_openai',
8409+ source: chatCompletionSource,
8410+ };
8411+}
84148412
84158413async function selectContextCallback(_, name) {
84168414 if (!name) {
@@ -8543,7 +8541,7 @@ export async function processDroppedFiles(files, data = new Map()) {
85438541
85448542 for (const file of files) {
85458543 const extension = file.name.split('.').pop().toLowerCase();
85468544 if (allowedMimeTypes.includessome(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) {
85478545 const preservedName = data instanceof Map && data.get(file);
85488546 await importCharacter(file, preservedName);
85498547 } else {
@@ -9142,14 +9140,14 @@ jQuery(async function () {
91429140 $('#send_textarea').on('focusin focus click', () => {
91439141 S_TAPreviouslyFocused = true;
91449142 });
91459143 $('#send_but, #option_regenerate, #option_continue, #mes_continue, #mes_impersonate').on('click', () => {
91469144 if (S_TAPreviouslyFocused) {
91479145 $('#send_textarea').focus();
91489146 }
91499147 });
91509148 $(document).click(event => {
91519149 if ($(':focus').attr('id') !== 'send_textarea') {
91529150 var validIDs = ['options_button', 'send_but', 'mes_impersonate', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];
91539151 if (!validIDs.includes($(event.target).attr('id'))) {
91549152 S_TAPreviouslyFocused = false;
91559153 }
@@ -9185,6 +9183,9 @@ jQuery(async function () {
91859183 debouncedCharacterSearch(searchQuery);
91869184 });
91879185
9186+ $('#mes_impersonate').on('click', function () {
9187+ $('#option_impersonate').trigger('click');
9188+ });
91889189
91899190 $('#mes_continue').on('click', function () {
91909191 $('#option_continue').trigger('click');
@@ -9806,8 +9807,8 @@ jQuery(async function () {
98069807 hideMenu();
98079808 });
98089809
98099810 $('#newChatFromManageScreenButton').on('click', async function () {
98109811 await doNewChat({ deleteCurrentChat: false });
98119812 $('#select_chat_cross').trigger('click');
98129813 });
98139814
@@ -10482,8 +10483,9 @@ jQuery(async function () {
1048210483 }
1048310484
1048410485 // Set the height of "autoSetHeight" textareas within the drawer to their scroll height
1048510486 $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {
1048610487 await resetScrollHeight($(this));
10488+ return;
1048710489 });
1048810490
1048910491 } else if (drawerWasOpenAlready) { //to close manually
@@ -10556,8 +10558,9 @@ jQuery(async function () {
1055610558 $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle();
1055710559
1055810560 // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height
1055910561 $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(async function () {
1056010562 await resetScrollHeight($(this));
10563+ return;
1056110564 });
1056210565 });
1056310566
@@ -10638,12 +10641,19 @@ jQuery(async function () {
1063810641 }
1063910642 });
1064010643
1064110644 $(document).onaddEventListener('click', '#OpenAllWIEntries', function (e) {
10642- $('#world_popup_entries_list').children().find('.down').click();
10645+ if (!(e.target instanceof HTMLElement)) return;
10646+ if (e.target.matches('#OpenAllWIEntries')) {
10647+ document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10648+ toggleDrawer(drawer, true);
10649+ });
10650+ } else if (e.target.matches('#CloseAllWIEntries')) {
10651+ document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10652+ toggleDrawer(drawer, false);
1064310653 });
10644- $(document).on('click', '#CloseAllWIEntries', function () {
10654+ }
10645- $('#world_popup_entries_list').children().find('.up').click();
1064610655 });
10656+
1064710657 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
1064810658 /* $('#set_character_world').on('click', openCharacterWorldPopup); */
1064910659
@@ -10738,7 +10748,7 @@ jQuery(async function () {
1073810748 }
1073910749 } break;
1074010750 case 'import_tags': {
1074110751 await importTags(characters[this_chid], { forceShowimportSetting: truetag_import_setting.ASK });
1074210752 } break;
1074310753 /*case 'delete_button':
1074410754 popup_type = "del_ch";
@@ -10767,15 +10777,17 @@ jQuery(async function () {
1076710777 var isManualInput = false;
1076810778 var valueBeforeManualInput;
1076910779
1077010780 $(document).on('input', '.range-block-counter input, .neo-range-input').on('click', function () {
1077110781 valueBeforeManualInput = $(this).val();
1077210782 console.log(valueBeforeManualInput);
1077310783 });
10774- .on('change', function (e) {
10784+
10785+ $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {
1077510786 e.target.focus();
1077610787 e.target.dispatchEvent(new EventKeyboardEvent('keyup', { bubbles: true }));
1077710788 });
10778- .on('keydown', function (e) {
10789+
10790+ $(document).on('keydown', '.range-block-counter input, .neo-range-input', function (e) {
1077910791 const masterSelector = '#' + $(this).data('for');
1078010792 const masterElement = $(masterSelector);
1078110793 if (e.key === 'Enter') {
@@ -10787,7 +10799,7 @@ jQuery(async function () {
1078710799 //newSlider.val(manualInput)
1078810800 //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
1078910801 valueBeforeManualInput = manualInput;
1079010802 $(masterElement).val($(this).val()).trigger('input', { forced: true });
1079110803 } else {
1079210804 //if value not ok, warn and reset to last known valid value
1079310805 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
@@ -10797,14 +10809,16 @@ jQuery(async function () {
1079710809 }
1079810810 }
1079910811 }
1080010812 });
10801- .on('keyup', function () {
10813+
10814+ $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {
1080210815 valueBeforeManualInput = $(this).val();
1080310816 console.log(valueBeforeManualInput);
1080410817 isManualInput = true;
1080510818 });
10819+
1080610820 //trigger slider changes when user clicks away
1080710821 $(document).on('mouseup blur', '.range-block-counter input, .neo-range-input', function () {
1080810822 const masterSelector = '#' + $(this).data('for');
1080910823 const masterElement = $(masterSelector);
1081010824 let manualInput = Number($(this).val());
@@ -10813,7 +10827,7 @@ jQuery(async function () {
1081310827 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
1081410828 valueBeforeManualInput = manualInput;
1081510829 //set the slider value to input value
1081610830 $(masterElement).val($(this).val()).trigger('input', { forced: true });
1081710831 } else {
1081810832 //if value not ok, warn and reset to last known valid value
1081910833 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
public/scripts/BulkEditOverlay.js+33 -3
@@ -18,7 +18,7 @@ import {
1818import { favsToHotswap } from './RossAscends-mods.js';
1919import { hideLoader, showLoader } from './loader.js';
2020import { convertCharacterToPersona } from './personas.js';
2121import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
2222
2323/**
2424 * Static object representing the actions of the
@@ -197,10 +197,10 @@ class BulkTagPopupHandler {
197197 #getHtml = () => {
198198 const characterData = JSON.stringify({ characterIds: this.characterIds });
199199 return `<div id="bulk_tag_shadow_popup">
200200 <div id="bulk_tag_popup" class="wider_dialogue_popup">
201201 <div id="bulk_tag_popup_holder">
202202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
203203 <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>
204204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
205205 <br>
206206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
@@ -219,6 +219,12 @@ class BulkTagPopupHandler {
219219 <i class="fa-solid fa-trash-can margin-right-10px"></i>
220220 Mutual
221221 </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>
222228 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
223229 </div>
224230 </div>
@@ -254,6 +260,30 @@ class BulkTagPopupHandler {
254260 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
255261 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
256262 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();
257287 }
258288
259289 /**
public/scripts/RossAscends-mods.js+12 -12
@@ -157,18 +157,15 @@ export function shouldSendOnEnter() {
157157//Does not break old characters/chats, as the code just uses whatever timestamp exists in the chat.
158158//New chats made with characters will use this new formatting.
159159export function humanizedDateTime() {
160160 letconst baseDatenow = new Date(Date.now());
161- let humanYear = baseDate.getFullYear();
161+ const dt = {
162- let humanMonth = baseDate.getMonth() + 1;
162+ year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
163- let humanDate = baseDate.getDate();
163+ hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
164- let humanHour = (baseDate.getHours() < 10 ? '0' : '') + baseDate.getHours();
164+ };
165- let humanMinute =
165+ for (const key in dt) {
166- (baseDate.getMinutes() < 10 ? '0' : '') + baseDate.getMinutes();
166+ dt[key] = dt[key].toString().padStart(2, '0');
167- let humanSecond =
167+ }
168- (baseDate.getSeconds() < 10 ? '0' : '') + baseDate.getSeconds();
168+ return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s`;
169- let HumanizedDateTime =
170- humanYear + '-' + humanMonth + '-' + humanDate + '@' + humanHour + 'h' + humanMinute + 'm' + humanSecond + 's';
171- return HumanizedDateTime;
172169}
173170
174171//this is a common format version to display a timestamp on each chat message
@@ -314,6 +311,7 @@ function RA_checkOnlineStatus() {
314311 $('#send_form').addClass('no-connection'); //entire input form area is red when not connected
315312 $('#send_but').addClass('displayNone'); //send button is hidden when not connected;
316313 $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
314+ $('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected;
317315 $('#API-status-top').removeClass('fa-plug');
318316 $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow');
319317 connection_made = false;
@@ -330,6 +328,7 @@ function RA_checkOnlineStatus() {
330328 if (!is_send_press && !(selected_group && is_group_generating)) {
331329 $('#send_but').removeClass('displayNone'); //on connect, send button shows
332330 $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected
331+ $('#mes_impersonate').removeClass('displayNone'); //continue button is shown when connected
333332 }
334333 }
335334 }
@@ -381,6 +380,7 @@ function RA_autoconnect(PrevApi) {
381380 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
382381 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
383382 || (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)
384384 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
385385 ) {
386386 $('#api_button_openai').trigger('click');
public/scripts/autocomplete/AutoComplete.js+63 -40
@@ -16,8 +16,15 @@ export const AUTOCOMPLETE_WIDTH = {
1616 'FULL': 2,
1717};
1818
19+/**@readonly*/
20+/**@enum {Number}*/
21+export const AUTOCOMPLETE_SELECT_KEY = {
22+ 'TAB': 1, // 2^0
23+ 'ENTER': 2, // 2^1
24+};
25+
1926export class AutoComplete {
2027 /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
2128 /**@type {boolean}*/ isFloating = false;
2229 /**@type {()=>boolean}*/ checkIfActivate;
2330 /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
@@ -56,6 +63,8 @@ export class AutoComplete {
5663 /**@type {function}*/ updateDetailsPositionDebounced;
5764 /**@type {function}*/ updateFloatingPositionDebounced;
5865
66+ /**@type {(item:AutoCompleteOption)=>any}*/ onSelect;
67+
5968 get matchType() {
6069 return power_user.stscript.matching ?? 'fuzzy';
6170 }
@@ -68,7 +77,7 @@ export class AutoComplete {
6877
6978
7079 /**
7180 * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete.
7281 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
7382 * @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.
7483 * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
@@ -102,10 +111,15 @@ export class AutoComplete {
102111 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
103112 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+ });
106118 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
107119 textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);{
108- textarea.addEventListener('selectionchange', ()=>this.show());
120+ this.selectionStart = this.textarea.selectionStart;
121+ if (this.isActive) this.show();
122+ });
109123 textarea.addEventListener('blur', ()=>this.hide());
110124 if (isFloating) {
111125 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
@@ -189,6 +203,11 @@ export class AutoComplete {
189203 * @returns The option.
190204 */
191205 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+ }
192211 const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
193212 let start = null;
194213 let consecutive = [];
@@ -339,7 +358,7 @@ export class AutoComplete {
339358
340359 this.result = this.effectiveParserResult.optionList
341360 // filter the list of options by the partial name according to the matching type
342361 .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)
343362 // remove aliases
344363 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
345364
@@ -357,10 +376,11 @@ export class AutoComplete {
357376 // build element
358377 option.dom = this.makeItem(option);
359378 // update replacer and add quotes if necessary
379+ const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name;
360380 if (this.effectiveParserResult.canBeQuoted) {
361381 option.replacer = option.nameoptionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionoptionName.namereplace(/"/g, '\\"')}"` : `${option.nameoptionName}`;
362382 } else {
363383 option.replacer = option.nameoptionName;
364384 }
365385 // calculate fuzzy score if matching is fuzzy
366386 if (this.matchType == 'fuzzy') this.fuzzyScore(option);
@@ -399,7 +419,7 @@ export class AutoComplete {
399419 ,
400420 );
401421 this.result.push(option);
402422 } else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) {
403423 // only one result that is exactly the current value? just show hint, no autocomplete
404424 this.isReplaceable = false;
405425 this.isShowingDetails = false;
@@ -439,11 +459,14 @@ export class AutoComplete {
439459 } else {
440460 item.dom.classList.remove('selected');
441461 }
462+ if (!item.isSelectable) {
463+ item.dom.classList.add('not-selectable');
464+ }
442465 frag.append(item.dom);
443466 }
444467 this.dom.append(frag);
445468 this.updatePosition();
446469 getTopmostModalLayerthis.getLayer().append(this.domWrap);
447470 } else {
448471 this.domWrap.remove();
449472 }
@@ -458,10 +481,17 @@ export class AutoComplete {
458481 if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
459482 this.detailsDom.innerHTML = '';
460483 this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
461484 getTopmostModalLayerthis.getLayer().append(this.detailsWrap);
462485 this.updateDetailsPositionDebounced();
463486 }
464487
488+ /**
489+ * @returns {HTMLElement} closest ancestor dialog or body
490+ */
491+ getLayer() {
492+ return this.textarea.closest('dialog, body');
493+ }
494+
465495
466496
467497 /**
@@ -474,7 +504,7 @@ export class AutoComplete {
474504 const rect = {};
475505 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
476506 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
477507 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayerthis.getLayer().getBoundingClientRect();
478508 this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
479509 this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
480510 this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
@@ -501,7 +531,7 @@ export class AutoComplete {
501531 const rect = {};
502532 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
503533 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
504534 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayerthis.getLayer().getBoundingClientRect();
505535 if (this.isReplaceable) {
506536 this.detailsWrap.classList.remove('full');
507537 const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
@@ -527,32 +557,34 @@ export class AutoComplete {
527557 updateFloatingPosition() {
528558 const location = this.getCursorPosition();
529559 const rect = this.textarea.getBoundingClientRect();
560+ const layerRect = this.getLayer().getBoundingClientRect();
530561 // cursor is out of view -> hide
531562 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
532563 return this.hide();
533564 }
534565 const left = Math.max(rect.left, location.left) - layerRect.left;
535566 this.domWrap.style.setProperty('--targetOffset', `${left}`);
536567 if (location.top <= window.innerHeight / 2) {
537568 // if cursor is in lower half of window, show list above line
538569 this.domWrap.style.top = `${location.bottom - layerRect.top}px`;
539570 this.domWrap.style.bottom = 'auto';
540571 this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
541572 } else {
542573 // if cursor is in upper half of window, show list below line
543574 this.domWrap.style.top = 'auto';
544575 this.domWrap.style.bottom = `calc(100vh${layerRect.height}px - ${location.top - layerRect.top}px)`;
545576 this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
546577 }
547578 }
548579
549580 updateFloatingDetailsPosition(location = null) {
550581 if (!location) location = this.getCursorPosition();
551582 const rect = this.textarea.getBoundingClientRect();
583+ const layerRect = this.getLayer().getBoundingClientRect();
552584 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
553585 return this.hide();
554586 }
555587 const left = Math.max(rect.left, location.left) - layerRect.left;
556588 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
557589 if (this.isReplaceable) {
558590 this.detailsWrap.classList.remove('full');
@@ -572,14 +604,14 @@ export class AutoComplete {
572604 }
573605 if (location.top <= window.innerHeight / 2) {
574606 // if cursor is in lower half of window, show list above line
575607 this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`;
576608 this.detailsWrap.style.bottom = 'auto';
577609 this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
578610 } else {
579611 // if cursor is in upper half of window, show list below line
580612 this.detailsWrap.style.top = 'auto';
581613 this.detailsWrap.style.bottom = `calc(100vh${layerRect.height}px - ${location.top - layerRect.top}px)`;
582614 this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
583615 }
584616 }
585617
@@ -597,7 +629,7 @@ export class AutoComplete {
597629 }
598630 this.clone.style.position = 'fixed';
599631 this.clone.style.visibility = 'hidden';
600632 getTopmostModalLayer()document.body.append(this.clone);
601633 const mo = new MutationObserver(muts=>{
602634 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
603635 this.clone.remove();
@@ -656,6 +688,7 @@ export class AutoComplete {
656688 }
657689 this.wasForced = false;
658690 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
691+ this.onSelect?.(this.selectedItem);
659692 }
660693
661694
@@ -708,8 +741,10 @@ export class AutoComplete {
708741 }
709742 case 'Enter': {
710743 // pick the selected item to autocomplete
744+ if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break;
711745 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
712746 if (this.selectedItem.name == this.name) break;
747+ if (!this.selectedItem.isSelectable) break;
713748 evt.preventDefault();
714749 evt.stopImmediatePropagation();
715750 this.select();
@@ -717,9 +752,11 @@ export class AutoComplete {
717752 }
718753 case 'Tab': {
719754 // pick the selected item to autocomplete
755+ if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break;
720756 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
721757 evt.preventDefault();
722758 evt.stopImmediatePropagation();
759+ if (!this.selectedItem.isSelectable) break;
723760 this.select();
724761 return;
725762 }
@@ -772,13 +809,7 @@ export class AutoComplete {
772809 // ignore keydown on modifier keys
773810 return;
774811 }
775- switch (evt.key) {
812+ // await keyup to see if cursor position or text has changed
776- case 'ArrowUp':
777- case 'ArrowDown':
778- case 'ArrowRight':
779- case 'ArrowLeft': {
780- if (this.isActive) {
781- // keyboard navigation, wait for keyup to complete cursor move
782813 const oldText = this.textarea.value;
783814 await new Promise(resolve=>{
784815 window.addEventListener('keyup', resolve, { once:true });
@@ -786,16 +817,8 @@ export class AutoComplete {
786817 if (this.selectionStart != this.textarea.selectionStart) {
787818 this.selectionStart = this.textarea.selectionStart;
788819 this.show(this.isReplaceable || oldText != this.textarea.value);
789- }
820+ } else if (this.isActive) {
790- }
791- break;
792- }
793- default: {
794- if (this.isActive) {
795821 this.text != this.textarea.value && this.show(this.isReplaceable);
796822 }
797- break;
798- }
799- }
800823 }
801824}
public/scripts/autocomplete/AutoCompleteNameResult.js+3 -30
@@ -1,36 +1,9 @@
11import { SlashCommandNamedArgumentAutoCompleteOptionAutoCompleteNameResultBase } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOptionAutoCompleteNameResultBase.js';
22import { AutoCompleteOptionAutoCompleteSecondaryNameResult } from './AutoCompleteOptionAutoCompleteSecondaryNameResult.js';
3-// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
43
54
65
76export 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-
347 /**
358 *
369 * @param {string} text The whole text
public/scripts/autocomplete/AutoCompleteNameResultBase.js+31 -0
@@ -0,0 +1,31 @@
1+import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
2+import { AutoCompleteOption } from './AutoCompleteOption.js';
3+
4+
5+
6+export 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 {
1111 /**@type {AutoCompleteFuzzyScore}*/ score;
1212 /**@type {string}*/ replacer;
1313 /**@type {HTMLElement}*/ dom;
14+ /**@type {(input:string)=>boolean}*/ matchProvider;
15+ /**@type {(input:string)=>string}*/ valueProvider;
16+ /**@type {boolean}*/ makeSelectable = false;
1417
1518
1619 /**
@@ -21,14 +24,21 @@ export class AutoCompleteOption {
2124 return this.name;
2225 }
2326
27+ get isSelectable() {
28+ return this.makeSelectable || !this.valueProvider;
29+ }
30+
2431
2532 /**
2633 * @param {string} name
2734 */
2835 constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) {
2936 this.name = name;
3037 this.typeIcon = typeIcon;
3138 this.type = type;
39+ this.matchProvider = matchProvider;
40+ this.valueProvider = valueProvider;
41+ this.makeSelectable = makeSelectable;
3242 }
3343
3444
public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js+2 -2
@@ -1,6 +1,6 @@
11import { AutoCompleteNameResultAutoCompleteNameResultBase } from './AutoCompleteNameResultAutoCompleteNameResultBase.js';
22
33export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultAutoCompleteNameResultBase {
44 /**@type {boolean}*/ isRequired = false;
55 /**@type {boolean}*/ forceMatch = true;
66}
public/scripts/bookmarks.js+2 -2
@@ -14,7 +14,7 @@ import {
1414 saveChatConditional,
1515 saveItemizedPrompts,
1616} from '../script.js';
1717import { humanizedDateTime, getMessageTimeStamp } from './RossAscends-mods.js';
1818import {
1919 getGroupPastChats,
2020 group_activation_strategy,
@@ -297,7 +297,7 @@ async function convertSoloToGroupChat() {
297297 if (groupChat.length === 0) {
298298 const newMessage = {
299299 ...system_messages[system_message_types.GROUP],
300300 send_date: humanizedDateTimegetMessageTimeStamp(),
301301 extra: { type: system_message_types.GROUP },
302302 };
303303 groupChat.push(newMessage);
public/scripts/extensions.js+23 -21
@@ -989,6 +989,28 @@ export async function writeExtensionField(characterId, key, value) {
989989 }
990990}
991991
992+/**
993+ * Prompts the user to enter the Git URL of the extension to import.
994+ * After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
995+ * If the extension is imported successfully, a success message is displayed.
996+ * If the extension import fails, an error message is displayed and the error is logged to the console.
997+ * After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
998+ * @param {string} [suggestUrl] Suggested URL to install
999+ * @returns {Promise<void>}
1000+ */
1001+export async function openThirdPartyExtensionMenu(suggestUrl = '') {
1002+ const html = await renderTemplateAsync('installExtension');
1003+ const input = await callGenericPopup(html, POPUP_TYPE.INPUT, suggestUrl ?? '');
1004+
1005+ if (!input) {
1006+ console.debug('Extension install cancelled');
1007+ return;
1008+ }
1009+
1010+ const url = String(input).trim();
1011+ await installExtension(url);
1012+}
1013+
9921014jQuery(async function () {
9931015 await addExtensionsButtonAndMenu();
9941016 $('#extensionsMenuButton').css('display', 'flex');
@@ -1004,28 +1026,8 @@ jQuery(async function () {
10041026
10051027 /**
10061028 * 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.
10121029 *
10131030 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
10141031 */
10151032 $('#third_party_extension_button').on('click', async () => {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- });
10311033});
public/scripts/extensions/caption/index.js+7 -3
@@ -8,13 +8,12 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
88import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
99import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1010import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
11-import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
1211import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
1312export { MODULE_NAME };
1413
1514const MODULE_NAME = 'caption';
1615
1716const PROMPT_DEFAULT = 'What\'s in this image?';
1817const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';
1918
2019/**
@@ -170,7 +169,11 @@ async function sendCaptionedMessage(caption, image) {
170169 },
171170 };
172171 context.chat.push(message);
172+ const messageId = context.chat.length - 1;
173+ await eventSource.emit(event_types.MESSAGE_SENT, messageId);
173174 context.addOneMessage(message);
175+ await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
176+ await context.saveChat();
174177}
175178
176179/**
@@ -334,7 +337,7 @@ async function getCaptionForFile(file, prompt, quiet) {
334337 }
335338 catch (error) {
336339 const errorMessage = error.message || 'Unknown error';
337340 toastr.error(errorMessage, "'Failed to caption image."');
338341 console.error(error);
339342 return '';
340343 }
@@ -399,6 +402,7 @@ jQuery(async function () {
399402 (modules.includes('caption') && extension_settings.caption.source === 'extras') ||
400403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
401404 (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]) ||
402406 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
403407 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
404408 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
public/scripts/extensions/caption/settings.html+5 -0
@@ -17,6 +17,7 @@
1717 <div class="flex1 flex-container flexFlowColumn flexNoGap">
1818 <label for="caption_multimodal_api" data-i18n="API">API</label>
1919 <select id="caption_multimodal_api" class="flex1 text_pole">
20+ <option value="zerooneai">01.AI (Yi)</option>
2021 <option value="anthropic">Anthropic</option>
2122 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
2223 <option value="google">Google MakerSuite</option>
@@ -32,16 +33,20 @@
3233 <div class="flex1 flex-container flexFlowColumn flexNoGap">
3334 <label for="caption_multimodal_model" data-i18n="Model">Model</label>
3435 <select id="caption_multimodal_model" class="flex1 text_pole">
36+ <option data-type="zerooneai" value="yi-vision">yi-vision</option>
3537 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
3638 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
3739 <option data-type="openai" value="gpt-4o">gpt-4o</option>
3840 <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>
3942 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
4043 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
4144 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
4245 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
4346 <option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
4447 <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>
4550 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
4651 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
4752 <option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
public/scripts/extensions/expressions/index.js+8 -5
@@ -1,4 +1,4 @@
11import { callPopup, eventSource, event_types, generateQuietPromptgenerateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';
22import { dragElement, isMobile } from '../../RossAscends-mods.js';
33import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
44import { loadMovingUIState, power_user } from '../../power-user.js';
@@ -1156,7 +1156,7 @@ async function getExpressionLabel(text) {
11561156
11571157 functionResult = args?.arguments;
11581158 });
11591159 const emotionResponse = await generateQuietPromptgenerateRaw(prompttext, main_api, false, false, prompt);
11601160 return parseLlmResponse(functionResult || emotionResponse, expressionsList);
11611161 }
11621162 // Extras
@@ -1387,7 +1387,8 @@ async function getExpressionsList() {
13871387 }
13881388
13891389 // If there was no specific list, or an error, just return the default expressions
1390- return DEFAULT_EXPRESSIONS;
1390+ expressionsList = DEFAULT_EXPRESSIONS.filter(e => e !== 'talkinghead').slice();
1391+ return expressionsList;
13911392 }
13921393
13931394 const result = await resolveExpressionsList();
@@ -1618,11 +1619,13 @@ async function onClickExpressionRemoveCustom() {
16181619 moduleWorker();
16191620}
16201621
16211622function onExperesionApiChangedonExpressionApiChanged() {
16221623 const tempApi = this.value;
16231624 if (tempApi) {
16241625 extension_settings.expressions.api = Number(tempApi);
16251626 $('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm);
1627+ expressionsList = null;
1628+ spriteCache = {};
16261629 moduleWorker();
16271630 saveSettingsDebounced();
16281631 }
@@ -1972,7 +1975,7 @@ function migrateSettings() {
19721975 $('#expression_custom_add').on('click', onClickExpressionAddCustom);
19731976 $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom);
19741977 $('#expression_fallback').on('change', onExpressionFallbackChanged);
19751978 $('#expression_api').on('change', onExperesionApiChangedonExpressionApiChanged);
19761979 }
19771980
19781981 // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized.
public/scripts/extensions/gallery/index.i18n.html+2 -0
@@ -0,0 +1,2 @@
1+<!-- I18n data for tools used to auto generate translations -->
2+<div data-i18n="Show Gallery">Show Gallery</div>
public/scripts/extensions/gallery/index.js+51 -30
@@ -3,6 +3,7 @@ import {
33 this_chid,
44 characters,
55 getRequestHeaders,
6+ event_types,
67} from '../../../script.js';
78import { groups, selected_group } from '../../group-chats.js';
89import { loadFileToDocument, delay } from '../../utils.js';
@@ -13,6 +14,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1314import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
1415import { DragAndDropHandler } from '../../dragdrop.js';
1516import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
17+import { translate } from '../../i18n.js';
1618
1719const extensionName = 'gallery';
1820const extensionFolderPath = `scripts/extensions/${extensionName}/`;
@@ -24,6 +26,27 @@ let paginationVisiblePages = 10;
2426let paginationMaxLinesPerPage = 2;
2527let galleryMaxRows = 3;
2628
29+$('body').on('click', '.dragClose', function () {
30+ const relatedId = $(this).data('related-id'); // Get the ID of the related draggable
31+ $(`body > .draggable[id="${relatedId}"]`).remove(); // Remove the associated draggable
32+});
33+
34+const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved';
35+
36+const mutationObserver = new MutationObserver((mutations) => {
37+ mutations.forEach((mutation) => {
38+ mutation.removedNodes.forEach((node) => {
39+ if (node instanceof HTMLElement && node.tagName === 'DIV' && node.id === 'gallery') {
40+ eventSource.emit(CUSTOM_GALLERY_REMOVED_EVENT);
41+ }
42+ });
43+ });
44+});
45+
46+mutationObserver.observe(document.body, {
47+ childList: true,
48+ subtree: false,
49+});
2750
2851/**
2952 * Retrieves a list of gallery items based on a given URL. This function calls an API endpoint
@@ -58,7 +81,9 @@ async function getGalleryItems(url) {
5881 * @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
5982 */
6083async function initGallery(items, url) {
84+ const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`;
6185 const gallery = $('#dragGallery');
86+ gallery.addClass(nonce);
6287 gallery.nanogallery2({
6388 'items': items,
6489 thumbnailWidth: 'auto',
@@ -81,16 +106,26 @@ async function initGallery(items, url) {
81106 fnThumbnailOpen: viewWithDragbox,
82107 });
83108
109+ const dragDropHandler = new DragAndDropHandler(`#dragGallery.${nonce}`, async (files, event) => {
110+ let file = files[0];
111+ uploadFile(file, url); // Added url parameter to know where to upload
112+ });
84113
85114 eventSource.on('resizeUI',const resizeHandler = function (elmntName) {
86115 gallery.nanogallery2('resize');
87116 });
88117
89- const dragDropHandler = new DragAndDropHandler('#dragGallery', async (files, event) => {
118+ eventSource.on('resizeUI', resizeHandler);
90- let file = files[0];
119+
91- uploadFile(file, url); // Added url parameter to know where to upload
120+ eventSource.once(event_types.CHAT_CHANGED, function () {
121+ gallery.closest('#gallery').remove();
92122 });
93123
124+ eventSource.once(CUSTOM_GALLERY_REMOVED_EVENT, function () {
125+ gallery.nanogallery2('destroy');
126+ dragDropHandler.destroy();
127+ eventSource.removeListener('resizeUI', resizeHandler);
128+ });
94129
95130 // Set dropzone height to be the same as the parent
96131 gallery.css('height', gallery.parent().css('height'));
@@ -139,16 +174,10 @@ async function showCharGallery() {
139174
140175 const items = await getGalleryItems(url);
141176 // if there already is a gallery, destroy it and place this one in its place
142177 if ($('#dragGallery').lengthclosest('#gallery') {.remove();
143- $('#dragGallery').nanogallery2('destroy');
144- initGallery(items, url);
145- } else {
146178 makeMovable();
147- setTimeout(async () => {
179+ await delay(100);
148180 await initGallery(items, url);
149- }, 100);
150- }
151-
152181 } catch (err) {
153182 console.trace();
154183 console.error(err);
@@ -201,11 +230,11 @@ async function uploadFile(file, url) {
201230 toastr.success('File uploaded successfully. Saved at: ' + result.path);
202231
203232 // Refresh the gallery
204- $('#dragGallery').nanogallery2('destroy'); // Destroy old gallery
205233 const newItems = await getGalleryItems(url); // Fetch the latest items
206- initGallery(newItems, url); // Reinitialize the gallery with new items and pass 'url'
234+ $('#dragGallery').closest('#gallery').remove(); // Destroy old gallery
207-
235+ makeMovable();
208-
236+ await delay(100);
237+ await initGallery(newItems, url); // Reinitialize the gallery with new items and pass 'url'
209238 } catch (error) {
210239 console.error('There was an issue uploading the file:', error);
211240
@@ -228,7 +257,7 @@ $(document).ready(function () {
228257 $('#char-management-dropdown').append(
229258 $('<option>', {
230259 id: 'show_char_gallery',
231260 text: translate('Show Gallery'),
232261 }),
233262 );
234263});
@@ -272,11 +301,6 @@ function makeMovable(id = 'gallery') {
272301 e.preventDefault();
273302 return false;
274303 });
275-
276- $('body').on('click', '.dragClose', function () {
277- const relatedId = $(this).data('related-id'); // Get the ID of the related draggable
278- $(`#${relatedId}`).remove(); // Remove the associated draggable
279- });
280304}
281305
282306/**
@@ -357,11 +381,6 @@ function makeDragImg(id, url) {
357381 } else {
358382 console.error('Failed to append the template content or retrieve the appended content.');
359383 }
360-
361- $('body').on('click', '.dragClose', function () {
362- const relatedId = $(this).data('related-id'); // Get the ID of the related draggable
363- $(`#${relatedId}`).remove(); // Remove the associated draggable
364- });
365384}
366385
367386/**
@@ -400,7 +419,8 @@ function viewWithDragbox(items) {
400419
401420
402421// Registers a simple command for opening the char gallery.
403422SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery',
423+ name: 'show-gallery',
404424 aliases: ['sg'],
405425 callback: () => {
406426 showCharGallery();
@@ -408,7 +428,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery
408428 },
409429 helpString: 'Shows the gallery.',
410430}));
411431SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery',
432+ name: 'list-gallery',
412433 aliases: ['lg'],
413434 callback: listGalleryCommand,
414435 returns: 'list of images',
public/scripts/extensions/gallery/manifest.json+1 -1
@@ -5,7 +5,7 @@
55 "optional": [
66 ],
77 "js": "index.js",
88 "css": "style.css",
99 "author": "City-Unit",
1010 "version": "1.5.0",
1111 "homePage": "https://github.com/SillyTavern/SillyTavern"
public/scripts/extensions/gallery/style.css+5 -0
@@ -0,0 +1,5 @@
1+.nGY2 .nGY2GalleryBottom {
2+ display: flex;
3+ align-items: center;
4+ justify-content: center;
5+}
public/scripts/extensions/memory/index.js+152 -26
@@ -14,6 +14,7 @@ import {
1414 substituteParamsExtended,
1515 generateRaw,
1616 getMaxContextSize,
17+ setExtensionPrompt,
1718} from '../../../script.js';
1819import { is_group_generating, selected_group } from '../../group-chats.js';
1920import { loadMovingUIState } from '../../power-user.js';
@@ -24,6 +25,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
2425import { SlashCommand } from '../../slash-commands/SlashCommand.js';
2526import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
2627import { MacrosParser } from '../../macros.js';
28+import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
2729export { MODULE_NAME };
2830
2931const MODULE_NAME = '1_memory';
@@ -35,6 +37,41 @@ let lastMessageHash = null;
3537let lastMessageId = null;
3638let inApiCall = false;
3739
40+/**
41+ * Count the number of tokens in the provided text.
42+ * @param {string} text Text to count tokens for
43+ * @param {number} padding Number of additional tokens to add to the count
44+ * @returns {Promise<number>} Number of tokens in the text
45+ */
46+async function countSourceTokens(text, padding = 0) {
47+ if (extension_settings.memory.source === summary_sources.webllm) {
48+ const count = await countWebLlmTokens(text);
49+ return count + padding;
50+ }
51+
52+ if (extension_settings.memory.source === summary_sources.extras) {
53+ const count = getTextTokens(tokenizers.GPT2, text).length;
54+ return count + padding;
55+ }
56+
57+ return await getTokenCountAsync(text, padding);
58+}
59+
60+async function getSourceContextSize() {
61+ const overrideLength = extension_settings.memory.overrideResponseLength;
62+
63+ if (extension_settings.memory.source === summary_sources.webllm) {
64+ const maxContext = await getWebLlmContextSize();
65+ return overrideLength > 0 ? (maxContext - overrideLength) : Math.round(maxContext * 0.75);
66+ }
67+
68+ if (extension_settings.source === summary_sources.extras) {
69+ return 1024 - 64;
70+ }
71+
72+ return getMaxContextSize(overrideLength);
73+}
74+
3875const formatMemoryValue = function (value) {
3976 if (!value) {
4077 return '';
@@ -54,6 +91,7 @@ const saveChatDebounced = debounce(() => getContext().saveChat(), debounce_timeo
5491const summary_sources = {
5592 'extras': 'extras',
5693 'main': 'main',
94+ 'webllm': 'webllm',
5795};
5896
5997const prompt_builders = {
@@ -73,6 +111,7 @@ const defaultSettings = {
73111 template: defaultTemplate,
74112 position: extension_prompt_types.IN_PROMPT,
75113 role: extension_prompt_roles.SYSTEM,
114+ scan: false,
76115 depth: 2,
77116 promptWords: 200,
78117 promptMinWords: 25,
@@ -122,17 +161,18 @@ function loadSettings() {
122161 $(`input[name="memory_prompt_builder"][value="${extension_settings.memory.prompt_builder}"]`).prop('checked', true).trigger('input');
123162 $('#memory_override_response_length').val(extension_settings.memory.overrideResponseLength).trigger('input');
124163 $('#memory_max_messages_per_request').val(extension_settings.memory.maxMessagesPerRequest).trigger('input');
164+ $('#memory_include_wi_scan').prop('checked', extension_settings.memory.scan).trigger('input');
125165 switchSourceControls(extension_settings.memory.source);
126166}
127167
128168async function onPromptForceWordsAutoClick() {
129169 const context = getContext();
130170 const maxPromptLength = getMaxContextSizeawait getSourceContextSize(extension_settings.memory.overrideResponseLength);
131171 const chat = context.chat;
132172 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
133173 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
134174 const averageMessageWordCount = messagesWordCount / allMessages.length;
135175 const tokensPerWord = await getTokenCountAsynccountSourceTokens(allMessages.join('\n')) / messagesWordCount;
136176 const wordsPerToken = 1 / tokensPerWord;
137177 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);
138178 // How many words should pass so that messages will start be dropped out of context;
@@ -165,15 +205,15 @@ async function onPromptForceWordsAutoClick() {
165205
166206async function onPromptIntervalAutoClick() {
167207 const context = getContext();
168208 const maxPromptLength = getMaxContextSizeawait getSourceContextSize(extension_settings.memory.overrideResponseLength);
169209 const chat = context.chat;
170210 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
171211 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
172212 const messagesTokenCount = await getTokenCountAsynccountSourceTokens(allMessages.join('\n'));
173213 const tokensPerWord = messagesTokenCount / messagesWordCount;
174214 const averageMessageTokenCount = messagesTokenCount / allMessages.length;
175215 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);
176216 const promptTokens = await getTokenCountAsynccountSourceTokens(extension_settings.memory.prompt);
177217 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;
178218 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
179219 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);
@@ -210,8 +250,8 @@ function onSummarySourceChange(event) {
210250
211251function switchSourceControls(value) {
212252 $('#memory_settings [data-summary-source]').each((_, element) => {
213253 const source = $(element).datadataset.summarySource.split('summary-source,').map(s => s.trim());
214254 $(element).toggle(source === .includes(value));
215255 });
216256}
217257
@@ -279,6 +319,13 @@ function onMemoryPositionChange(e) {
279319 saveSettingsDebounced();
280320}
281321
322+function onMemoryIncludeWIScanInput() {
323+ const value = !!$(this).prop('checked');
324+ extension_settings.memory.scan = value;
325+ reinsertMemory();
326+ saveSettingsDebounced();
327+}
328+
282329function onMemoryPromptWordsForceInput() {
283330 const value = $(this).val();
284331 extension_settings.memory.promptForceWords = Number(value);
@@ -343,10 +390,13 @@ function getIndexOfLatestChatSummary(chat) {
343390
344391async function onChatEvent() {
345392 // Module not enabled
346393 if (extension_settings.memory.source === summary_sources.extras && !modules.includes('summarize')) {
347- if (!modules.includes('summarize')) {
348394 return;
349395 }
396+
397+ // WebLLM is not supported
398+ if (extension_settings.memory.source === summary_sources.webllm && !isWebLlmSupported()) {
399+ return;
350400 }
351401
352402 const context = getContext();
@@ -421,8 +471,12 @@ async function forceSummarizeChat() {
421471 return '';
422472 }
423473
424474 const toast = toastr.info('Summarizing chat...', 'Please wait', { timeOut: 0, extendedTimeOut: 0 });
425- const value = await summarizeChatMain(context, true, skipWIAN);
475+ const value = extension_settings.memory.source === summary_sources.main
476+ ? await summarizeChatMain(context, true, skipWIAN)
477+ : await summarizeChatWebLLM(context, true);
478+
479+ toastr.clear(toast);
426480
427481 if (!value) {
428482 toastr.warning('Failed to summarize chat');
@@ -454,6 +508,11 @@ async function summarizeCallback(args, text) {
454508 return await callExtrasSummarizeAPI(text);
455509 case summary_sources.main:
456510 return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
511+ case summary_sources.webllm: {
512+ const messages = [{ role: 'system', content: prompt }, { role: 'user', content: text }].filter(m => m.content);
513+ const params = extension_settings.memory.overrideResponseLength > 0 ? { max_tokens: extension_settings.memory.overrideResponseLength } : {};
514+ return await generateWebLlmChatPrompt(messages, params);
515+ }
457516 default:
458517 toastr.warning('Invalid summarization source specified');
459518 return '';
@@ -474,16 +533,25 @@ async function summarizeChat(context) {
474533 case summary_sources.main:
475534 await summarizeChatMain(context, false, skipWIAN);
476535 break;
536+ case summary_sources.webllm:
537+ await summarizeChatWebLLM(context, false);
538+ break;
477539 default:
478540 break;
479541 }
480542}
481543
482-async function summarizeChatMain(context, force, skipWIAN) {
544+/**
483-
545+ * Check if the chat should be summarized based on the current conditions.
546+ * Return summary prompt if it should be summarized.
547+ * @param {any} context ST context
548+ * @param {boolean} force Summarize the chat regardless of the conditions
549+ * @returns {Promise<string>} Summary prompt or empty string
550+ */
551+async function getSummaryPromptForNow(context, force) {
484552 if (extension_settings.memory.promptInterval === 0 && !force) {
485553 console.debug('Prompt interval is set to 0, skipping summarization');
486554 return '';
487555 }
488556
489557 try {
@@ -495,17 +563,17 @@ async function summarizeChatMain(context, force, skipWIAN) {
495563 waitUntilCondition(() => is_send_press === false, 30000, 100);
496564 } catch {
497565 console.debug('Timeout waiting for is_send_press');
498566 return '';
499567 }
500568
501569 if (!context.chat.length) {
502570 console.debug('No messages in chat to summarize');
503571 return '';
504572 }
505573
506574 if (context.chat.length < extension_settings.memory.promptInterval && !force) {
507575 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);
508576 return '';
509577 }
510578
511579 let messagesSinceLastSummary = 0;
@@ -529,7 +597,7 @@ async function summarizeChatMain(context, force, skipWIAN) {
529597
530598 if (!conditionSatisfied && !force) {
531599 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);
532600 return '';
533601 }
534602
535603 console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);
@@ -537,6 +605,63 @@ async function summarizeChatMain(context, force, skipWIAN) {
537605
538606 if (!prompt) {
539607 console.debug('Summarization prompt is empty. Skipping summarization.');
608+ return '';
609+ }
610+
611+ return prompt;
612+}
613+
614+async function summarizeChatWebLLM(context, force) {
615+ if (!isWebLlmSupported()) {
616+ return;
617+ }
618+
619+ const prompt = await getSummaryPromptForNow(context, force);
620+
621+ if (!prompt) {
622+ return;
623+ }
624+
625+ const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
626+
627+ if (lastUsedIndex === null || lastUsedIndex === -1) {
628+ if (force) {
629+ toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');
630+ }
631+
632+ return null;
633+ }
634+
635+ const messages = [
636+ { role: 'system', content: prompt },
637+ { role: 'user', content: rawPrompt },
638+ ];
639+
640+ const params = {};
641+
642+ if (extension_settings.memory.overrideResponseLength > 0) {
643+ params.max_tokens = extension_settings.memory.overrideResponseLength;
644+ }
645+
646+ const summary = await generateWebLlmChatPrompt(messages, params);
647+ const newContext = getContext();
648+
649+ // something changed during summarization request
650+ if (newContext.groupId !== context.groupId ||
651+ newContext.chatId !== context.chatId ||
652+ (!newContext.groupId && (newContext.characterId !== context.characterId))) {
653+ console.log('Context changed, summary discarded');
654+ return;
655+ }
656+
657+ setMemoryContext(summary, true, lastUsedIndex);
658+ return summary;
659+}
660+
661+async function summarizeChatMain(context, force, skipWIAN) {
662+ const prompt = await getSummaryPromptForNow(context, force);
663+
664+ if (!prompt) {
540665 return;
541666 }
542667
@@ -624,7 +749,7 @@ async function getRawSummaryPrompt(context, prompt) {
624749 chat.pop(); // We always exclude the last message from the buffer
625750 const chatBuffer = [];
626751 const PADDING = 64;
627752 const PROMPT_SIZE = getMaxContextSizeawait getSourceContextSize(extension_settings.memory.overrideResponseLength);
628753 let latestUsedMessage = null;
629754
630755 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {
@@ -641,7 +766,7 @@ async function getRawSummaryPrompt(context, prompt) {
641766 const entry = `${message.name}:\n${message.mes}`;
642767 chatBuffer.push(entry);
643768
644769 const tokens = await getTokenCountAsynccountSourceTokens(getMemoryString(true), PADDING);
645770
646771 if (tokens > PROMPT_SIZE) {
647772 chatBuffer.pop();
@@ -670,7 +795,7 @@ async function summarizeChatExtras(context) {
670795 const reversedChat = chat.slice().reverse();
671796 reversedChat.shift();
672797 const memoryBuffer = [];
673798 const CONTEXT_SIZE = 1024 -await 64getSourceContextSize();
674799
675800 for (const message of reversedChat) {
676801 // we reached the point of latest memory
@@ -688,14 +813,14 @@ async function summarizeChatExtras(context) {
688813 memoryBuffer.push(entry);
689814
690815 // check if token limit was reached
691816 const tokens = getTextTokens(tokenizers.GPT2,await countSourceTokens(getMemoryString()).length;
692817 if (tokens >= CONTEXT_SIZE) {
693818 break;
694819 }
695820 }
696821
697822 const resultingString = getMemoryString();
698823 const resultingTokens = getTextTokens(tokenizers.GPT2,await countSourceTokens(resultingString).length;
699824
700825 if (!resultingString || resultingTokens < CONTEXT_SIZE) {
701826 console.debug('Not enough context to summarize');
@@ -800,8 +925,7 @@ function reinsertMemory() {
800925 * @param {number|null} index Index of the chat message to save the summary to. If null, the pre-last message is used.
801926 */
802927function setMemoryContext(value, saveToMessage, index = null) {
803- const context = getContext();
928+ setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, extension_settings.memory.scan, extension_settings.memory.role);
804- context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, false, extension_settings.memory.role);
805929 $('#memory_contents').val(value);
806930
807931 const summaryLog = value
@@ -809,6 +933,7 @@ function setMemoryContext(value, saveToMessage, index = null) {
809933 : 'Summary has no content';
810934 console.debug(summaryLog);
811935
936+ const context = getContext();
812937 if (saveToMessage && context.chat.length) {
813938 const idx = index ?? context.chat.length - 2;
814939 const mes = context.chat[idx < 0 ? 0 : idx];
@@ -894,6 +1019,7 @@ function setupListeners() {
8941019 $('#memory_prompt_words_auto').off('click').on('click', onPromptForceWordsAutoClick);
8951020 $('#memory_override_response_length').off('click').on('input', onOverrideResponseLengthInput);
8961021 $('#memory_max_messages_per_request').off('click').on('input', onMaxMessagesPerRequestInput);
1022+ $('#memory_include_wi_scan').off('input').on('input', onMemoryIncludeWIScanInput);
8971023 $('#summarySettingsBlockToggle').off('click').on('click', function () {
8981024 console.log('saw settings button click');
8991025 $('#summarySettingsBlock').slideToggle(200, 'swing'); //toggleClass("hidden");
@@ -922,7 +1048,7 @@ jQuery(async function () {
9221048 name: 'summarize',
9231049 callback: summarizeCallback,
9241050 namedArgumentList: [
9251051 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']Object.values(summary_sources)),
9261052 SlashCommandNamedArgument.fromProps({
9271053 name: 'prompt',
9281054 description: 'prompt to use for summarization',
public/scripts/extensions/memory/settings.html+13 -3
@@ -13,6 +13,7 @@
1313 <select id="summary_source">
1414 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
1515 <option value="extras">Extras API</option>
16+ <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
1617 </select><br>
1718
1819 <div class="flex-container justifyspacebetween alignitemscenter">
@@ -24,7 +25,7 @@
2425
2526 <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>
2627 <div class="memory_contents_controls">
2728 <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">
2829 <i class="fa-solid fa-database"></i>
2930 <span data-i18n="ext_sum_force_text">Summarize now</span>
3031 </div>
@@ -58,7 +59,7 @@
5859 <span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>
5960 </label>
6061 </div>
6162 <div data-summary-source="main,webllm">
6263 <label for="memory_prompt" class="title_restorable">
6364 <span data-i18n="Summary Prompt">Summary Prompt</span>
6465 <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 @@
7475 </label>
7576 <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />
7677 <label for="memory_max_messages_per_request">
7778 <span data-i18n="ext_sum_raw_max_msg">[Raw/WebLLM] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)
7879 <small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>
7980 </label>
8081 <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />
@@ -109,8 +110,17 @@
109110 <textarea id="memory_template" class="text_pole textarea_compact" rows="2" data-i18n="[placeholder]ext_sum_memory_template_placeholder" placeholder="&lcub;&lcub;summary&rcub;&rcub; will resolve to the current summary contents."></textarea>
110111 </div>
111112 <label for="memory_position" data-i18n="ext_sum_injection_position">Injection Position</label>
113+ <label class="checkbox_label" for="memory_include_wi_scan" data-i18n="[title]ext_sum_include_wi_scan_desc" title="Include the latest summary in the WI scan.">
114+ <input id="memory_include_wi_scan" type="checkbox" />
115+ <span data-i18n="ext_sum_include_wi_scan">Include in World Info Scanning</span>
116+ </label>
112117 <div class="radio_group">
113118 <label>
119+ <input type="radio" name="memory_position" value="-1" />
120+ <span data-i18n="None (not injected)">None (not injected)</span>
121+ <i class="fa-solid fa-info-circle" title="The summary will not be injected into the prompt. You can still access it via the &lcub;&lcub;summary&rcub;&rcub; macro." data-i18n="[title]ext_sum_injection_position_none"></i>
122+ </label>
123+ <label>
114124 <input type="radio" name="memory_position" value="2" />
115125 <span data-i18n="Before Main Prompt / Story String">Before Main Prompt / Story String</span>
116126 </label>
public/scripts/extensions/quick-reply/api/QuickReplyApi.js+94 -66
@@ -24,9 +24,17 @@ export class QuickReplyApi {
2424
2525
2626 /**
27+ * @param {QuickReply} qr
28+ * @returns {QuickReplySet}
29+ */
30+ getSetByQr(qr) {
31+ return QuickReplySet.list.find(it=>it.qrList.includes(qr));
32+ }
33+
34+ /**
2735 * Finds and returns an existing Quick Reply Set by its name.
2836 *
2937 * @param {Stringstring} name name of the quick reply set
3038 * @returns the quick reply set, or undefined if not found
3139 */
3240 getSetByName(name) {
@@ -36,13 +44,14 @@ export class QuickReplyApi {
3644 /**
3745 * Finds and returns an existing Quick Reply by its set's name and its label.
3846 *
3947 * @param {Stringstring} setName name of the quick reply set
4048 * @param {Stringstring|number} label label or numeric ID of the quick reply
4149 * @returns the quick reply, or undefined if not found
4250 */
4351 getQrByLabel(setName, label) {
4452 const set = this.getSetByName(setName);
4553 if (!set) return;
54+ if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label);
4655 return set.qrList.find(it=>it.label == label);
4756 }
4857
@@ -70,24 +79,25 @@ export class QuickReplyApi {
7079 /**
7180 * Executes an existing quick reply.
7281 *
7382 * @param {Stringstring} setName name of the existing quick reply set
7483 * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID
7584 * @param {Objectobject} [args] optional arguments
85+ * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
7686 */
7787 async executeQuickReply(setName, label, args = {}, options = {}) {
7888 const qr = this.getQrByLabel(setName, label);
7989 if (!qr) {
8090 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
8191 }
8292 return await qr.execute(args, false, false, options);
8393 }
8494
8595
8696 /**
8797 * Adds or removes a quick reply set to the list of globally active quick reply sets.
8898 *
8999 * @param {Stringstring} name the name of the set
90100 * @param {Booleanboolean} isVisible whether to show the set's buttons or not
91101 */
92102 toggleGlobalSet(name, isVisible = true) {
93103 const set = this.getSetByName(name);
@@ -104,8 +114,8 @@ export class QuickReplyApi {
104114 /**
105115 * Adds a quick reply set to the list of globally active quick reply sets.
106116 *
107117 * @param {Stringstring} name the name of the set
108118 * @param {Booleanboolean} isVisible whether to show the set's buttons or not
109119 */
110120 addGlobalSet(name, isVisible = true) {
111121 const set = this.getSetByName(name);
@@ -118,7 +128,7 @@ export class QuickReplyApi {
118128 /**
119129 * Removes a quick reply set from the list of globally active quick reply sets.
120130 *
121131 * @param {Stringstring} name the name of the set
122132 */
123133 removeGlobalSet(name) {
124134 const set = this.getSetByName(name);
@@ -132,8 +142,8 @@ export class QuickReplyApi {
132142 /**
133143 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
134144 *
135145 * @param {Stringstring} name the name of the set
136146 * @param {Booleanboolean} isVisible whether to show the set's buttons or not
137147 */
138148 toggleChatSet(name, isVisible = true) {
139149 if (!this.settings.chatConfig) return;
@@ -151,8 +161,8 @@ export class QuickReplyApi {
151161 /**
152162 * Adds a quick reply set to the list of the current chat's active quick reply sets.
153163 *
154164 * @param {Stringstring} name the name of the set
155165 * @param {Booleanboolean} isVisible whether to show the set's buttons or not
156166 */
157167 addChatSet(name, isVisible = true) {
158168 if (!this.settings.chatConfig) return;
@@ -166,7 +176,7 @@ export class QuickReplyApi {
166176 /**
167177 * Removes a quick reply set from the list of the current chat's active quick reply sets.
168178 *
169179 * @param {Stringstring} name the name of the set
170180 */
171181 removeChatSet(name) {
172182 if (!this.settings.chatConfig) return;
@@ -181,21 +191,26 @@ export class QuickReplyApi {
181191 /**
182192 * Creates a new quick reply in an existing quick reply set.
183193 *
184194 * @param {Stringstring} setName name of the quick reply set to insert the new quick reply into
185195 * @param {Stringstring} label label for the new quick reply (text on the button)
186196 * @param {Objectobject} [props]
187197 * @param {Stringstring} [props.messageicon] the message to be sent or slash commandicon to be executedshow byon the new quickQR replybutton
188198 * @param {Stringboolean} [props.titleshowLabel] the titlewhether /to tooltipshow tothe belabel showneven onwhen thean quickicon replyis buttonassigned
189199 * @param {Booleanstring} [props.isHiddenmessage] whetherthe message to hidebe sent or showslash command to be executed by the buttonnew quick reply
190200 * @param {Booleanstring} [props.executeOnStartuptitle] whetherthe title / tooltip to executebe shown on the quick reply when SillyTavern startsbutton
191201 * @param {Booleanboolean} [props.executeOnUserisHidden] whether to execute the quick reply after a userhide hasor sentshow athe messagebutton
192202 * @param {Booleanboolean} [props.executeOnAiexecuteOnStartup] whether to execute the quick reply after the AI has sentwhen aSillyTavern messagestarts
193203 * @param {Booleanboolean} [props.executeOnChatChangeexecuteOnUser] whether to execute the quick reply whenafter a newuser chathas issent loadeda message
194204 * @param {Booleanboolean} [props.executeOnGroupMemberDraftexecuteOnAi] whether to execute the quick reply whenafter athe groupAI memberhas issent selecteda message
195205 * @param {Stringboolean} [props.automationIdexecuteOnChatChange] whenwhether notto empty,execute the quick reply will be executed when the WI with the givena automationnew IDchat is activatedloaded
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
196209 * @returns {QuickReply} the new quick reply
197210 */
198211 createQuickReply(setName, label, {
212+ icon,
213+ showLabel,
199214 message,
200215 title,
201216 isHidden,
@@ -204,6 +219,7 @@ export class QuickReplyApi {
204219 executeOnAi,
205220 executeOnChatChange,
206221 executeOnGroupMemberDraft,
222+ executeOnNewChat,
207223 automationId,
208224 } = {}) {
209225 const set = this.getSetByName(setName);
@@ -212,6 +228,8 @@ export class QuickReplyApi {
212228 }
213229 const qr = set.addQuickReply();
214230 qr.label = label ?? '';
231+ qr.icon = icon ?? '';
232+ qr.showLabel = showLabel ?? false;
215233 qr.message = message ?? '';
216234 qr.title = title ?? '';
217235 qr.isHidden = isHidden ?? false;
@@ -220,6 +238,7 @@ export class QuickReplyApi {
220238 qr.executeOnAi = executeOnAi ?? false;
221239 qr.executeOnChatChange = executeOnChatChange ?? false;
222240 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
241+ qr.executeOnNewChat = executeOnNewChat ?? false;
223242 qr.automationId = automationId ?? '';
224243 qr.onUpdate();
225244 return qr;
@@ -228,22 +247,27 @@ export class QuickReplyApi {
228247 /**
229248 * Updates an existing quick reply.
230249 *
231250 * @param {Stringstring} setName name of the existing quick reply set
232251 * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID
233252 * @param {Objectobject} [props]
234253 * @param {Stringstring} [props.newLabelicon] new label forthe quickicon replyto (textshow on the QR button)
235254 * @param {Stringboolean} [props.messageshowLabel] the messagewhether to be sent or slashshow commandthe tolabel beeven executedwhen byan theicon quickis replyassigned
236255 * @param {Stringstring} [props.titlenewLabel] the titlenew /label tooltipfor toquick bereply shown(text on the quick reply button)
237256 * @param {Booleanstring} [props.isHiddenmessage] whetherthe message to hidebe sent or showslash command to be executed by the buttonquick reply
238257 * @param {Booleanstring} [props.executeOnStartuptitle] whetherthe title / tooltip to executebe shown on the quick reply when SillyTavern startsbutton
239258 * @param {Booleanboolean} [props.executeOnUserisHidden] whether to execute the quick reply after a userhide hasor sentshow athe messagebutton
240259 * @param {Booleanboolean} [props.executeOnAiexecuteOnStartup] whether to execute the quick reply after the AI has sentwhen aSillyTavern messagestarts
241260 * @param {Booleanboolean} [props.executeOnChatChangeexecuteOnUser] whether to execute the quick reply whenafter a newuser chathas issent loadeda message
242261 * @param {Booleanboolean} [props.executeOnGroupMemberDraftexecuteOnAi] whether to execute the quick reply whenafter athe groupAI memberhas issent selecteda message
243262 * @param {Stringboolean} [props.automationIdexecuteOnChatChange] whenwhether notto empty,execute the quick reply will be executed when the WI with the givena automationnew IDchat is activatedloaded
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
244266 * @returns {QuickReply} the altered quick reply
245267 */
246268 updateQuickReply(setName, label, {
269+ icon,
270+ showLabel,
247271 newLabel,
248272 message,
249273 title,
@@ -253,12 +277,15 @@ export class QuickReplyApi {
253277 executeOnAi,
254278 executeOnChatChange,
255279 executeOnGroupMemberDraft,
280+ executeOnNewChat,
256281 automationId,
257282 } = {}) {
258283 const qr = this.getQrByLabel(setName, label);
259284 if (!qr) {
260285 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
261286 }
287+ qr.updateIcon(icon ?? qr.icon);
288+ qr.updateShowLabel(showLabel ?? qr.showLabel);
262289 qr.updateLabel(newLabel ?? qr.label);
263290 qr.updateMessage(message ?? qr.message);
264291 qr.updateTitle(title ?? qr.title);
@@ -268,6 +295,7 @@ export class QuickReplyApi {
268295 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
269296 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
270297 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
298+ qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
271299 qr.automationId = automationId ?? qr.automationId;
272300 qr.onUpdate();
273301 return qr;
@@ -276,8 +304,8 @@ export class QuickReplyApi {
276304 /**
277305 * Deletes an existing quick reply.
278306 *
279307 * @param {Stringstring} setName name of the existing quick reply set
280308 * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID
281309 */
282310 deleteQuickReply(setName, label) {
283311 const qr = this.getQrByLabel(setName, label);
@@ -291,10 +319,10 @@ export class QuickReplyApi {
291319 /**
292320 * Adds an existing quick reply set as a context menu to an existing quick reply.
293321 *
294322 * @param {Stringstring} setName name of the existing quick reply set containing the quick reply
295323 * @param {Stringstring|number} label label of the existing quick reply or its numeric ID
296324 * @param {Stringstring} contextSetName name of the existing quick reply set to be used as a context menu
297325 * @param {Booleanboolean} isChained whether or not to chain the context menu quick replies
298326 */
299327 createContextItem(setName, label, contextSetName, isChained = false) {
300328 const qr = this.getQrByLabel(setName, label);
@@ -314,9 +342,9 @@ export class QuickReplyApi {
314342 /**
315343 * Removes a quick reply set from a quick reply's context menu.
316344 *
317345 * @param {Stringstring} setName name of the existing quick reply set containing the quick reply
318346 * @param {Stringstring|number} label label of the existing quick reply or its numeric ID
319347 * @param {Stringstring} contextSetName name of the existing quick reply set to be used as a context menu
320348 */
321349 deleteContextItem(setName, label, contextSetName) {
322350 const qr = this.getQrByLabel(setName, label);
@@ -333,8 +361,8 @@ export class QuickReplyApi {
333361 /**
334362 * Removes all entries from a quick reply's context menu.
335363 *
336364 * @param {Stringstring} setName name of the existing quick reply set containing the quick reply
337365 * @param {Stringstring|number} label label of the existing quick reply or its numeric ID
338366 */
339367 clearContextMenu(setName, label) {
340368 const qr = this.getQrByLabel(setName, label);
@@ -348,11 +376,11 @@ export class QuickReplyApi {
348376 /**
349377 * Create a new quick reply set.
350378 *
351379 * @param {Stringstring} name name of the new quick reply set
352380 * @param {Objectobject} [props]
353381 * @param {Booleanboolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
354382 * @param {Booleanboolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
355383 * @param {Booleanboolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
356384 * @returns {Promise<QuickReplySet>} the new quick reply set
357385 */
358386 async createSet(name, {
@@ -384,11 +412,11 @@ export class QuickReplyApi {
384412 /**
385413 * Update an existing quick reply set.
386414 *
387415 * @param {Stringstring} name name of the existing quick reply set
388416 * @param {Objectobject} [props]
389417 * @param {Booleanboolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
390418 * @param {Booleanboolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
391419 * @param {Booleanboolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
392420 * @returns {Promise<QuickReplySet>} the altered quick reply set
393421 */
394422 async updateSet(name, {
@@ -411,7 +439,7 @@ export class QuickReplyApi {
411439 /**
412440 * Delete an existing quick reply set.
413441 *
414442 * @param {Stringstring} name name of the existing quick reply set
415443 */
416444 async deleteSet(name) {
417445 const set = this.getSetByName(name);
@@ -451,7 +479,7 @@ export class QuickReplyApi {
451479 /**
452480 * Gets a list of all quick replies in the quick reply set.
453481 *
454482 * @param {Stringstring} setName name of the existing quick reply set
455483 * @returns array with the labels of this set's quick replies
456484 */
457485 listQuickReplies(setName) {
public/scripts/extensions/quick-reply/html/qrEditor.html+37 -7
@@ -2,10 +2,23 @@
22 <div id="qr--main">
33 <h3 data-i18n="Labels and Message">Labels and Message</h3>
44 <div class="qr--labels">
5- <label>
5+ <label class="qr--fit">
6+ <span class="qr--labelText" data-i18n="Label">Icon</span>
7+ <small class="qr--labelHint">&nbsp;</small>
8+ <div class="menu_button fa-fw" id="qr--modal-icon" title="Click to change icon"></div>
9+ </label>
10+ <div class="label">
611 <span class="qr--labelText" data-i18n="Label">Label</span>
7- <input type="text" class="text_pole" id="qr--modal-label">
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
817 </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>
922 <label>
1023 <span class="qr--labelText" data-i18n="Title">Title</span>
1124 <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 @@
3346 <input type="checkbox" id="qr--modal-syntax">
3447 <span>Syntax highlight</span>
3548 </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>
3651 </div>
3752 <div id="qr--modal-messageHolder">
3853 <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+
4665 <div id="qr--qrOptions">
4766 <h3 data-i18n="Context Menu">Context Menu</h3>
4867 <div id="qr--ctxEditor">
@@ -64,7 +83,7 @@
6483
6584
6685 <h3 data-i18n="Auto-Execute">Auto-Execute</h3>
6786 <div id="qr--autoExec" class="flex-container flexFlowColumn">
6887 <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)">
6988 <input type="checkbox" id="qr--preventAutoExecute" >
7089 <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 @@
90109 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>
91110 </label>
92111 <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">
93116 <input type="checkbox" id="qr--executeOnGroupMemberDraft">
94117 <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>
95118 </label>
@@ -117,11 +140,18 @@
117140 </div>
118141 </div>
119142 <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>
124143 <div id="qr--modal-executeErrors"></div>
125144 <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>
126156 </div>
127157</div>
public/scripts/extensions/quick-reply/html/settings.html+10 -0
@@ -60,10 +60,20 @@
6060 <label class="flex-container" id="qr--injectInputContainer">
6161 <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>
6262 </label>
63+ <div class="flex-container alignItemsCenter">
64+ <toolcool-color-picker id="qr--color"></toolcool-color-picker>
65+ <div class="menu_button" id="qr--colorClear">Clear</div>
66+ <span data-i18n="Color">Color</span>
67+ </div>
68+ <label class="flex-container" id="qr--onlyBorderColorContainer">
69+ <input type="checkbox" id="qr--onlyBorderColor"> <span data-i18n="Only apply color as accent">Only apply color as accent</span>
70+ </label>
6371 </div>
6472 <div id="qr--set-qrList" class="qr--qrList"></div>
6573 <div class="qr--set-qrListActions">
6674 <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
75+ <div class="qr--paste menu_button menu_button_icon fa-solid fa-paste" id="qr--set-paste" title="Paste quick reply from clipboard"></div>
76+ <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>
6777 </div>
6878 </div>
6979 </div>
public/scripts/extensions/quick-reply/index.js+8 -2
@@ -105,6 +105,7 @@ const loadSets = async () => {
105105 qr.executeOnAi = slot.autoExecute_botMessage ?? false;
106106 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
107107 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
108+ qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
108109 qr.automationId = slot.automationId ?? '';
109110 qr.contextList = (slot.contextMenu ?? []).map(it=>({
110111 set: it.preset,
@@ -176,7 +177,7 @@ const init = async () => {
176177 buttons.show();
177178 settings.onSave = ()=>buttons.refresh();
178179
179180 window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
180181 let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
181182 .map(it=>it.set.qrList)
182183 .flat()
@@ -191,7 +192,7 @@ const init = async () => {
191192 }
192193 }
193194 if (qr && qr.onExecute) {
194195 return await qr.execute(args, false, true, options);
195196 } else {
196197 throw new Error(`No Quick Reply found for "${name}".`);
197198 }
@@ -260,3 +261,8 @@ const onWIActivation = async (entries) => {
260261 await autoExec.handleWIActivation(entries);
261262};
262263eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));
264+
265+const onNewChat = async () => {
266+ await autoExec.handleNewChat();
267+};
268+eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
public/scripts/extensions/quick-reply/lib/morphdom-esm.js+769 -0
@@ -0,0 +1,769 @@
1+var DOCUMENT_FRAGMENT_NODE = 11;
2+
3+function 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+
65+var range; // Create a range object for efficently rendering strings to elements.
66+var NS_XHTML = 'http://www.w3.org/1999/xhtml';
67+
68+var doc = typeof document === 'undefined' ? undefined : document;
69+var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
70+var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
71+
72+function createFragmentFromTemplate(str) {
73+ var template = doc.createElement('template');
74+ template.innerHTML = str;
75+ return template.content.childNodes[0];
76+}
77+
78+function 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+
88+function 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+ */
102+function 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+ */
126+function 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+ */
160+function 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+ */
169+function 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+
179+function 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+
190+var 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+
290+var ELEMENT_NODE = 1;
291+var DOCUMENT_FRAGMENT_NODE$1 = 11;
292+var TEXT_NODE = 3;
293+var COMMENT_NODE = 8;
294+
295+function noop() {}
296+
297+function defaultGetNodeKey(node) {
298+ if (node) {
299+ return (node.getAttribute && node.getAttribute('id')) || node.id;
300+ }
301+}
302+
303+function 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+
767+var morphdom = morphdomFactory(morphAttrs);
768+
769+export default morphdom;
public/scripts/extensions/quick-reply/lib/morphdom.LICENSE.txt+21 -0
@@ -0,0 +1,21 @@
1+The MIT License (MIT)
2+
3+Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com)
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in
13+all copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+THE SOFTWARE.
21 \ No newline at end of file
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+9 -0
@@ -83,6 +83,15 @@ export class AutoExecuteHandler {
8383 await this.performAutoExecute(qrList);
8484 }
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+
8695 /**
8796 * @param {any[]} entries Set of activated entries
8897 */
public/scripts/extensions/quick-reply/src/QuickReply.js+1268 -70
@@ -1,10 +1,17 @@
11import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js';
22import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
33import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
4+import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js';
5+import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
6+import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js';
7+import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
8+import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js';
9+import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
410import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
511import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
612import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
713import { log, quickReplyApi, warn } from '../index.js';
14+import morphdom from '../lib/morphdom-esm.js';
815import { QuickReplyContextLink } from './QuickReplyContextLink.js';
916import { QuickReplySet } from './QuickReplySet.js';
1017import { ContextMenu } from './ui/ctx/ContextMenu.js';
@@ -21,48 +28,62 @@ export class QuickReply {
2128
2229
2330
2431 /**@type {Numbernumber}*/ id;
2532 /**@type {Stringstring}*/ label = ''icon;
2633 /**@type {Stringstring}*/ titlelabel = '';
2734 /**@type {Stringboolean}*/ messageshowLabel = ''false;
35+ /**@type {string}*/ title = '';
36+ /**@type {string}*/ message = '';
2837
2938 /**@type {QuickReplyContextLink[]}*/ contextList;
3039
3140 /**@type {Booleanboolean}*/ preventAutoExecute = true;
3241 /**@type {Booleanboolean}*/ isHidden = false;
3342 /**@type {Booleanboolean}*/ executeOnStartup = false;
3443 /**@type {Booleanboolean}*/ executeOnUser = false;
3544 /**@type {Booleanboolean}*/ executeOnAi = false;
3645 /**@type {Booleanboolean}*/ executeOnChatChange = false;
3746 /**@type {Booleanboolean}*/ executeOnGroupMemberDraft = false;
3847 /**@type {Stringboolean}*/ automationIdexecuteOnNewChat = ''false;
48+ /**@type {string}*/ automationId = '';
3949
4050 /**@type {Functionfunction}*/ onExecute;
41- /**@type {Function}*/ onDelete;
51+ /**@type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>}*/ onDebug;
4252 /**@type {Functionfunction}*/ onUpdateonDelete;
53+ /**@type {function}*/ onUpdate;
54+ /**@type {function}*/ onInsertBefore;
55+ /**@type {function}*/ onTransfer;
4356
4457
4558 /**@type {HTMLElement}*/ dom;
59+ /**@type {HTMLElement}*/ domIcon;
4660 /**@type {HTMLElement}*/ domLabel;
4761 /**@type {HTMLElement}*/ settingsDom;
62+ /**@type {HTMLElement}*/ settingsDomIcon;
4863 /**@type {HTMLInputElement}*/ settingsDomLabel;
4964 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;
5065
5166 /**@type {Popup}*/ editorPopup;
67+ /**@type {HTMLElement}*/ editorDom;
5268
69+ /**@type {HTMLTextAreaElement}*/ editorMessage;
70+ /**@type {HTMLTextAreaElement}*/ editorMessageLabel;
71+ /**@type {HTMLElement}*/ editorSyntax;
5372 /**@type {HTMLElement}*/ editorExecuteBtn;
5473 /**@type {HTMLElement}*/ editorExecuteBtnPause;
5574 /**@type {HTMLElement}*/ editorExecuteBtnStop;
5675 /**@type {HTMLElement}*/ editorExecuteProgress;
5776 /**@type {HTMLElement}*/ editorExecuteErrors;
5877 /**@type {HTMLElement}*/ editorExecuteResult;
5978 /**@type {HTMLInputElementHTMLElement}*/ editorExecuteHideeditorDebugState;
6079 /**@type {Promise}*/ editorExecutePromise;
80+ /**@type {boolean}*/ isExecuting;
6181 /**@type {SlashCommandAbortController}*/ abortController;
82+ /**@type {SlashCommandDebugController}*/ debugController;
6283
6384
6485 get hasContext() {
6586 return this.contextList && this.contextList.filter(it => it.set).length > 0;
6687 }
6788
6889
@@ -75,6 +96,14 @@ export class QuickReply {
7596 updateRender() {
7697 if (!this.dom) return;
7798 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+ }
78107 this.domLabel.textContent = this.label;
79108 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');
80109 }
@@ -105,9 +134,18 @@ export class QuickReply {
105134 }
106135 this.execute();
107136 });
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+ }
108145 const lbl = document.createElement('div'); {
109146 this.domLabel = lbl;
110147 lbl.classList.add('qr--button-label');
148+ if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
111149 lbl.textContent = this.label;
112150 root.append(lbl);
113151 }
@@ -138,14 +176,90 @@ export class QuickReply {
138176 item.classList.add('qr--set-item');
139177 item.setAttribute('data-order', String(idx));
140178 item.setAttribute('data-id', String(this.id));
179+ const adder = document.createElement('div'); {
180+ adder.classList.add('qr--set-itemAdder');
181+ const actions = document.createElement('div'); {
182+ actions.classList.add('qr--actions');
183+ const addNew = document.createElement('div'); {
184+ addNew.classList.add('qr--action');
185+ addNew.classList.add('qr--add');
186+ addNew.classList.add('menu_button');
187+ addNew.classList.add('menu_button_icon');
188+ addNew.classList.add('fa-solid');
189+ addNew.classList.add('fa-plus');
190+ addNew.title = 'Add quick reply';
191+ addNew.addEventListener('click', ()=>this.onInsertBefore());
192+ actions.append(addNew);
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');
141239 const drag = document.createElement('div'); {
142240 drag.classList.add('drag-handle');
143241 drag.classList.add('ui-sortable-handle');
144242 drag.textContent = '☰';
145243 item itemContent.append(drag);
146244 }
147245 const lblContainer = document.createElement('div'); {
148246 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+ }
149263 const lbl = document.createElement('input'); {
150264 this.settingsDomLabel = lbl;
151265 lbl.classList.add('qr--set-itemLabel');
@@ -154,20 +268,23 @@ export class QuickReply {
154268 lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
155269 lblContainer.append(lbl);
156270 }
157271 item itemContent.append(lblContainer);
272+ }
273+ item.append(itemContent);
158274 }
159275 const optContainer = document.createElement('div'); {
160276 optContainer.classList.add('qr--set-optionsContainer');
161277 const opt = document.createElement('div'); {
162278 opt.classList.add('qr--action');
163279 opt.classList.add('menu_button');
280+ opt.classList.add('fa-fw');
164281 opt.classList.add('fa-solid');
165282 opt.textContent = '⁝';
166283 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';
167284 opt.addEventListener('click', ()=>this.showEditor());
168285 optContainer.append(opt);
169286 }
170287 itemitemContent.append(optContainer);
171288 }
172289 const mes = document.createElement('textarea'); {
173290 this.settingsDomMessage = mes;
@@ -176,22 +293,89 @@ export class QuickReply {
176293 mes.value = this.message;
177294 //HACK need to use jQuery to catch the triggered event from the expanded editor
178295 $(mes).on('input', ()=>this.updateMessage(mes.value));
179296 itemitemContent.append(mes);
180297 }
181298 const actions = document.createElement('div'); {
182299 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+ }
183356 const del = document.createElement('div'); {
184357 del.classList.add('qr--action');
185358 del.classList.add('menu_button');
186359 del.classList.add('menu_button_iconfa-fw');
187360 del.classList.add('fa-solid');
188361 del.classList.add('fa-trash-can');
189362 del.classList.add('redWarningBG');
190363 del.title = 'Remove quickQuick replyReply\n---\nShit+Click to skip confirmation';
191364 del.addEventListener('click', async(evt)=>this.delete());{
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+ });
192376 actions.append(del);
193377 }
194378 itemitemContent.append(actions);
195379 }
196380 }
197381 }
@@ -208,22 +392,156 @@ export class QuickReply {
208392 /**@type {HTMLElement} */
209393 // @ts-ignore
210394 const dom = this.template.cloneNode(true);
395+ this.editorDom = dom;
211396 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
212397 const popupResult = this.editorPopup.show();
213398
214399 // 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+ });
215429 /**@type {HTMLInputElement}*/
216430 const label = dom.querySelector('#qr--modal-label');
217431 label.value = this.label;
218432 label.addEventListener('input', ()=>{
219433 this.updateLabel(label.value);
220434 });
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+ });
221536 /**@type {HTMLInputElement}*/
222537 const title = dom.querySelector('#qr--modal-title');
223538 title.value = this.title;
224539 title.addEventListener('input', () => {
225540 this.updateTitle(title.value);
226541 });
542+ /**@type {HTMLElement}*/
543+ const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
544+ this.editorSyntax = messageSyntaxInner;
227545 /**@type {HTMLInputElement}*/
228546 const wrap = dom.querySelector('#qr--modal-wrap');
229547 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');
@@ -235,9 +553,15 @@ export class QuickReply {
235553 if (wrap.checked) {
236554 message.style.whiteSpace = 'pre-wrap';
237555 messageSyntaxInner.style.whiteSpace = 'pre-wrap';
556+ if (this.clone) {
557+ this.clone.style.whiteSpace = 'pre-wrap';
558+ }
238559 } else {
239560 message.style.whiteSpace = 'pre';
240561 messageSyntaxInner.style.whiteSpace = 'pre';
562+ if (this.clone) {
563+ this.clone.style.whiteSpace = 'pre';
564+ }
241565 }
242566 updateScrollDebounced();
243567 };
@@ -261,11 +585,8 @@ export class QuickReply {
261585 });
262586 };
263587 const updateScrollDebounced = updateScroll;
264- const updateSyntax = ()=>{
265- messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
266- };
267588 const updateSyntaxEnabled = ()=>{
268- if (JSON.parse(localStorage.getItem('qr--syntax'))) {
589+ if (syntax.checked) {
269590 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
270591 } else {
271592 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
@@ -296,48 +617,108 @@ export class QuickReply {
296617 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
297618 updateSyntaxEnabled();
298619 });
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"]');
299626 /**@type {HTMLTextAreaElement}*/
300627 const message = dom.querySelector('#qr--modal-message');
628+ this.editorMessage = message;
301629 message.value = this.message;
630+ const updateMessageDebounced = debounce((value)=>this.updateMessage(value), 10);
302631 message.addEventListener('input', () => {
303632 updateSyntaxupdateMessageDebounced(message.value);
304- this.updateMessage(message.value);
305633 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_maximize
636+ 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+ };
309650 message.addEventListener('keydown', async(evt) => {
651+ if (this.isExecuting) return;
310652 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
653+ // increase indent
311654 evt.preventDefault();
312655 const start = message.selectionStart;
313656 const end = message.selectionEnd;
314657 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();
318661 message.selectionStart = start + 1lineStart;
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)}`;
323665 message.selectionStart = start + 1;
324666 message.selectionEnd = end + 1affectedLines.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 }));
326674 }
327675 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
676+ // decrease indent
328677 evt.preventDefault();
678+ evt.stopImmediatePropagation();
679+ evt.stopPropagation();
329680 const start = message.selectionStart;
330681 const end = message.selectionEnd;
331682 const lineStart = message.value.lastIndexOfgetLineStart('\n', start);
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
337- } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
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();
338708 evt.stopPropagation();
339709 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+ }
716+ } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
340717 if (executeShortcut.checked) {
718+ // execute QR
719+ evt.stopImmediatePropagation();
720+ evt.stopPropagation();
721+ evt.preventDefault();
341722 const selectionStart = message.selectionStart;
342723 const selectionEnd = message.selectionEnd;
343724 message.blur();
@@ -348,17 +729,175 @@ export class QuickReply {
348729 message.selectionEnd = selectionEnd;
349730 }
350731 }
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 }));
351779 }
352780 });
781+ const ac = await setSlashCommandAutoComplete(message, true);
353782 message.addEventListener('wheel', (evt)=>{
354783 updateScrollDebounced(evt);
355784 });
356785 message.addEventListener('scroll', (evt)=>{
357786 updateScrollDebounced();
358787 });
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+ });
359899 /** @type {any} */
360900 const resizeListener = debounce((evt) => {
361- updateSyntax();
362901 updateScrollDebounced(evt);
363902 if (document.activeElement == message) {
364903 message.blur();
@@ -367,10 +906,43 @@ export class QuickReply {
367906 });
368907 window.addEventListener('resize', resizeListener);
369908 updateSyntaxEnabled();
370- message.style.setProperty('text-shadow', 'none', 'important');
909+ const updateSyntax = ()=>{
371- /**@type {HTMLElement}*/
910+ if (messageSyntaxInner && syntax.checked) {
372- const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
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;
373941 updateSyntax();
942+ requestAnimationFrame(updateSyntaxLoop);
943+ };
944+ requestAnimationFrame(()=>updateSyntaxLoop());
945+ message.style.setProperty('text-shadow', 'none', 'important');
374946 updateWrap();
375947 updateTabSize();
376948
@@ -379,7 +951,7 @@ export class QuickReply {
379951 const tpl = dom.querySelector('#qr--ctxItem');
380952 const linkList = dom.querySelector('#qr--ctxEditor');
381953 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
382954 [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => {
383955 const opt = document.createElement('option'); {
384956 opt.value = qrs.name;
385957 opt.textContent = qrs.name;
@@ -388,7 +960,7 @@ export class QuickReply {
388960 }
389961 });
390962 };
391963 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Numbernumber}*/idx) => {
392964 /**@type {HTMLElement} */
393965 // @ts-ignore
394966 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
@@ -490,6 +1062,13 @@ export class QuickReply {
4901062 this.updateContext();
4911063 });
4921064 /**@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}*/
4931072 const automationId = dom.querySelector('#qr--automationId');
4941073 automationId.value = this.automationId;
4951074 automationId.addEventListener('input', () => {
@@ -506,9 +1085,9 @@ export class QuickReply {
5061085 /**@type {HTMLElement}*/
5071086 const executeResult = dom.querySelector('#qr--modal-executeResult');
5081087 this.editorExecuteResult = executeResult;
5091088 /**@type {HTMLInputElementHTMLElement}*/
5101089 const executeHidedebugState = dom.querySelector('#qr--modal-executeHidedebugState');
5111090 this.editorExecuteHideeditorDebugState = executeHidedebugState;
5121091 /**@type {HTMLElement}*/
5131092 const executeBtn = dom.querySelector('#qr--modal-execute');
5141093 this.editorExecuteBtn = executeBtn;
@@ -536,6 +1115,76 @@ export class QuickReply {
5361115 this.abortController?.abort('Stop button clicked');
5371116 });
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+
5391188 await popupResult;
5401189
5411190 window.removeEventListener('resize', resizeListener);
@@ -544,8 +1193,62 @@ export class QuickReply {
5441193 }
5451194 }
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+ }
5471241 async executeFromEditor() {
5481242 if (this.editorExecutePromiseisExecuting) 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+ }
5491252 this.editorExecuteBtn.classList.add('qr--busy');
5501253 this.editorExecuteProgress.style.setProperty('--prog', '0');
5511254 this.editorExecuteErrors.classList.remove('qr--hasErrors');
@@ -556,12 +1259,451 @@ export class QuickReply {
5561259 this.editorExecuteProgress.classList.remove('qr--aborted');
5571260 this.editorExecuteErrors.innerHTML = '';
5581261 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) => {
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+ });
5611275 }
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+ });
5621289 try {
5631290 this.editorExecutePromiseabortController = this.execute({},new trueSlashCommandAbortController();
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);
5651707 if (this.abortController?.signal?.aborted) {
5661708 this.editorExecuteProgress.classList.add('qr--aborted');
5671709 } else {
@@ -586,9 +1728,18 @@ export class QuickReply {
5861728 `;
5871729 }
5881730 }
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 }));
5891738 this.editorExecutePromise = null;
5901739 this.editorExecuteBtn.classList.remove('qr--busy');
5911740 this.editorPopup.dlgeditorDom.classList.remove('qr--hideisExecuting');
1741+ this.isExecuting = false;
1742+ this.editorPopup.onClosing = null;
5921743 }
5931744
5941745 updateEditorProgress(done, total) {
@@ -623,6 +1774,47 @@ export class QuickReply {
6231774 /**
6241775 * @param {string} value
6251776 */
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+ */
6261818 updateLabel(value) {
6271819 if (this.onUpdate) {
6281820 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {
@@ -670,12 +1862,15 @@ export class QuickReply {
6701862 }
6711863
6721864
6731865 async execute(args = {}, isEditor = false, isRun = false, options = {}) {
6741866 if (this.message?.length > 0 && this.onExecute) {
6751867 const scope = new SlashCommandScope();
6761868 for (const key of Object.keys(args)) {
1869+ if (key[0] == '_') continue;
1870+ if (key == 'isAutoExecute') continue;
6771871 scope.setMacro(`arg::${key}`, args[key]);
6781872 }
1873+ scope.setMacro('arg::*', '');
6791874 if (isEditor) {
6801875 this.abortController = new SlashCommandAbortController();
6811876 }
@@ -685,6 +1880,7 @@ export class QuickReply {
6851880 isEditor,
6861881 isRun,
6871882 scope,
1883+ executionOptions: options,
6881884 });
6891885 }
6901886 }
@@ -695,6 +1891,8 @@ export class QuickReply {
6951891 toJSON() {
6961892 return {
6971893 id: this.id,
1894+ icon: this.icon,
1895+ showLabel: this.showLabel,
6981896 label: this.label,
6991897 title: this.title,
7001898 message: this.message,
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+6 -1
@@ -60,7 +60,12 @@ export class QuickReplyConfig {
6060 /**@type {HTMLElement}*/
6161 this.setListDom = root.querySelector('.qr--setList');
6262 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+ }
6469 });
6570 this.updateSetListDom();
6671 }
public/scripts/extensions/quick-reply/src/QuickReplySet.js+189 -18
@@ -1,7 +1,9 @@
11import { getRequestHeaders, substituteParams } from '../../../../script.js';
2+import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
23import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4+import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
35import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
46import { debounceAsync, log, warn } from '../index.js';
57import { QuickReply } from './QuickReply.js';
68
79export class QuickReplySet {
@@ -16,7 +18,7 @@ export class QuickReplySet {
1618 }
1719
1820 /**
1921 * @param {Stringstring} name - name of the QuickReplySet
2022 */
2123 static get(name) {
2224 return this.list.find(it=>it.name == name);
@@ -25,17 +27,19 @@ export class QuickReplySet {
2527
2628
2729
2830 /**@type {Stringstring}*/ name;
2931 /**@type {Booleanboolean}*/ disableSend = false;
3032 /**@type {Booleanboolean}*/ placeBeforeInput = false;
3133 /**@type {Booleanboolean}*/ injectInput = false;
34+ /**@type {string}*/ color = 'transparent';
35+ /**@type {boolean}*/ onlyBorderColor = false;
3236 /**@type {QuickReply[]}*/ qrList = [];
3337
3438 /**@type {Numbernumber}*/ idIndex = 0;
3539
3640 /**@type {Booleanboolean}*/ isDeleted = false;
3741
3842 /**@type {Functionfunction}*/ save;
3943
4044 /**@type {HTMLElement}*/ dom;
4145 /**@type {HTMLElement}*/ settingsDom;
@@ -64,6 +68,7 @@ export class QuickReplySet {
6468 const root = document.createElement('div'); {
6569 this.dom = root;
6670 root.classList.add('qr--buttons');
71+ this.updateColor();
6772 this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
6873 root.append(qr.render());
6974 });
@@ -78,6 +83,22 @@ export class QuickReplySet {
7883 this.dom.append(qr.render());
7984 });
8085 }
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 {
93114 }
94115 return this.settingsDom;
95116 }
117+ /**
118+ *
119+ * @param {QuickReply} qr
120+ * @param {number} idx
121+ */
96122 renderSettingsItem(qr, idx) {
97123 this.settingsDom.append(qr.renderSettings(idx));
98124 }
@@ -102,6 +128,18 @@ export class QuickReplySet {
102128
103129 /**
104130 *
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+ *
105143 * @param {QuickReply} qr The QR to execute.
106144 * @param {object} options
107145 * @param {string} [options.message] (null) altered message to be used
@@ -109,6 +147,7 @@ export class QuickReplySet {
109147 * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
110148 * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
111149 * @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
112151 * @returns
113152 */
114153 async executeWithOptions(qr, options = {}) {
@@ -118,7 +157,9 @@ export class QuickReplySet {
118157 isEditor:false,
119158 isRun:false,
120159 scope:null,
160+ executionOptions:{},
121161 }, options);
162+ const execOptions = options.executionOptions;
122163 /**@type {HTMLTextAreaElement}*/
123164 const ta = document.querySelector('#send_textarea');
124165 const finalMessage = options.message ?? qr.message;
@@ -136,21 +177,24 @@ export class QuickReplySet {
136177 if (input[0] == '/' && !this.disableSend) {
137178 let result;
138179 if (options.isAutoExecute || options.isRun) {
139180 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
140181 handleParserErrors: true,
141182 scope: options.scope,
142- });
183+ source: `${this.name}.${qr.label}`,
184+ }));
143185 } else if (options.isEditor) {
144186 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
145187 handleParserErrors: false,
146188 scope: options.scope,
147189 abortController: qr.abortController,
190+ source: `${this.name}.${qr.label}`,
148191 onProgress: (done, total) => qr.updateEditorProgress(done, total),
149192 }));
150193 } else {
151194 result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, {
152195 scope: options.scope,
153- });
196+ source: `${this.name}.${qr.label}`,
197+ }));
154198 }
155199 return typeof result === 'object' ? result?.pipe : '';
156200 }
@@ -165,7 +209,7 @@ export class QuickReplySet {
165209 }
166210 /**
167211 * @param {QuickReply} qr
168212 * @param {Stringstring} [message] - optional altered message to be used
169213 * @param {SlashCommandScope} [scope] - optional scope to be used when running the command
170214 */
171215 async execute(qr, message = null, isAutoExecute = false, scope = null) {
@@ -179,10 +223,11 @@ export class QuickReplySet {
179223
180224
181225
182226 addQuickReply(data = {}) {
183227 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228+ data.id =
184229 this.idIndex = id + 1;
185230 const qr = QuickReply.from({ id }data);
186231 this.qrList.push(qr);
187232 this.hookQuickReply(qr);
188233 if (this.settingsDom) {
@@ -194,11 +239,131 @@ export class QuickReplySet {
194239 this.save();
195240 return qr;
196241 }
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+ */
198273 hookQuickReply(qr) {
274+ qr.onDebug = ()=>this.debug(qr);
199275 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
200276 qr.onDelete = ()=>this.removeQuickReply(qr);
201277 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+ };
202367 }
203368
204369 removeQuickReply(qr) {
@@ -214,6 +379,8 @@ export class QuickReplySet {
214379 disableSend: this.disableSend,
215380 placeBeforeInput: this.placeBeforeInput,
216381 injectInput: this.injectInput,
382+ color: this.color,
383+ onlyBorderColor: this.onlyBorderColor,
217384 qrList: this.qrList,
218385 idIndex: this.idIndex,
219386 };
@@ -245,9 +412,13 @@ export class QuickReplySet {
245412 if (response.ok) {
246413 this.unrender();
247414 const idx = QuickReplySet.list.indexOf(this);
415+ if (idx > -1) {
248416 QuickReplySet.list.splice(idx, 1);
249417 this.isDeleted = true;
250418 } else {
419+ warn(`Deleted Quick Reply Set was not found in the list of sets: ${this.name}`);
420+ }
421+ } else {
251422 warn(`Failed to delete Quick Reply Set: ${this.name}`);
252423 }
253424 }
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js+1 -1
@@ -45,7 +45,7 @@ export class QuickReplySetLink {
4545 this.set = QuickReplySet.get(set.value);
4646 this.update();
4747 });
48- QuickReplySet.list.forEach(qrs=>{
48+ QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
4949 const opt = document.createElement('option'); {
5050 opt.value = qrs.name;
5151 opt.textContent = qrs.name;
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+259 -18
@@ -1,8 +1,12 @@
11import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
2+import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
23import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
4+import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
35import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';
6+import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
47import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';
58import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
9+import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
610import { isTrueBoolean } from '../../../utils.js';
711// eslint-disable-next-line no-unused-vars
812import { QuickReplyApi } from '../api/QuickReplyApi.js';
@@ -47,6 +51,13 @@ export class SlashCommandHandler {
4751 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);
4852 }) ?? [],
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+
5061 /** All QRs as a set.name string, to be able to execute, for example via the /run command */
5162 qrExecutables: () => {
5263 const globalSetList = this.api.settings.config.setList;
@@ -63,7 +74,7 @@ export class SlashCommandHandler {
6374 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),
6475 ];
6576 },
6677 };
6778
6879 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;
6980
@@ -234,8 +245,20 @@ export class SlashCommandHandler {
234245 name: 'label',
235246 description: 'text on the button, e.g., label=MyButton',
236247 typeList: [ARGUMENT_TYPE.STRING],
237248 isRequired: truefalse,
238249 enumProvider: localEnumProviders.qrLabelsqrEntries,
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,
239262 }),
240263 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
241264 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 {
247270 ];
248271 const qrUpdateArgs = [
249272 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+ }),
250280 ];
251281
252282 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
@@ -272,13 +302,61 @@ export class SlashCommandHandler {
272302 </div>
273303 `,
274304 }));
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+ }));
275346 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
276347 callback: (args, message) => {
277348 this.updateQuickReply(args, message);
278349 return '';
279350 },
280351 returns: 'updated quick reply',
281352 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+ })],
282360 unnamedArgumentList: [
283361 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),
284362 ],
@@ -315,6 +393,12 @@ export class SlashCommandHandler {
315393 typeList: [ARGUMENT_TYPE.STRING],
316394 enumProvider: localEnumProviders.qrEntries,
317395 }),
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+ }),
318402 ],
319403 unnamedArgumentList: [
320404 SlashCommandArgument.fromProps({
@@ -344,6 +428,12 @@ export class SlashCommandHandler {
344428 typeList: [ARGUMENT_TYPE.STRING],
345429 enumProvider: localEnumProviders.qrEntries,
346430 }),
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+ }),
347437 new SlashCommandNamedArgument(
348438 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
349439 ),
@@ -389,6 +479,12 @@ export class SlashCommandHandler {
389479 typeList: [ARGUMENT_TYPE.STRING],
390480 enumProvider: localEnumProviders.qrEntries,
391481 }),
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+ }),
392488 ],
393489 unnamedArgumentList: [
394490 SlashCommandArgument.fromProps({
@@ -425,6 +521,12 @@ export class SlashCommandHandler {
425521 isRequired: true,
426522 enumProvider: localEnumProviders.qrSets,
427523 }),
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+ }),
428530 ],
429531 unnamedArgumentList: [
430532 SlashCommandArgument.fromProps({
@@ -454,8 +556,8 @@ export class SlashCommandHandler {
454556 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
455557 ];
456558 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
457559 callback: async (args, name) => {
458560 await this.createSet(name, args);
459561 return '';
460562 },
461563 aliases: ['qr-presetadd'],
@@ -485,8 +587,8 @@ export class SlashCommandHandler {
485587 }));
486588
487589 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
488590 callback: async (args, name) => {
489591 await this.updateSet(name, args);
490592 return '';
491593 },
492594 aliases: ['qr-presetupdate'],
@@ -510,8 +612,8 @@ export class SlashCommandHandler {
510612 `,
511613 }));
512614 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
513615 callback: async (_, name) => {
514616 await this.deleteSet(name);
515617 return '';
516618 },
517619 aliases: ['qr-presetdelete'],
@@ -533,6 +635,134 @@ export class SlashCommandHandler {
533635 </div>
534636 `,
535637 }));
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+ }));
536766 }
537767
538768
@@ -618,6 +848,8 @@ export class SlashCommandHandler {
618848 args.set ?? '',
619849 args.label ?? '',
620850 {
851+ icon: args.icon,
852+ showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
621853 message: message ?? '',
622854 title: args.title,
623855 isHidden: isTrueBoolean(args.hidden),
@@ -633,12 +865,21 @@ export class SlashCommandHandler {
633865 toastr.error(ex.message);
634866 }
635867 }
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+ }
636875 updateQuickReply(args, message) {
637876 try {
638877 this.api.updateQuickReply(
639878 args.set ?? '',
640- args.label ?? '',
879+ args.id !== undefined ? Number(args.id) : (args.label ?? ''),
641880 {
881+ icon: args.icon,
882+ showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
642883 newLabel: args.newlabel,
643884 message: (message ?? '').trim().length > 0 ? message : undefined,
644885 title: args.title,
@@ -657,7 +898,7 @@ export class SlashCommandHandler {
657898 }
658899 deleteQuickReply(args, label) {
659900 try {
660901 this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label));
661902 } catch (ex) {
662903 toastr.error(ex.message);
663904 }
@@ -692,9 +933,9 @@ export class SlashCommandHandler {
692933 }
693934
694935
695936 async createSet(name, args) {
696937 try {
697938 await this.api.createSet(
698939 args.name ?? name ?? '',
699940 {
700941 disableSend: isTrueBoolean(args.nosend),
@@ -706,9 +947,9 @@ export class SlashCommandHandler {
706947 toastr.error(ex.message);
707948 }
708949 }
709950 async updateSet(name, args) {
710951 try {
711952 await this.api.updateSet(
712953 args.name ?? name ?? '',
713954 {
714955 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,
@@ -720,9 +961,9 @@ export class SlashCommandHandler {
720961 toastr.error(ex.message);
721962 }
722963 }
723964 async deleteSet(name) {
724965 try {
725966 await this.api.deleteSet(name ?? '');
726967 } catch (ex) {
727968 toastr.error(ex.message);
728969 }
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+54 -3
@@ -23,6 +23,8 @@ export class SettingsUi {
2323 /**@type {HTMLInputElement}*/ disableSend;
2424 /**@type {HTMLInputElement}*/ placeBeforeInput;
2525 /**@type {HTMLInputElement}*/ injectInput;
26+ /**@type {HTMLInputElement}*/ color;
27+ /**@type {HTMLInputElement}*/ onlyBorderColor;
2628 /**@type {HTMLSelectElement}*/ currentSet;
2729
2830
@@ -117,10 +119,29 @@ export class SettingsUi {
117119 this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
118120 this.currentQrSet.addQuickReply();
119121 });
122+ this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{
123+ const text = await navigator.clipboard.readText();
124+ this.currentQrSet.addQuickReplyFromText(text);
125+ });
126+ this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{
127+ const inp = document.createElement('input'); {
128+ inp.type = 'file';
129+ inp.accept = '.json';
130+ inp.addEventListener('change', async()=>{
131+ if (inp.files.length > 0) {
132+ for (const file of inp.files) {
133+ const text = await file.text();
134+ this.currentQrSet.addQuickReply(JSON.parse(text));
135+ }
136+ }
137+ });
138+ inp.click();
139+ }
140+ });
120141 this.qrList = this.dom.querySelector('#qr--set-qrList');
121142 this.currentSet = this.dom.querySelector('#qr--set');
122143 this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
123- QuickReplySet.list.forEach(qrs=>{
144+ QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
124145 const opt = document.createElement('option'); {
125146 opt.value = qrs.name;
126147 opt.textContent = qrs.name;
@@ -145,6 +166,34 @@ export class SettingsUi {
145166 qrs.injectInput = this.injectInput.checked;
146167 qrs.save();
147168 });
169+ let initialColorChange = true;
170+ this.color = this.dom.querySelector('#qr--color');
171+ this.color.color = this.currentQrSet?.color ?? 'transparent';
172+ this.color.addEventListener('change', (evt)=>{
173+ if (!this.dom.closest('body')) return;
174+ const qrs = this.currentQrSet;
175+ if (initialColorChange) {
176+ initialColorChange = false;
177+ this.color.color = qrs.color;
178+ return;
179+ }
180+ qrs.color = evt.detail.rgb;
181+ qrs.save();
182+ this.currentQrSet.updateColor();
183+ });
184+ this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
185+ const qrs = this.currentQrSet;
186+ this.color.color = 'transparent';
187+ qrs.save();
188+ this.currentQrSet.updateColor();
189+ });
190+ this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor');
191+ this.onlyBorderColor.addEventListener('click', ()=>{
192+ const qrs = this.currentQrSet;
193+ qrs.onlyBorderColor = this.onlyBorderColor.checked;
194+ qrs.save();
195+ this.currentQrSet.updateColor();
196+ });
148197 this.onQrSetChange();
149198 }
150199 onQrSetChange() {
@@ -152,6 +201,8 @@ export class SettingsUi {
152201 this.disableSend.checked = this.currentQrSet.disableSend;
153202 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
154203 this.injectInput.checked = this.currentQrSet.injectInput;
204+ this.color.color = this.currentQrSet.color ?? 'transparent';
205+ this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
155206 this.qrList.innerHTML = '';
156207 const qrsDom = this.currentQrSet.renderSettings();
157208 this.qrList.append(qrsDom);
@@ -265,7 +316,7 @@ export class SettingsUi {
265316 const qrs = new QuickReplySet();
266317 qrs.name = name;
267318 qrs.addQuickReply();
268319 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
269320 if (idx > -1) {
270321 QuickReplySet.list.splice(idx, 0, qrs);
271322 } else {
@@ -321,7 +372,7 @@ export class SettingsUi {
321372 this.prepareChatSetList();
322373 }
323374 } else {
324375 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
325376 if (idx > -1) {
326377 QuickReplySet.list.splice(idx, 0, qrs);
327378 } else {
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js+5 -0
@@ -33,11 +33,14 @@ export class ContextMenu {
3333 */
3434 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
3535 const tree = {
36+ icon: qr.icon,
37+ showLabel: qr.showLabel,
3638 label: qr.label,
3739 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
3840 children: [],
3941 };
4042 qr.contextList.forEach((cl) => {
43+ if (!cl.set) return;
4144 if (!hierarchy.includes(cl.set)) {
4245 const nextHierarchy = [...hierarchy, cl.set];
4346 const nextLabelHierarchy = [...labelHierarchy, tree.label];
@@ -45,6 +48,8 @@ export class ContextMenu {
4548 cl.set.qrList.forEach(subQr => {
4649 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
4750 tree.children.push(new MenuItem(
51+ subTree.icon,
52+ subTree.showLabel,
4853 subTree.label,
4954 subTree.message,
5055 (evt) => {
public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js+1 -1
@@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js';
22
33export class MenuHeader extends MenuItem {
44 constructor(/**@type {String}*/label) {
55 super(null, null, label, null, null);
66 }
77
88
public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js+34 -7
@@ -1,21 +1,34 @@
11import { SubMenu } from './SubMenu.js';
22
33export class MenuItem {
44 /**@type {Stringstring}*/ labelicon;
55 /**@type {Objectboolean}*/ valueshowLabel;
66 /**@type {Functionstring}*/ callbacklabel;
7+ /**@type {object}*/ value;
8+ /**@type {function}*/ callback;
79 /**@type {MenuItem[]}*/ childList = [];
810 /**@type {SubMenu}*/ subMenu;
911 /**@type {Booleanboolean}*/ isForceExpanded = false;
1012
1113 /**@type {HTMLElement}*/ root;
1214
1315 /**@type {Functionfunction}*/ 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;
1932 this.label = label;
2033 this.value = value;
2134 this.callback = callback;
@@ -33,7 +46,21 @@ export class MenuItem {
3346 if (this.callback) {
3447 item.addEventListener('click', (evt) => this.callback(evt, this));
3548 }
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+ }
3764 if (this.childList.length > 0) {
3865 item.classList.add('ctx-has-children');
3966 const sub = new SubMenu(this.childList);
public/scripts/extensions/quick-reply/style.css+603 -21
@@ -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+}
118#qr--bar {
219 outline: none;
320 margin: 0;
@@ -41,6 +58,7 @@
4158}
4259#qr--bar > .qr--buttons,
4360#qr--popout > .qr--body > .qr--buttons {
61+ --qr--color: transparent;
4462 margin: 0;
4563 padding: 0;
4664 display: flex;
@@ -49,10 +67,44 @@
4967 gap: 5px;
5068 width: 100%;
5169}
70+#qr--bar > .qr--buttons.qr--color,
71+#qr--popout > .qr--body > .qr--buttons.qr--color {
72+ background-color: var(--qr--color);
73+}
74+#qr--bar > .qr--buttons.qr--borderColor,
75+#qr--popout > .qr--body > .qr--buttons.qr--borderColor {
76+ background-color: transparent;
77+ border-left: 5px solid var(--qr--color);
78+ border-right: 5px solid var(--qr--color);
79+}
80+#qr--bar > .qr--buttons:has(.qr--buttons.qr--color),
81+#qr--popout > .qr--body > .qr--buttons:has(.qr--buttons.qr--color) {
82+ margin: 5px;
83+}
5284#qr--bar > .qr--buttons > .qr--buttons,
5385#qr--popout > .qr--body > .qr--buttons > .qr--buttons {
5486 display: contents;
5587}
88+#qr--bar > .qr--buttons > .qr--buttons.qr--color .qr--button:before,
89+#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color .qr--button:before {
90+ content: '';
91+ background-color: var(--qr--color);
92+ position: absolute;
93+ inset: -5px;
94+ z-index: -1;
95+}
96+#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before,
97+#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before {
98+ display: none;
99+}
100+#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
101+#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
102+#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after,
103+#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after {
104+ content: '';
105+ width: 5px;
106+ background-color: var(--qr--color);
107+}
56108#qr--bar > .qr--buttons .qr--button,
57109#qr--popout > .qr--body > .qr--buttons .qr--button {
58110 color: var(--SmartThemeBodyColor);
@@ -66,11 +118,19 @@
66118 align-items: center;
67119 justify-content: center;
68120 text-align: center;
121+ position: relative;
69122}
70123#qr--bar > .qr--buttons .qr--button:hover,
71124#qr--popout > .qr--body > .qr--buttons .qr--button:hover {
72125 opacitybackground-color: 1#4d4d4d;
73- filter: brightness(1.2);
126+}
127+#qr--bar > .qr--buttons .qr--button .qr--hidden,
128+#qr--popout > .qr--body > .qr--buttons .qr--button .qr--hidden {
129+ display: none;
130+}
131+#qr--bar > .qr--buttons .qr--button .qr--button-icon,
132+#qr--popout > .qr--body > .qr--buttons .qr--button .qr--button-icon {
133+ margin: 0 0.5em;
74134}
75135#qr--bar > .qr--buttons .qr--button > .qr--button-expander,
76136#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {
@@ -170,36 +230,80 @@
170230#qr--settings #qr--set-qrList .qr--set-qrListContents {
171231 padding: 0 0.5em;
172232}
173233#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder {
234+ display: flex;
235+ align-items: center;
236+ opacity: 0;
237+ transition: 100ms;
238+ margin: -2px 0 -11px 0;
239+ position: relative;
240+}
241+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions {
242+ display: flex;
243+ gap: 0.25em;
244+ flex: 0 0 auto;
245+}
246+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions .qr--action {
247+ margin: 0;
248+}
249+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:before,
250+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:after {
251+ content: "";
252+ display: block;
253+ flex: 1 1 auto;
254+ border: 1px solid;
255+ margin: 0 1em;
256+ height: 0;
257+}
258+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:hover,
259+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:focus-within {
260+ opacity: 1;
261+}
262+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content {
174263 display: flex;
175264 flex-direction: row;
176265 gap: 0.5em;
177266 align-items: baseline;
178267 padding: 0.25em 0;
179268}
180269#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(12) {
181270 flex: 0 0 auto;
182271}
183272#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
184273 flex: 1 1 25%;
185274}
186275#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) {
187276 flex: 0 0 auto;
188277}
189278#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) {
190279 flex: 1 1 75%;
191280}
192281#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) {
193282 flex: 0 01 auto;
283+ display: flex;
284+ gap: 0.25em;
285+ justify-content: flex-end;
286+ flex-wrap: wrap;
194287}
195288#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle {
196289 padding: 0.75em;
197290}
198291#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,itemLabelContainer {
199-#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {
292+ display: flex;
293+ align-items: center;
294+ gap: 0.5em;
295+}
296+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) {
297+ display: none;
298+}
299+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemLabel {
300+ min-width: 24px;
301+}
302+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,
303+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--action {
200304 margin: 0;
201305}
202306#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage {
203307 font-size: smaller;
204308}
205309#qr--settings .qr--set-qrListActions {
@@ -212,6 +316,7 @@
212316#qr--qrOptions {
213317 display: flex;
214318 flex-direction: column;
319+ padding-right: 1px;
215320}
216321#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
217322 display: flex;
@@ -219,6 +324,12 @@
219324 gap: 0.5em;
220325 align-items: baseline;
221326}
327+#qr--qrOptions > #qr--autoExec .checkbox_label {
328+ text-wrap: nowrap;
329+}
330+#qr--qrOptions > #qr--autoExec .checkbox_label .fa-fw {
331+ margin-right: 2px;
332+}
222333@media screen and (max-width: 750px) {
223334 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
224335 flex-direction: column;
@@ -231,13 +342,79 @@
231342 flex-direction: column;
232343 }
233344 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
234345 min-height: 50svh50dvh;
235346 height: 50svh50dvh;
236347 }
237348}
238349.popup:has(#qr--modalEditor) {
239350 aspect-ratio: unset;
240351}
352+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {
353+ min-width: unset;
354+ min-height: unset;
355+ height: auto !important;
356+ width: min-content !important;
357+ position: absolute;
358+ right: 1em;
359+ top: 1em;
360+ left: unset;
361+ bottom: unset;
362+ margin: unset;
363+ padding: 0;
364+}
365+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized)::backdrop {
366+ backdrop-filter: unset;
367+ background-color: transparent;
368+}
369+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-body {
370+ flex: 0 0 auto;
371+ height: min-content;
372+ width: min-content;
373+}
374+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content {
375+ flex: 0 0 auto;
376+ margin-top: 0;
377+}
378+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor {
379+ max-height: 50vh;
380+}
381+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--main,
382+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--resizeHandle,
383+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > h3,
384+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeButtons,
385+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeProgress {
386+ display: none;
387+}
388+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--qrOptions {
389+ width: auto;
390+}
391+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
392+ display: flex;
393+}
394+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
395+ display: none;
396+}
397+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugState {
398+ padding-top: 0;
399+}
400+.popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls {
401+ display: none;
402+}
403+.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight {
404+ position: absolute;
405+ z-index: 50000;
406+ pointer-events: none;
407+ background-color: rgba(47, 150, 180, 0.5);
408+}
409+.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight.qr--unresolved {
410+ background-color: rgba(255, 255, 0, 0.5);
411+}
412+.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary {
413+ position: absolute;
414+ z-index: 50000;
415+ pointer-events: none;
416+ border: 3px solid red;
417+}
241418.popup:has(#qr--modalEditor) .popup-content {
242419 display: flex;
243420 flex-direction: column;
@@ -249,6 +426,67 @@
249426 gap: 1em;
250427 overflow: hidden;
251428}
429+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > h3:first-child,
430+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--labels,
431+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
432+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > h3:first-child,
433+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > #qr--ctxEditor,
434+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions,
435+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3,
436+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3 + div {
437+ display: none;
438+}
439+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
440+ visibility: hidden;
441+}
442+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons {
443+ display: flex;
444+}
445+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
446+ cursor: not-allowed;
447+ opacity: 0.5;
448+ pointer-events: none;
449+ transition: 200ms;
450+ border-color: transparent;
451+}
452+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
453+ cursor: pointer;
454+ opacity: 1;
455+ pointer-events: all;
456+}
457+.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 {
458+ animation-name: qr--debugPulse;
459+ animation-duration: 1500ms;
460+ animation-timing-function: ease-in-out;
461+ animation-delay: 0s;
462+ animation-iteration-count: infinite;
463+}
464+.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 {
465+ border-color: #51a351;
466+}
467+.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 {
468+ border-color: var(--SmartThemeQuoteColor);
469+}
470+.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 {
471+ border-color: var(--SmartThemeQuoteColor);
472+}
473+.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 {
474+ border-color: var(--SmartThemeQuoteColor);
475+}
476+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle {
477+ width: 6px;
478+ background-color: var(--SmartThemeBorderColor);
479+ border: 2px solid var(--SmartThemeBlurTintColor);
480+ transition: border-color 200ms, background-color 200ms;
481+ cursor: w-resize;
482+}
483+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle:hover {
484+ background-color: var(--SmartThemeQuoteColor);
485+ border-color: var(--SmartThemeQuoteColor);
486+}
487+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions {
488+ width: var(--width, auto);
489+}
252490.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
253491 flex: 1 1 auto;
254492 display: flex;
@@ -260,21 +498,115 @@
260498 display: flex;
261499 flex-direction: row;
262500 gap: 0.5em;
501+ padding: 1px;
263502}
264503.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {,
504+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label {
265505 flex: 1 1 1px;
266506 display: flex;
267507 flex-direction: column;
508+ position: relative;
509+}
510+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label.qr--fit,
511+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label.qr--fit {
512+ flex: 0 0 auto;
513+ justify-content: center;
514+}
515+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup,
516+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup {
517+ display: flex;
518+ align-items: baseline;
519+ gap: 0.5em;
520+}
521+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup input,
522+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup input {
523+ flex: 1 1 auto;
268524}
269525.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {,
526+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelText {
270527 flex: 1 1 auto;
271528}
272529.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {,
530+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelHint {
273531 flex: 1 1 auto;
274532}
275533.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {,
534+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label input {
276535 flex: 0 0 auto;
277536}
537+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList,
538+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList {
539+ background-color: var(--stcdx--bgColor);
540+ border: 1px solid var(--SmartThemeBorderColor);
541+ backdrop-filter: blur(var(--SmartThemeBlurStrength));
542+ border-radius: 10px;
543+ font-size: smaller;
544+ position: absolute;
545+ top: 100%;
546+ left: 0;
547+ right: 0;
548+ overflow: auto;
549+ margin: 0;
550+ padding: 0.5em;
551+ max-height: 50vh;
552+ list-style: none;
553+ z-index: 40000;
554+ max-width: 100%;
555+}
556+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem,
557+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem {
558+ display: flex;
559+ gap: 1em;
560+ text-align: left;
561+ opacity: 0.75;
562+ transition: 200ms;
563+ cursor: pointer;
564+}
565+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem:hover,
566+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem:hover {
567+ opacity: 1;
568+}
569+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current,
570+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current {
571+ opacity: 1;
572+}
573+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
574+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
575+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id,
576+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id {
577+ font-weight: bold;
578+}
579+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label,
580+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label {
581+ white-space: nowrap;
582+}
583+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label .menu_button,
584+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label .menu_button {
585+ display: inline-block;
586+ height: min-content;
587+ width: min-content;
588+ margin: 0 0.5em 0 0;
589+}
590+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id,
591+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id {
592+ opacity: 0.5;
593+}
594+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:before,
595+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:before {
596+ content: "[";
597+}
598+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:after,
599+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:after {
600+ content: "]";
601+}
602+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--message,
603+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--message {
604+ height: 1lh;
605+ overflow: hidden;
606+ text-overflow: ellipsis;
607+ white-space: nowrap;
608+ opacity: 0.5;
609+}
278610.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
279611 flex: 1 1 auto;
280612 display: flex;
@@ -283,8 +615,9 @@
283615}
284616.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
285617 display: flex;
618+ flex-wrap: wrap;
286619 flex-direction: row;
287620 column-gap: 1em;
288621 color: var(--grey70);
289622 font-size: smaller;
290623 align-items: baseline;
@@ -308,6 +641,11 @@
308641 background-color: var(--ac-style-color-background);
309642 color: var(--ac-style-color-text);
310643}
644+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar,
645+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar-thumb {
646+ visibility: visible;
647+ cursor: unset;
648+}
311649.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
312650 color: unset;
313651 background-color: rgba(108 171 251 / 0.25);
@@ -357,11 +695,15 @@
357695 font-family: var(--monoFontFamily);
358696 padding: 0.75em;
359697 margin: 0;
360- border: none;
361698 resize: none;
362699 line-height: 1.2;
363700 border: 1px solid var(--SmartThemeBorderColor);
364701 border-radius: 5px;
702+ position: relative;
703+}
704+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-icon {
705+ height: 100%;
706+ aspect-ratio: 1 / 1;
365707}
366708.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {
367709 display: flex;
@@ -410,6 +752,46 @@
410752.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
411753 border-color: #d78872;
412754}
755+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons {
756+ display: none;
757+ gap: 1em;
758+}
759+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton {
760+ aspect-ratio: 1.25 / 1;
761+ width: 2.25em;
762+ position: relative;
763+}
764+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid) {
765+ border-width: 1px;
766+ border-style: solid;
767+}
768+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid):after {
769+ content: '';
770+ position: absolute;
771+ inset: 3px;
772+ background-color: var(--SmartThemeBodyColor);
773+ mask-size: contain;
774+ mask-position: center;
775+ mask-repeat: no-repeat;
776+}
777+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-resume:after {
778+ mask-image: url('/img/step-resume.svg');
779+}
780+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-step:after {
781+ mask-image: url('/img/step-over.svg');
782+}
783+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepInto:after {
784+ mask-image: url('/img/step-into.svg');
785+}
786+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepOut:after {
787+ mask-image: url('/img/step-out.svg');
788+}
789+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
790+ display: none;
791+}
792+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-send_textarea {
793+ flex: 0 0 auto;
794+}
413795.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {
414796 --prog: 0;
415797 --progColor: #92befc;
@@ -417,6 +799,7 @@
417799 --progSuccessColor: #51a351;
418800 --progErrorColor: #bd362f;
419801 --progAbortedColor: #d78872;
802+ flex: 0 0 auto;
420803 height: 0.5em;
421804 background-color: var(--black50a);
422805 position: relative;
@@ -469,6 +852,7 @@
469852 overflow: auto;
470853 min-width: 100%;
471854 width: 0;
855+ white-space: pre-wrap;
472856}
473857.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
474858 display: block;
@@ -476,6 +860,150 @@
476860.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {
477861 content: 'Result: ';
478862}
863+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState {
864+ display: none;
865+ text-align: left;
866+ font-size: smaller;
867+ font-family: var(--monoFontFamily);
868+ color: white;
869+ padding: 0.5em 0;
870+ overflow: auto;
871+ min-width: 100%;
872+ width: 0;
873+ white-space: pre-wrap;
874+}
875+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState.qr--active {
876+ display: block;
877+}
878+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope {
879+ display: grid;
880+ grid-template-columns: 0fr 1fr 1fr;
881+ column-gap: 0em;
882+}
883+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title {
884+ grid-column: 1 / 4;
885+ font-weight: bold;
886+ font-family: var(--mainFontFamily);
887+ background-color: var(--black50a);
888+ padding: 0.25em;
889+ margin-top: 0.5em;
890+}
891+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var,
892+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro,
893+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe {
894+ display: contents;
895+}
896+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--key,
897+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--key,
898+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--key,
899+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val,
900+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val,
901+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val {
902+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
903+}
904+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:nth-child(2n),
905+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:nth-child(2n),
906+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:nth-child(2n) {
907+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
908+}
909+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:hover,
910+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:hover,
911+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:hover {
912+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
913+}
914+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:nth-child(2n),
915+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:nth-child(2n),
916+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:nth-child(2n) {
917+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
918+}
919+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:hover,
920+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:hover,
921+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:hover {
922+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
923+}
924+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--key,
925+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key,
926+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key,
927+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--val,
928+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--val,
929+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--val {
930+ opacity: 0.5;
931+}
932+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val,
933+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val,
934+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val {
935+ grid-column: 2 / 4;
936+}
937+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--singleCol,
938+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--singleCol,
939+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--singleCol {
940+ grid-column: unset;
941+}
942+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:before,
943+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:before,
944+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:before,
945+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:after,
946+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:after,
947+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:after {
948+ content: '"';
949+ color: var(--SmartThemeQuoteColor);
950+}
951+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--unresolved:after,
952+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--unresolved:after,
953+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--unresolved:after {
954+ content: '-UNRESOLVED-';
955+ font-style: italic;
956+ color: var(--SmartThemeQuoteColor);
957+}
958+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key {
959+ margin-left: 0.5em;
960+ padding-right: 1em;
961+}
962+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key:after {
963+ content: ": ";
964+}
965+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:before,
966+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:before {
967+ content: "{{";
968+}
969+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:after,
970+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:after {
971+ content: "}}: ";
972+}
973+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope {
974+ display: contents;
975+}
976+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--key,
977+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val {
978+ opacity: 0.5;
979+}
980+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack {
981+ display: grid;
982+ grid-template-columns: 1fr 0fr;
983+}
984+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title {
985+ grid-column: 1 / 3;
986+ font-weight: bold;
987+ font-family: var(--mainFontFamily);
988+ background-color: var(--black50a);
989+ padding: 0.25em;
990+ margin-top: 1em;
991+}
992+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item {
993+ display: contents;
994+}
995+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--name,
996+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--source {
997+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
998+}
999+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--name {
1000+ margin-left: 0.5em;
1001+}
1002+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source {
1003+ opacity: 0.5;
1004+ text-align: right;
1005+ white-space: nowrap;
1006+}
4791007@keyframes qr--progressPulse {
4801008 0%,
4811009 100% {
@@ -485,9 +1013,63 @@
4851013 background-color: var(--progFlashColor);
4861014 }
4871015}
1016+@keyframes qr--debugPulse {
1017+ 0%,
1018+ 100% {
1019+ border-color: #51a351;
1020+ }
1021+ 50% {
1022+ border-color: #92befc;
1023+ }
1024+}
4881025.popup.qr--hide {
4891026 opacity: 0 !important;
4901027}
4911028.popup.qr--hide::backdrop {
4921029 opacity: 0 !important;
4931030}
1031+.popup.qr--hide::backdrop {
1032+ opacity: 0 !important;
1033+}
1034+.popup:has(.qr--transferModal) .popup-button-ok {
1035+ display: flex;
1036+ align-items: center;
1037+ flex-direction: column;
1038+ white-space: pre;
1039+ font-weight: normal;
1040+ box-shadow: 0 0 0;
1041+ transition: 200ms;
1042+}
1043+.popup:has(.qr--transferModal) .popup-button-ok:after {
1044+ content: 'Transfer';
1045+ height: 0;
1046+ overflow: hidden;
1047+ font-weight: bold;
1048+}
1049+.popup:has(.qr--transferModal) .qr--copy {
1050+ display: flex;
1051+ align-items: center;
1052+ flex-direction: column;
1053+ white-space: pre;
1054+ font-weight: normal;
1055+ box-shadow: 0 0 0;
1056+ transition: 200ms;
1057+}
1058+.popup:has(.qr--transferModal) .qr--copy:after {
1059+ content: 'Copy';
1060+ height: 0;
1061+ overflow: hidden;
1062+ font-weight: bold;
1063+}
1064+.popup:has(.qr--transferModal):has(.qr--transferSelect:focus) .popup-button-ok {
1065+ font-weight: bold;
1066+ box-shadow: 0 0 10px;
1067+}
1068+.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .popup-button-ok {
1069+ font-weight: normal;
1070+ box-shadow: 0 0 0;
1071+}
1072+.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .qr--copy {
1073+ font-weight: bold;
1074+ box-shadow: 0 0 10px;
1075+}
public/scripts/extensions/quick-reply/style.less+577 -39
@@ -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+}
116#qr--bar {
217 outline: none;
318 margin: 0;
@@ -50,6 +65,18 @@
5065#qr--bar,
5166#qr--popout>.qr--body {
5267 >.qr--buttons {
68+ --qr--color: transparent;
69+ &.qr--color {
70+ background-color: var(--qr--color);
71+ }
72+ &.qr--borderColor {
73+ background-color: transparent;
74+ border-left: 5px solid var(--qr--color);
75+ border-right: 5px solid var(--qr--color);
76+ }
77+ &:has(.qr--buttons.qr--color) {
78+ margin: 5px;
79+ }
5380 margin: 0;
5481 padding: 0;
5582 display: flex;
@@ -60,6 +87,25 @@
6087
6188 >.qr--buttons {
6289 display: contents;
90+ &.qr--color {
91+ .qr--button:before {
92+ content: '';
93+ background-color: var(--qr--color);
94+ position: absolute;
95+ inset: -5px;
96+ z-index: -1;
97+ }
98+ &.qr--borderColor {
99+ .qr--button:before {
100+ display: none;
101+ }
102+ &:before, &:after {
103+ content: '';
104+ width: 5px;
105+ background-color: var(--qr--color);
106+ }
107+ }
108+ }
63109 }
64110
65111 .qr--button {
@@ -75,10 +121,17 @@
75121 align-items: center;
76122 justify-content: center;
77123 text-align: center;
124+ position: relative;
78125
79126 &:hover {
80- opacity: 1;
127+ background-color: rgb(30% 30% 30%);
81- filter: brightness(1.2);
128+ }
129+
130+ .qr--hidden {
131+ display: none;
132+ }
133+ .qr--button-icon {
134+ margin: 0 0.5em;
82135 }
83136
84137 >.qr--button-expander {
@@ -211,14 +264,41 @@
211264 .qr--set-qrListContents> {
212265 padding: 0 0.5em;
213266
214267 >.qr--set-item .qr--set-itemAdder {
268+ display: flex;
269+ align-items: center;
270+ opacity: 0;
271+ transition: 100ms;
272+ margin: -2px 0 -11px 0;
273+ position: relative;
274+ .qr--actions {
275+ display: flex;
276+ gap: 0.25em;
277+ flex: 0 0 auto;
278+ .qr--action {
279+ margin: 0;
280+ }
281+ }
282+ &:before, &:after {
283+ content: "";
284+ display: block;
285+ flex: 1 1 auto;
286+ border: 1px solid;
287+ margin: 0 1em;
288+ height: 0;
289+ }
290+ &:hover, &:focus-within {
291+ opacity: 1;
292+ }
293+ }
294+ >.qr--set-item .qr--content {
215295 display: flex;
216296 flex-direction: row;
217297 gap: 0.5em;
218298 align-items: baseline;
219299 padding: 0.25em 0;
220300
221301 > :nth-child(12) {
222302 flex: 0 0 auto;
223303 }
224304
@@ -235,13 +315,29 @@
235315 }
236316
237317 > :nth-child(5) {
238318 flex: 0 01 auto;
319+ display: flex;
320+ gap: 0.25em;
321+ justify-content: flex-end;
322+ flex-wrap: wrap;
239323 }
240324
241325 >.drag-handle {
242326 padding: 0.75em;
243327 }
244328
329+ .qr--set-itemLabelContainer {
330+ display: flex;
331+ align-items: center;
332+ gap: 0.5em;
333+ .qr--set-itemIcon:not(.fa-solid) {
334+ display: none;
335+ }
336+ .qr--set-itemLabel {
337+ min-width: 24px;
338+ }
339+ }
340+
245341 .qr--set-itemLabel,
246342 .qr--action {
247343 margin: 0;
@@ -251,6 +347,8 @@
251347 font-size: smaller;
252348 }
253349 }
350+
351+
254352 }
255353 }
256354
@@ -270,6 +368,7 @@
270368#qr--qrOptions {
271369 display: flex;
272370 flex-direction: column;
371+ padding-right: 1px;
273372
274373 >#qr--ctxEditor {
275374 .qr--ctxItem {
@@ -279,6 +378,15 @@
279378 align-items: baseline;
280379 }
281380 }
381+ >#qr--autoExec {
382+ .checkbox_label {
383+ text-wrap: nowrap;
384+
385+ .fa-fw {
386+ margin-right: 2px;
387+ }
388+ }
389+ }
282390}
283391
284392
@@ -297,8 +405,8 @@
297405 }
298406
299407 >#qr--main>.qr--modal-messageContainer>#qr--modal-messageHolder {
300408 min-height: 50svh50dvh;
301409 height: 50svh50dvh;
302410 }
303411 }
304412}
@@ -306,6 +414,78 @@
306414.popup:has(#qr--modalEditor) {
307415 aspect-ratio: unset;
308416
417+ &:has(.qr--isExecuting.qr--minimized) {
418+ min-width: unset;
419+ min-height: unset;
420+ height: auto !important;
421+ width: min-content !important;
422+ position: absolute;
423+ right: 1em;
424+ top: 1em;
425+ left: unset;
426+ bottom: unset;
427+ margin: unset;
428+ padding: 0;
429+ &::backdrop {
430+ backdrop-filter: unset;
431+ background-color: transparent;
432+ }
433+ .popup-body {
434+ flex: 0 0 auto;
435+ height: min-content;
436+ width: min-content;
437+ }
438+ .popup-content {
439+ flex: 0 0 auto;
440+ margin-top: 0;
441+
442+ > #qr--modalEditor {
443+ max-height: 50vh;
444+ > #qr--main,
445+ > #qr--resizeHandle,
446+ > #qr--qrOptions > h3,
447+ > #qr--qrOptions > #qr--modal-executeButtons,
448+ > #qr--qrOptions > #qr--modal-executeProgress
449+ {
450+ display: none;
451+ }
452+ #qr--qrOptions {
453+ width: auto;
454+ }
455+ #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
456+ display: flex;
457+ }
458+ #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
459+ display: none;
460+ }
461+ #qr--modal-debugState {
462+ padding-top: 0;
463+ }
464+ }
465+ }
466+ }
467+ &:has(.qr--isExecuting) {
468+ .popup-controls {
469+ display: none;
470+ }
471+
472+ .qr--highlight {
473+ position: absolute;
474+ z-index: 50000;
475+ pointer-events: none;
476+ background-color: rgb(47 150 180 / 0.5);
477+ &.qr--unresolved {
478+ background-color: rgb(255 255 0 / 0.5);
479+ }
480+ }
481+ .qr--highlight-secondary {
482+ position: absolute;
483+ z-index: 50000;
484+ pointer-events: none;
485+ border: 3px solid red;
486+ }
487+ }
488+
309489 .popup-content {
310490 display: flex;
311491 flex-direction: column;
@@ -317,86 +497,213 @@
317497 gap: 1em;
318498 overflow: hidden;
319499
500+ &.qr--isExecuting {
501+ #qr--main > h3:first-child,
502+ #qr--main > .qr--labels,
503+ #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
504+ #qr--qrOptions > h3:first-child,
505+ #qr--qrOptions > #qr--ctxEditor,
506+ #qr--qrOptions > .qr--ctxEditorActions,
507+ #qr--qrOptions > .qr--ctxEditorActions + h3,
508+ #qr--qrOptions > .qr--ctxEditorActions + h3 + div
509+ {
510+ display: none;
511+ }
512+ #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
513+ visibility: hidden;
514+ }
515+ #qr--modal-debugButtons {
516+ display: flex;
517+ .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
518+ cursor: not-allowed;
519+ opacity: 0.5;
520+ pointer-events: none;
521+ transition: 200ms;
522+ border-color: transparent;
523+ }
524+ }
525+ &.qr--isPaused #qr--modal-debugButtons {
526+ .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
527+ cursor: pointer;
528+ opacity: 1;
529+ pointer-events: all;
530+ &#qr--modal-resume {
531+ animation-name: qr--debugPulse;
532+ animation-duration: 1500ms;
533+ animation-timing-function: ease-in-out;
534+ animation-delay: 0s;
535+ animation-iteration-count: infinite;
536+ }
537+ &#qr--modal-resume {
538+ border-color: rgb(81, 163, 81);
539+ }
540+ &#qr--modal-step {
541+ border-color: var(--SmartThemeQuoteColor);
542+ }
543+ &#qr--modal-stepInto {
544+ border-color: var(--SmartThemeQuoteColor);
545+ }
546+ &#qr--modal-stepOut {
547+ border-color: var(--SmartThemeQuoteColor);
548+ }
549+ }
550+ }
551+ #qr--resizeHandle {
552+ width: 6px;
553+ background-color: var(--SmartThemeBorderColor);
554+ border: 2px solid var(--SmartThemeBlurTintColor);
555+ transition: border-color 200ms, background-color 200ms;
556+ cursor: w-resize;
557+ &:hover {
558+ background-color: var(--SmartThemeQuoteColor);
559+ border-color: var(--SmartThemeQuoteColor);
560+ }
561+ }
562+ #qr--qrOptions {
563+ width: var(--width, auto);
564+ }
565+ }
566+
320567 > #qr--main {
321568 flex: 1 1 auto;
322569 display: flex;
323570 flex-direction: column;
324571 overflow: hidden;
325-
326572 > .qr--labels {
327573 flex: 0 0 auto;
328574 display: flex;
329575 flex-direction: row;
330576 gap: 0.5em;
331-
577+ padding: 1px;
332- >label {
578+ > label, > .label {
333579 flex: 1 1 1px;
334580 display: flex;
335581 flex-direction: column;
336-
582+ position: relative;
337583 >&.qr--labelTextfit {
584+ flex: 0 0 auto;
585+ justify-content: center;
586+ }
587+ .qr--inputGroup {
588+ display: flex;
589+ align-items: baseline;
590+ gap: 0.5em;
591+ input {
338592 flex: 1 1 auto;
339593 }
340-
594+ }
341595 > .qr--labelHintlabelText {
342596 flex: 1 1 auto;
343597 }
344-
598+ .qr--labelHint {
345- >input {
599+ flex: 1 1 auto;
600+ }
601+ input {
346602 flex: 0 0 auto;
347603 }
604+ .qr--modal-switcherList {
605+ background-color: var(--stcdx--bgColor);
606+ border: 1px solid var(--SmartThemeBorderColor);
607+ backdrop-filter: blur(var(--SmartThemeBlurStrength));
608+ border-radius: 10px;
609+ font-size: smaller;
610+ position: absolute;
611+ top: 100%;
612+ left: 0;
613+ right: 0;
614+ overflow: auto;
615+ margin: 0;
616+ padding: 0.5em;
617+ max-height: 50vh;
618+ list-style: none;
619+ z-index: 40000;
620+ max-width: 100%;
621+ .qr--modal-switcherItem {
622+ display: flex;
623+ gap: 1em;
624+ text-align: left;
625+ opacity: 0.75;
626+ transition: 200ms;
627+ cursor: pointer;
628+ &:hover {
629+ opacity: 1;
630+ }
631+ &.qr--current {
632+ opacity: 1;
633+ .qr--label, .qr--id {
634+ font-weight: bold;
635+ }
636+ }
637+ }
638+ .qr--label {
639+ white-space: nowrap;
640+ .menu_button {
641+ display: inline-block;
642+ height: min-content;
643+ width: min-content;
644+ margin: 0 0.5em 0 0;
645+ }
646+ }
647+ .qr--id {
648+ opacity: 0.5;
649+ &:before { content: "["; }
650+ &:after { content: "]"; }
651+ }
652+ .qr--message {
653+ height: 1lh;
654+ overflow: hidden;
655+ text-overflow: ellipsis;
656+ white-space: nowrap;
657+ opacity: 0.5;
658+ }
659+ }
348660 }
349661 }
350-
351662 > .qr--modal-messageContainer {
352663 flex: 1 1 auto;
353664 display: flex;
354665 flex-direction: column;
355666 overflow: hidden;
356-
357667 > .qr--modal-editorSettings {
358668 display: flex;
669+ flex-wrap: wrap;
359670 flex-direction: row;
360671 column-gap: 1em;
361672 color: var(--grey70);
362673 font-size: smaller;
363674 align-items: baseline;
364-
365675 > .checkbox_label {
366676 white-space: nowrap;
367-
368677 > input {
369678 font-size: inherit;
370679 }
371680 }
372681 }
373-
374682 > #qr--modal-messageHolder {
375683 flex: 1 1 auto;
376684 display: grid;
377685 text-align: left;
378686 overflow: hidden;
379-
380687 &.qr--noSyntax {
381688 > #qr--modal-messageSyntax {
382689 display: none;
383690 }
384-
385691 > #qr--modal-message {
386692 background-color: var(--ac-style-color-background);
387693 color: var(--ac-style-color-text);
388-
694+ &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
695+ visibility: visible;
696+ cursor: unset;
697+ }
389698 &::selection {
390699 color: unset;
391700 background-color: rgba(108 171 251 / 0.25);
392-
393701 @supports (color: rgb(from white r g b / 0.25)) {
394702 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
395703 }
396704 }
397705 }
398706 }
399-
400707 > #qr--modal-messageSyntax {
401708 grid-column: 1;
402709 grid-row: 1;
@@ -406,12 +713,10 @@
406713 overflow: hidden;
407714 min-width: 100%;
408715 width: 0;
409-
410716 > #qr--modal-messageSyntaxInner {
411717 height: 100%;
412718 }
413719 }
414-
415720 > #qr--modal-message {
416721 background-color: transparent;
417722 color: transparent;
@@ -419,38 +724,35 @@
419724 grid-row: 1;
420725 caret-color: var(--ac-style-color-text);
421726 overflow: auto;
422-
727+ &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
423- &::-webkit-scrollbar,
424- &::-webkit-scrollbar-thumb {
425728 visibility: hidden;
426729 cursor: default;
427730 }
428-
429731 &::selection {
430732 color: transparent;
431733 background-color: rgba(108 171 251 / 0.25);
432-
433734 @supports (color: rgb(from white r g b / 0.25)) {
434735 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
435736 }
436737 }
437738 }
438-
739+ #qr--modal-message, #qr--modal-messageSyntaxInner {
439- #qr--modal-message,
440- #qr--modal-messageSyntaxInner {
441740 font-family: var(--monoFontFamily);
442741 padding: 0.75em;
443742 margin: 0;
444- border: none;
445743 resize: none;
446744 line-height: 1.2;
447745 border: 1px solid var(--SmartThemeBorderColor);
448746 border-radius: 5px;
747+ position: relative;
449748 }
450749 }
451750 }
452751 }
453-
752+ #qr--modal-icon {
753+ height: 100%;
754+ aspect-ratio: 1 / 1;
755+ }
454756 #qr--modal-executeButtons {
455757 display: flex;
456758 gap: 1em;
@@ -510,6 +812,47 @@
510812 border-color: rgb(215, 136, 114);
511813 }
512814 }
815+ #qr--modal-debugButtons {
816+ display: none;
817+ gap: 1em;
818+ .qr--modal-debugButton {
819+ aspect-ratio: 1.25 / 1;
820+ width: 2.25em;
821+ position: relative;
822+ &:not(.fa-solid) {
823+ border-width: 1px;
824+ border-style: solid;
825+ &:after {
826+ content: '';
827+ position: absolute;
828+ inset: 3px;
829+ background-color: var(--SmartThemeBodyColor);
830+ mask-size: contain;
831+ mask-position: center;
832+ mask-repeat: no-repeat;
833+ }
834+ }
835+ &#qr--modal-resume:after {
836+ mask-image: url('/img/step-resume.svg');
837+ }
838+ &#qr--modal-step:after {
839+ mask-image: url('/img/step-over.svg');
840+ }
841+ &#qr--modal-stepInto:after {
842+ mask-image: url('/img/step-into.svg');
843+ }
844+ &#qr--modal-stepOut:after {
845+ mask-image: url('/img/step-out.svg');
846+ }
847+ &#qr--modal-maximize {
848+ display: none;
849+ }
850+ }
851+ }
852+
853+ #qr--modal-send_textarea {
854+ flex: 0 0 auto;
855+ }
513856
514857 #qr--modal-executeProgress {
515858 --prog: 0;
@@ -518,6 +861,7 @@
518861 --progSuccessColor: rgb(81, 163, 81);
519862 --progErrorColor: rgb(189, 54, 47);
520863 --progAbortedColor: rgb(215, 136, 114);
864+ flex: 0 0 auto;
521865 height: 0.5em;
522866 background-color: var(--black50a);
523867 position: relative;
@@ -588,6 +932,135 @@
588932 overflow: auto;
589933 min-width: 100%;
590934 width: 0;
935+ white-space: pre-wrap;
936+ }
937+ #qr--modal-debugState {
938+ display: none;
939+ &.qr--active {
940+ display: block;
941+ }
942+ text-align: left;
943+ font-size: smaller;
944+ font-family: var(--monoFontFamily);
945+ // background-color: rgb(146, 190, 252);
946+ color: white;
947+ padding: 0.5em 0;
948+ overflow: auto;
949+ min-width: 100%;
950+ width: 0;
951+ white-space: pre-wrap;
952+
953+ .qr--scope {
954+ display: grid;
955+ grid-template-columns: 0fr 1fr 1fr;
956+ column-gap: 0em;
957+ .qr--title {
958+ grid-column: 1 / 4;
959+ font-weight: bold;
960+ font-family: var(--mainFontFamily);
961+ background-color: var(--black50a);
962+ padding: 0.25em;
963+ margin-top: 0.5em;
964+ }
965+ .qr--var, .qr--macro, .qr--pipe {
966+ display: contents;
967+ &:nth-child(2n + 1) {
968+ .qr--key, .qr--val {
969+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
970+ }
971+ .qr--val {
972+ &:nth-child(2n) {
973+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
974+ }
975+ &:hover {
976+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
977+ }
978+ }
979+ }
980+ &:nth-child(2n) {
981+ .qr--val {
982+ &:nth-child(2n) {
983+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
984+ }
985+ &:hover {
986+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
987+ }
988+ }
989+ }
990+ &.qr--isHidden {
991+ .qr--key, .qr--val {
992+ opacity: 0.5;
993+ }
994+ }
995+ .qr--val {
996+ grid-column: 2 / 4;
997+ &.qr--singleCol {
998+ grid-column: unset;
999+ }
1000+ &.qr--simple {
1001+ &:before, &:after {
1002+ content: '"';
1003+ color: var(--SmartThemeQuoteColor);
1004+ }
1005+ }
1006+ &.qr--unresolved {
1007+ &:after {
1008+ content: '-UNRESOLVED-';
1009+ font-style: italic;
1010+ color: var(--SmartThemeQuoteColor);
1011+ }
1012+ }
1013+ }
1014+ }
1015+ .qr--key {
1016+ margin-left: 0.5em;
1017+ padding-right: 1em;
1018+ &:after { content: ": "; }
1019+ }
1020+ .qr--pipe, .qr--macro {
1021+ > .qr--key {
1022+ &:before { content: "{{"; }
1023+ &:after { content: "}}: "; }
1024+ }
1025+ }
1026+ .qr--scope {
1027+ display: contents;
1028+ .qr--pipe {
1029+ .qr--key, .qr--val {
1030+ opacity: 0.5;
1031+ }
1032+ }
1033+ }
1034+ }
1035+
1036+ .qr--stack {
1037+ display: grid;
1038+ grid-template-columns: 1fr 0fr;
1039+ .qr--title {
1040+ grid-column: 1 / 3;
1041+ font-weight: bold;
1042+ font-family: var(--mainFontFamily);
1043+ background-color: var(--black50a);
1044+ padding: 0.25em;
1045+ margin-top: 1em;
1046+ }
1047+ .qr--item {
1048+ display: contents;
1049+ &:nth-child(2n + 1) {
1050+ .qr--name, .qr--source {
1051+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
1052+ }
1053+ }
1054+ .qr--name {
1055+ margin-left: 0.5em;
1056+ }
1057+ .qr--source {
1058+ opacity: 0.5;
1059+ text-align: right;
1060+ white-space: nowrap;
1061+ }
1062+ }
1063+ }
5911064 }
5921065 }
5931066 }
@@ -605,10 +1078,75 @@
6051078 }
6061079}
6071080
1081+@keyframes qr--debugPulse {
1082+ 0%,
1083+ 100% {
1084+ border-color: rgb(81, 163, 81);
1085+ }
1086+
1087+ 50% {
1088+ border-color: rgb(146, 190, 252);
1089+ }
1090+}
1091+
6081092.popup.qr--hide {
6091093 opacity: 0 !important;
1094+ &::backdrop {
1095+ opacity: 0 !important;
1096+ }
6101097}
6111098
6121099.popup.qr--hide::backdrop {
6131100 opacity: 0 !important;
6141101}
1102+
1103+
1104+
1105+.popup:has(.qr--transferModal) {
1106+ .popup-button-ok {
1107+ &:after {
1108+ content: 'Transfer';
1109+ height: 0;
1110+ overflow: hidden;
1111+ font-weight: bold;
1112+ }
1113+ display: flex;
1114+ align-items: center;
1115+ flex-direction: column;
1116+ white-space: pre;
1117+ font-weight: normal;
1118+ box-shadow: 0 0 0;
1119+ transition: 200ms;
1120+ }
1121+ .qr--copy {
1122+ &:after {
1123+ content: 'Copy';
1124+ height: 0;
1125+ overflow: hidden;
1126+ font-weight: bold;
1127+ }
1128+ display: flex;
1129+ align-items: center;
1130+ flex-direction: column;
1131+ white-space: pre;
1132+ font-weight: normal;
1133+ box-shadow: 0 0 0;
1134+ transition: 200ms;
1135+ }
1136+ &:has(.qr--transferSelect:focus) {
1137+ .popup-button-ok {
1138+ font-weight: bold;
1139+ box-shadow: 0 0 10px;
1140+ }
1141+ &.qr--isCopy {
1142+ .popup-button-ok {
1143+ font-weight: normal;
1144+ box-shadow: 0 0 0;
1145+ }
1146+ .qr--copy {
1147+ font-weight: bold;
1148+ box-shadow: 0 0 10px;
1149+ }
1150+ }
1151+ }
1152+}
public/scripts/extensions/regex/editor.html+15 -22
@@ -54,12 +54,7 @@
5454 <small data-i18n="Replace With">Replace With</small>
5555 </label>
5656 <div>
57- <textarea
57+ <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>
6358 </div>
6459 </div>
6560 <div class="flex1">
@@ -67,11 +62,7 @@
6762 <small data-i18n="Trim Out">Trim Out</small>
6863 </label>
6964 <div>
70- <textarea
65+ <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>
7566 </div>
7667 </div>
7768 </div>
@@ -126,17 +117,6 @@
126117 <input type="checkbox" name="disabled" />
127118 <span data-i18n="Disabled">Disabled</span>
128119 </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>
140120 <label class="checkbox flex-container">
141121 <input type="checkbox" name="run_on_edit" />
142122 <span data-i18n="Run On Edit">Run On Edit</span>
@@ -148,6 +128,19 @@
148128 <span class="fa-solid fa-circle-question note-link-span"></span>
149129 </span>
150130 </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+
151144 </div>
152145 </div>
153146 </div>
public/scripts/extensions/shared.js+88 -1
@@ -1,5 +1,5 @@
11import { getRequestHeaders } from '../../script.js';
22import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
33import { oai_settings } from '../openai.js';
44import { SECRET_KEYS, secret_state } from '../secrets.js';
55import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@@ -136,6 +136,10 @@ function throwIfInvalidModel(useReverseProxy) {
136136 throw new Error('Anthropic (Claude) API key is not set.');
137137 }
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+
139143 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
140144 throw new Error('MakerSuite API key is not set.');
141145 }
@@ -172,3 +176,86 @@ function throwIfInvalidModel(useReverseProxy) {
172176 throw new Error('Custom API URL is not set.');
173177 }
174178}
179+
180+/**
181+ * Check if the WebLLM extension is installed and supported.
182+ * @returns {boolean} Whether the extension is installed and supported
183+ */
184+export 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+ */
221+export 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+ */
238+export 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+ */
252+export 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 @@
11<div id="sd_gen" class="list-group-item flex-container flexGap5">
22 <div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion" /></div>
33 <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>
48</div>
public/scripts/extensions/stable-diffusion/index.js+9 -7
@@ -30,13 +30,14 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
3030import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
3131import { debounce_timeout } from '../../constants.js';
3232import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
3333import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
3434export { MODULE_NAME };
3535
3636const MODULE_NAME = 'sd';
3737const UPDATE_INTERVAL = 1000;
3838// This is a 1x1 transparent PNG
3939const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
40+const CUSTOM_STOP_EVENT = 'sd_stop_generation';
4041
4142const sources = {
4243 extras: 'extras',
@@ -50,6 +51,7 @@ const sources = {
5051 drawthings: 'drawthings',
5152 pollinations: 'pollinations',
5253 stability: 'stability',
54+ blockentropy: 'blockentropy',
5355};
5456
5557const initiators = {
@@ -57,6 +59,7 @@ const initiators = {
5759 action: 'action',
5860 interactive: 'interactive',
5961 wand: 'wand',
62+ swipe: 'swipe',
6063};
6164
6265const generationMode = {
@@ -718,29 +721,29 @@ function onChatChanged() {
718721 adjustElementScrollHeight();
719722}
720723
721724async function adjustElementScrollHeight() {
722725 if (!$('.sd_settings').is(':visible')) {
723726 return;
724727 }
725728
726729 await resetScrollHeight($('#sd_prompt_prefix'));
727730 await resetScrollHeight($('#sd_negative_prompt'));
728731 await resetScrollHeight($('#sd_character_prompt'));
729732 await resetScrollHeight($('#sd_character_negative_prompt'));
730733}
731734
732-function onCharacterPromptInput() {
public/scripts/extensions/stable-diffusion/settings.html+0 -0
public/scripts/extensions/token-counter/index.js+0 -0
public/scripts/extensions/translate/buttons.html+0 -0
public/scripts/extensions/translate/index.js+0 -0
public/scripts/extensions/tts/azure.js+0 -0
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/personas.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/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/charTagImport.i18n.html+0 -0
public/scripts/templates/installExtension.html+0 -0
public/scripts/templates/macros.html+0 -0
public/scripts/templates/tabbyDownloader.html+0 -0
public/scripts/templates/welcome.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/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/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
tests/package-lock.json+0 -0
Diff truncated