Merge branch 'staging' into enable-autoselect-inputs

321f0500e02756743bd8e54991d5ad83b068b625

Wolfsblvt <wolfsblvt@gmail.com>

149 files changed, +5808 -996Ignore whitespace
.eslintrc.js+1 -0
@@ -55,6 +55,7 @@ module.exports = {
55 isProbablyReaderable: 'readonly',55 isProbablyReaderable: 'readonly',
56 ePub: 'readonly',56 ePub: 'readonly',
57 diff_match_patch: 'readonly',57 diff_match_patch: 'readonly',
58 SillyTavern: 'readonly',
58 },59 },
59 },60 },
60 ],61 ],
.github/workflows/close-stale-issues.yml+12 -12
@@ -16,21 +16,21 @@ jobs:
16 uses: actions/stale@v416 uses: actions/stale@v4
17 with:17 with:
18 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}18 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
19 days-before-stale: 36019 days-before-stale: 183
20 days-before-close: 520 days-before-close: 7
21 operations-per-run: 3021 operations-per-run: 30
22 remove-stale-when-updated: true22 remove-stale-when-updated: true
23 enable-statistics: true23 enable-statistics: true
24 stale-issue-message: > 24 stale-issue-message: >
25 This issue has gone 3 months without an update. To keep the ticket open, please indicate that it is still relevant in a comment below.25 This issue has gone 6 months without an update. To keep the ticket open, please indicate that it is still relevant in a comment below.
26 Otherwise it will be closed in 5 working days.26 Otherwise it will be closed in 7 days.
27 stale-pr-message: >27 stale-pr-message: >
28 This PR is stale because it has been open 6 weeks with no activity. Either remove the stale label or comment below with a short update,28 This PR is stale because it has been open 6 months with no activity. Either remove the stale label or comment below with a short update,
29 otherwise this PR will be closed in 5 days.29 otherwise this PR will be closed in 7 days.
30 close-issue-message: >30 close-issue-message: >
31 This issue was automatically closed because it has been stalled for over 1 year with no activity.31 This issue was automatically closed because it has been stalled for over 6 months with no activity.
32 close-pr-message: >32 close-pr-message: >
33 This pull request was automatically closed because it has been stalled for over 1 year with no activity.33 This pull request was automatically closed because it has been stalled for over 6 months with no activity.
34 stale-issue-label: '⚰️ Stale'34 stale-issue-label: '⚰️ Stale'
35 close-issue-label: '🕸️ Inactive'35 close-issue-label: '🕸️ Inactive'
36 stale-pr-label: '⚰️ Stale'36 stale-pr-label: '⚰️ Stale'
@@ -44,8 +44,8 @@ jobs:
44 uses: actions/stale@v444 uses: actions/stale@v4
45 with:45 with:
46 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}46 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
47 days-before-stale: 547 days-before-stale: 7
48 days-before-close: 348 days-before-close: 7
49 operations-per-run: 3049 operations-per-run: 30
50 remove-stale-when-updated: true50 remove-stale-when-updated: true
51 stale-issue-message: >51 stale-issue-message: >
@@ -65,7 +65,7 @@ jobs:
65 with:65 with:
66 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}66 repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
67 days-before-stale: 767 days-before-stale: 7
68 days-before-close: 36568 days-before-close: 183
69 operations-per-run: 3069 operations-per-run: 30
70 remove-stale-when-updated: true70 remove-stale-when-updated: true
71 stale-issue-message: Hey SillyTavern, - Don't forget to respond!71 stale-issue-message: Hey SillyTavern, - Don't forget to respond!
@@ -79,4 +79,4 @@ jobs:
79 close-issue-label: '🕸️ Inactive'79 close-issue-label: '🕸️ Inactive'
80 close-pr-label: '🕸️ Inactive'80 close-pr-label: '🕸️ Inactive'
81 exempt-issue-labels: '📌 Keep Open'81 exempt-issue-labels: '📌 Keep Open'
82 exempt-pr-labels: '📌 Keep Open'
82 \ No newline at end of file \ No newline at end of file
82 exempt-pr-labels: '📌 Keep Open'
CONTRIBUTING.md+10 -1
@@ -29,4 +29,13 @@
29 - Updating GitHub Actions.29 - Updating GitHub Actions.
30 - Hotfixing a critical bug.30 - Hotfixing a critical bug.
314. Project maintainers will test and can change your code before merging.314. Project maintainers will test and can change your code before merging.
325. 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.325. 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?
366. 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
401. [How to write UI extensions](https://docs.sillytavern.app/for-contributors/writing-extensions/)
412. [How to write server plugins](https://docs.sillytavern.app/for-contributors/server-plugins)
default/config.yaml+44 -5
@@ -4,8 +4,22 @@ dataRoot: ./data
4# -- SERVER CONFIGURATION --4# -- SERVER CONFIGURATION --
5# Listen for incoming connections5# Listen for incoming connections
6listen: false6listen: false
7# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
8protocol:
9 ipv4: true
10 ipv6: false
11# Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6
12dnsPreferIPv6: false
13# The hostname that autorun opens.
14# - Use "auto" to let the server decide
15# - Use options like 'localhost', 'st.example.com'
16autorunHostname: "auto"
7# Server port17# Server port
8port: 800018port: 8000
19# Overrides the port for autorun in browser.
20# - Use -1 to use the server port.
21# - Specify a port to override the default.
22autorunPortOverride: -1
9# -- SECURITY CONFIGURATION --23# -- SECURITY CONFIGURATION --
10# Toggle whitelist mode24# Toggle whitelist mode
11whitelistMode: true25whitelistMode: true
@@ -13,6 +27,7 @@ whitelistMode: true
13enableForwardedWhitelist: true27enableForwardedWhitelist: true
14# Whitelist of allowed IP addresses28# Whitelist of allowed IP addresses
15whitelist:29whitelist:
30 - ::1
16 - 127.0.0.131 - 127.0.0.1
17# Toggle basic authentication for endpoints32# Toggle basic authentication for endpoints
18basicAuthMode: false33basicAuthMode: false
@@ -26,6 +41,11 @@ enableCorsProxy: false
26enableUserAccounts: false41enableUserAccounts: false
27# Enable discreet login mode: hides user list on the login screen42# Enable discreet login mode: hides user list on the login screen
28enableDiscreetLogin: false43enableDiscreetLogin: false
44# User session timeout *in seconds* (defaults to 24 hours).
45## Set to a positive number to expire session after a certain time of inactivity
46## Set to 0 to expire session when the browser is closed
47## Set to a negative number to disable session expiration
48sessionTimeout: 86400
29# Used to sign session cookies. Will be auto-generated if not set49# Used to sign session cookies. Will be auto-generated if not set
30cookieSecret: ''50cookieSecret: ''
31# Disable CSRF protection - NOT RECOMMENDED51# Disable CSRF protection - NOT RECOMMENDED
@@ -35,6 +55,9 @@ securityOverride: false
35# -- ADVANCED CONFIGURATION --55# -- ADVANCED CONFIGURATION --
36# Open the browser automatically56# Open the browser automatically
37autorun: true57autorun: true
58# Avoids using 'localhost' for autorun in auto mode.
59# use if you don't have 'localhost' in your hosts file
60avoidLocalhost: false
38# Disable thumbnail generation61# Disable thumbnail generation
39disableThumbnails: false62disableThumbnails: false
40# Thumbnail quality (0-100)63# Thumbnail quality (0-100)
@@ -93,10 +116,26 @@ openai:
93deepl:116deepl:
94 # Available options: default, more, less, prefer_more, prefer_less117 # Available options: default, more, less, prefer_more, prefer_less
95 formality: default118 formality: default
119# -- MISTRAL API CONFIGURATION --
120mistral:
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 --
125ollama:
126 # Controls how long the model will stay loaded into memory following the request
127 # * -1: Keep the model loaded indefinitely
128 # * 0: Unload the model immediately after the request
129 # * 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 --
132claude:
133 # Enables caching of the system prompt (if supported).
134 # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
135 # -- IMPORTANT! --
136 # Use only when the prompt before the chat history is static and doesn't change between requests
137 # (e.g {{random}} macro or lorebooks not as in-chat injections).
138 # Otherwise, you'll just waste money on cache misses.
139 enableSystemPromptCache: false
96# -- SERVER PLUGIN CONFIGURATION --140# -- SERVER PLUGIN CONFIGURATION --
97enableServerPlugins: false141enableServerPlugins: 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
102sessionTimeout: 86400
default/content/presets/openai/Default.json+4 -4
@@ -1,7 +1,7 @@
1{1{
2 "chat_completion_source": "openai",2 "chat_completion_source": "openai",
3 "openai_model": "gpt-3.5-turbo",3 "openai_model": "gpt-4-turbo",
4 "claude_model": "claude-instant-v1",4 "claude_model": "claude-3-5-sonnet-20240620",
5 "windowai_model": "",5 "windowai_model": "",
6 "openrouter_model": "OR_Website",6 "openrouter_model": "OR_Website",
7 "openrouter_use_fallback": false,7 "openrouter_use_fallback": false,
@@ -9,7 +9,7 @@
9 "openrouter_group_models": false,9 "openrouter_group_models": false,
10 "openrouter_sort_models": "alphabetically",10 "openrouter_sort_models": "alphabetically",
11 "ai21_model": "j2-ultra",11 "ai21_model": "j2-ultra",
12 "mistralai_model": "mistral-medium-latest",12 "mistralai_model": "mistral-large-latest",
13 "custom_model": "",13 "custom_model": "",
14 "custom_url": "",14 "custom_url": "",
15 "custom_include_body": "",15 "custom_include_body": "",
@@ -22,7 +22,7 @@
22 "count_penalty": 0,22 "count_penalty": 0,
23 "top_p": 1,23 "top_p": 1,
24 "top_k": 0,24 "top_k": 0,
25 "top_a": 1,25 "top_a": 0,
26 "min_p": 0,26 "min_p": 0,
27 "repetition_penalty": 1,27 "repetition_penalty": 1,
28 "openai_max_context": 4095,28 "openai_max_context": 4095,
default/content/settings.json+3 -3
@@ -610,9 +610,9 @@
610 }610 }
611 ]611 ]
612 },612 },
613 "wi_format": "[Details of the fictional world the RP is set in:\n{0}]\n",613 "wi_format": "{0}",
614 "openai_model": "gpt-3.5-turbo",614 "openai_model": "gpt-4-turbo",
615 "claude_model": "claude-instant-v1",615 "claude_model": "claude-3-5-sonnet-20240620",
616 "ai21_model": "j2-ultra",616 "ai21_model": "j2-ultra",
617 "windowai_model": "",617 "windowai_model": "",
618 "openrouter_model": "OR_Website",618 "openrouter_model": "OR_Website",
index.d.ts+5 -0
@@ -9,6 +9,11 @@ declare global {
9 };9 };
10 }10 }
11 }11 }
12
13 /**
14 * The root directory for user data.
15 */
16 var DATA_ROOT: string;
12}17}
1318
14declare module 'express-session' {19declare module 'express-session' {
package-lock.json+29 -28
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.12.3",3 "version": "1.12.4",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "sillytavern",8 "name": "sillytavern",
9 "version": "1.12.3",9 "version": "1.12.4",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -42,7 +42,7 @@
42 "rate-limiter-flexible": "^5.0.0",42 "rate-limiter-flexible": "^5.0.0",
43 "response-time": "^2.3.2",43 "response-time": "^2.3.2",
44 "sanitize-filename": "^1.6.3",44 "sanitize-filename": "^1.6.3",
45 "sillytavern-transformers": "^2.14.6",45 "sillytavern-transformers": "2.14.6",
46 "simple-git": "^3.19.1",46 "simple-git": "^3.19.1",
47 "tiktoken": "^1.0.15",47 "tiktoken": "^1.0.15",
48 "vectra": "^0.2.2",48 "vectra": "^0.2.2",
@@ -58,7 +58,7 @@
58 },58 },
59 "devDependencies": {59 "devDependencies": {
60 "@types/jquery": "^3.5.29",60 "@types/jquery": "^3.5.29",
61 "eslint": "^8.55.0",61 "eslint": "^8.57.0",
62 "jquery": "^3.6.4"62 "jquery": "^3.6.4"
63 },63 },
64 "engines": {64 "engines": {
@@ -166,9 +166,9 @@
166 "license": "MIT"166 "license": "MIT"
167 },167 },
168 "node_modules/@eslint/js": {168 "node_modules/@eslint/js": {
169 "version": "8.55.0",169 "version": "8.57.0",
170 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",170 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
171 "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",171 "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
172 "dev": true,172 "dev": true,
173 "license": "MIT",173 "license": "MIT",
174 "engines": {174 "engines": {
@@ -185,14 +185,15 @@
185 }185 }
186 },186 },
187 "node_modules/@humanwhocodes/config-array": {187 "node_modules/@humanwhocodes/config-array": {
188 "version": "0.11.13",188 "version": "0.11.14",
189 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",189 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
190 "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",190 "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
191 "deprecated": "Use @eslint/config-array instead",
191 "dev": true,192 "dev": true,
192 "license": "Apache-2.0",193 "license": "Apache-2.0",
193 "dependencies": {194 "dependencies": {
194 "@humanwhocodes/object-schema": "^2.0.1",195 "@humanwhocodes/object-schema": "^2.0.2",
195 "debug": "^4.1.1",196 "debug": "^4.3.1",
196 "minimatch": "^3.0.5"197 "minimatch": "^3.0.5"
197 },198 },
198 "engines": {199 "engines": {
@@ -200,9 +201,9 @@
200 }201 }
201 },202 },
202 "node_modules/@humanwhocodes/config-array/node_modules/debug": {203 "node_modules/@humanwhocodes/config-array/node_modules/debug": {
203 "version": "4.3.4",204 "version": "4.3.6",
204 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",205 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
205 "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",206 "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
206 "dev": true,207 "dev": true,
207 "license": "MIT",208 "license": "MIT",
208 "dependencies": {209 "dependencies": {
@@ -239,9 +240,10 @@
239 }240 }
240 },241 },
241 "node_modules/@humanwhocodes/object-schema": {242 "node_modules/@humanwhocodes/object-schema": {
242 "version": "2.0.1",243 "version": "2.0.3",
243 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",244 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
244 "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",245 "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
246 "deprecated": "Use @eslint/object-schema instead",
245 "dev": true,247 "dev": true,
246 "license": "BSD-3-Clause"248 "license": "BSD-3-Clause"
247 },249 },
@@ -1391,12 +1393,11 @@
1391 "license": "MIT"1393 "license": "MIT"
1392 },1394 },
1393 "node_modules/axios": {1395 "node_modules/axios": {
1394 "version": "1.6.1",1396 "version": "1.7.4",
1395 "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",1397 "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
1396 "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",1398 "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
1397 "license": "MIT",
1398 "dependencies": {1399 "dependencies": {
1399 "follow-redirects": "^1.15.0",1400 "follow-redirects": "^1.15.6",
1400 "form-data": "^4.0.0",1401 "form-data": "^4.0.0",
1401 "proxy-from-env": "^1.1.0"1402 "proxy-from-env": "^1.1.0"
1402 }1403 }
@@ -2456,17 +2457,17 @@
2456 }2457 }
2457 },2458 },
2458 "node_modules/eslint": {2459 "node_modules/eslint": {
2459 "version": "8.55.0",2460 "version": "8.57.0",
2460 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",2461 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
2461 "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",2462 "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
2462 "dev": true,2463 "dev": true,
2463 "license": "MIT",2464 "license": "MIT",
2464 "dependencies": {2465 "dependencies": {
2465 "@eslint-community/eslint-utils": "^4.2.0",2466 "@eslint-community/eslint-utils": "^4.2.0",
2466 "@eslint-community/regexpp": "^4.6.1",2467 "@eslint-community/regexpp": "^4.6.1",
2467 "@eslint/eslintrc": "^2.1.4",2468 "@eslint/eslintrc": "^2.1.4",
2468 "@eslint/js": "8.55.0",2469 "@eslint/js": "8.57.0",
2469 "@humanwhocodes/config-array": "^0.11.13",2470 "@humanwhocodes/config-array": "^0.11.14",
2470 "@humanwhocodes/module-importer": "^1.0.1",2471 "@humanwhocodes/module-importer": "^1.0.1",
2471 "@nodelib/fs.walk": "^1.2.8",2472 "@nodelib/fs.walk": "^1.2.8",
2472 "@ungap/structured-clone": "^1.2.0",2473 "@ungap/structured-clone": "^1.2.0",
package.json+3 -3
@@ -32,7 +32,7 @@
32 "rate-limiter-flexible": "^5.0.0",32 "rate-limiter-flexible": "^5.0.0",
33 "response-time": "^2.3.2",33 "response-time": "^2.3.2",
34 "sanitize-filename": "^1.6.3",34 "sanitize-filename": "^1.6.3",
35 "sillytavern-transformers": "^2.14.6",35 "sillytavern-transformers": "2.14.6",
36 "simple-git": "^3.19.1",36 "simple-git": "^3.19.1",
37 "tiktoken": "^1.0.15",37 "tiktoken": "^1.0.15",
38 "vectra": "^0.2.2",38 "vectra": "^0.2.2",
@@ -70,7 +70,7 @@
70 "type": "git",70 "type": "git",
71 "url": "https://github.com/SillyTavern/SillyTavern.git"71 "url": "https://github.com/SillyTavern/SillyTavern.git"
72 },72 },
73 "version": "1.12.3",73 "version": "1.12.4",
74 "scripts": {74 "scripts": {
75 "start": "node server.js",75 "start": "node server.js",
76 "start:no-csrf": "node server.js --disableCsrf",76 "start:no-csrf": "node server.js --disableCsrf",
@@ -90,7 +90,7 @@
90 "main": "server.js",90 "main": "server.js",
91 "devDependencies": {91 "devDependencies": {
92 "@types/jquery": "^3.5.29",92 "@types/jquery": "^3.5.29",
93 "eslint": "^8.55.0",93 "eslint": "^8.57.0",
94 "jquery": "^3.6.4"94 "jquery": "^3.6.4"
95 }95 }
96}96}
public/css/character-group-overlay.css+2 -2
@@ -89,7 +89,7 @@
89 position: absolute;89 position: absolute;
90 width: 100%;90 width: 100%;
91 height: 100vh;91 height: 100vh;
92 height: 100svh;92 height: 100dvh;
93 z-index: 9998;93 z-index: 9998;
94 top: 0;94 top: 0;
95}95}
@@ -99,6 +99,6 @@
99}99}
100100
101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
102 width: 100px;102 width: unset;
103 padding: 0.25em;103 padding: 0.25em;
104}104}
public/css/extensions-panel.css+2 -1
@@ -7,7 +7,8 @@
7 background-color: green;7 background-color: green;
8}8}
99
10.extensions_block input[type="checkbox"] {10.extensions_block input[type="checkbox"],
11.extensions_block input[type="radio"] {
11 margin-left: 10px;12 margin-left: 10px;
12 margin-right: 10px;13 margin-right: 10px;
13}14}
public/css/loader.css+2 -2
@@ -7,8 +7,8 @@
7 z-index: 999999;7 z-index: 999999;
8 width: 100vw;8 width: 100vw;
9 height: 100vh;9 height: 100vh;
10 width: 100svw;10 width: 100dvw;
11 height: 100svh;11 height: 100dvh;
12 background-color: var(--SmartThemeBlurTintColor);12 background-color: var(--SmartThemeBlurTintColor);
13 color: var(--SmartThemeBodyColor);13 color: var(--SmartThemeBodyColor);
14 /*for some reason the full screen blur does not work on iOS*/14 /*for some reason the full screen blur does not work on iOS*/
public/css/logprobs.css+3 -3
@@ -1,7 +1,7 @@
1#logprobsViewer {1#logprobsViewer {
2 overflow-y: auto;2 overflow-y: auto;
3 max-width: 90svw;3 max-width: 90dvw;
4 max-height: 90svh;4 max-height: 90dvh;
5 min-width: 100px;5 min-width: 100px;
6 min-height: 50px;6 min-height: 50px;
7 border-radius: 10px;7 border-radius: 10px;
@@ -16,7 +16,7 @@
16 top: 0;16 top: 0;
17 margin: 0;17 margin: 0;
18 right: unset;18 right: unset;
19 width: calc(((100svw - var(--sheldWidth)) / 2) - 1px);19 width: calc(((100dvw - var(--sheldWidth)) / 2) - 1px);
20}20}
2121
22.logprobs_panel_header {22.logprobs_panel_header {
public/css/mobile-styles.css+25 -29
@@ -1,6 +1,8 @@
1/*will apply to anything 1000px or less. this catches ipads, horizontal phones, and vertical phones)*/1/*will apply to anything 1000px or less. this catches ipads, horizontal phones, and vertical phones)*/
2@media screen and (max-width: 1000px) {2@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 {
4 flex-wrap: nowrap;6 flex-wrap: nowrap;
5 width: unset;7 width: unset;
6 }8 }
@@ -34,9 +36,9 @@
34 right: 0;36 right: 0;
35 width: fit-content;37 width: fit-content;
36 max-height: calc(60vh - 60px);38 max-height: calc(60vh - 60px);
37 max-height: calc(60svh - 60px);39 max-height: calc(60dvh - 60px);
38 max-width: 90vw;40 max-width: 90vw;
39 max-width: 90svw;41 max-width: 90dvw;
40 left: 50%;42 left: 50%;
41 top: 50%;43 top: 50%;
42 transform: translateX(-50%) translateY(-50%);44 transform: translateX(-50%) translateY(-50%);
@@ -102,7 +104,7 @@
102 min-width: unset;104 min-width: unset;
103 width: 100%;105 width: 100%;
104 max-height: calc(100vh - 45px);106 max-height: calc(100vh - 45px);
105 max-height: calc(100svh - 45px);107 max-height: calc(100dvh - 45px);
106 position: fixed;108 position: fixed;
107 left: 0;109 left: 0;
108 top: 5px;110 top: 5px;
@@ -130,15 +132,15 @@
130 #top-bar {132 #top-bar {
131 position: fixed;133 position: fixed;
132 width: 100vw;134 width: 100vw;
133 width: 100svw;135 width: 100dvw;
134 }136 }
135137
136 #bg1,138 #bg1,
137 #bg_custom {139 #bg_custom {
138 height: 100vh !important;140 height: 100vh !important;
139 height: 100svh !important;141 height: 100dvh !important;
140 width: 100vw !important;142 width: 100vw !important;
141 width: 100svw !important;143 width: 100dvw !important;
142 background-position: center;144 background-position: center;
143145
144 }146 }
@@ -146,13 +148,7 @@
146148
147 #sheld,149 #sheld,
148 #character_popup,150 #character_popup,
149 .drawer-content151 .drawer-content {
150
151 /* ,
152 #world_popup */
153 {
154 /*max-height: calc(100vh - 36px);
155 max-height: calc(100svh - 36px);*/
156 width: 100% !important;152 width: 100% !important;
157 margin: 0 auto;153 margin: 0 auto;
158 max-width: 100%;154 max-width: 100%;
@@ -223,10 +219,10 @@
223 #floatingPrompt,219 #floatingPrompt,
224 #cfgConfig,220 #cfgConfig,
225 #logprobsViewer,221 #logprobsViewer,
226 #movingDivs > div {222 #movingDivs>div {
227 /* 100vh are fallback units for browsers that don't support svh */223 /* 100vh are fallback units for browsers that don't support dvh */
228 height: calc(100vh - 45px);224 height: calc(100vh - 45px);
229 height: calc(100svh - 45px);225 height: calc(100dvh - 45px);
230 min-width: 100% !important;226 min-width: 100% !important;
231 width: 100% !important;227 width: 100% !important;
232 max-width: 100% !important;228 max-width: 100% !important;
@@ -249,7 +245,7 @@
249 #floatingPrompt,245 #floatingPrompt,
250 #cfgConfig,246 #cfgConfig,
251 #logprobsViewer,247 #logprobsViewer,
252 #movingDivs > div {248 #movingDivs>div {
253 height: min-content;249 height: min-content;
254 }250 }
255251
@@ -286,9 +282,9 @@
286282
287 body.waifuMode #sheld {283 body.waifuMode #sheld {
288 height: 40vh;284 height: 40vh;
289 height: 40svh;285 height: 40dvh;
290 top: 60vh;286 top: 60vh;
291 top: 60svh;287 top: 60dvh;
292 bottom: 0 !important;288 bottom: 0 !important;
293 }289 }
294290
@@ -325,16 +321,16 @@
325 body.waifuMode .zoomed_avatar {321 body.waifuMode .zoomed_avatar {
326 width: fit-content;322 width: fit-content;
327 max-height: calc(60vh - 60px);323 max-height: calc(60vh - 60px);
328 max-height: calc(60svh - 60px);324 max-height: calc(60dvh - 60px);
329 max-width: 90vw;325 max-width: 90vw;
330 max-width: 90svw;326 max-width: 90dvw;
331 }327 }
332328
333 .scrollableInner {329 .scrollableInner {
334 overflow-y: auto;330 overflow-y: auto;
335 overflow-x: hidden;331 overflow-x: hidden;
336 max-height: calc(100vh - 90px);332 max-height: calc(100vh - 90px);
337 max-height: calc(100svh - 90px);333 max-height: calc(100dvh - 90px);
338 }334 }
339335
340 .horde_multiple_hint {336 .horde_multiple_hint {
@@ -370,9 +366,9 @@
370366
371 body:not(.waifuMode) .zoomed_avatar {367 body:not(.waifuMode) .zoomed_avatar {
372 max-height: calc(60vh - 60px);368 max-height: calc(60vh - 60px);
373 max-height: calc(60svh - 60px);369 max-height: calc(60dvh - 60px);
374 max-width: 90vw;370 max-width: 90vw;
375 max-width: 90svw;371 max-width: 90dvw;
376 left: 50%;372 left: 50%;
377 top: 50%;373 top: 50%;
378 transform: translateX(-50%) translateY(-50%);374 transform: translateX(-50%) translateY(-50%);
@@ -453,9 +449,9 @@
453 min-height: unset;449 min-height: unset;
454 max-height: unset;450 max-height: unset;
455 width: 100vw;451 width: 100vw;
456 width: 100svw;452 width: 100dvw;
457 height: calc(100vh - 36px);453 height: calc(100vh - 36px);
458 height: calc(100svh - 36px);454 height: calc(100dvh - 36px);
459 padding-right: max(env(safe-area-inset-right), 0px);455 padding-right: max(env(safe-area-inset-right), 0px);
460 padding-left: max(env(safe-area-inset-left), 0px);456 padding-left: max(env(safe-area-inset-left), 0px);
461 padding-bottom: 0;457 padding-bottom: 0;
@@ -485,10 +481,10 @@
485 top: 0;481 top: 0;
486 margin: 0 auto;482 margin: 0 auto;
487 height: calc(100vh - 70px);483 height: calc(100vh - 70px);
488 height: calc(100svh - 70px);484 height: calc(100dvh - 70px);
489 width: calc(100% - 5px);485 width: calc(100% - 5px);
490 max-height: calc(100vh - 70px);486 max-height: calc(100vh - 70px);
491 max-height: calc(100svh - 70px);487 max-height: calc(100dvh - 70px);
492 max-width: calc(100% - 5px);488 max-width: calc(100% - 5px);
493489
494 }490 }
public/css/popup-safari-fix.css+1 -1
@@ -7,5 +7,5 @@ body.safari .popup.large_dialogue_popup .popup-body {
7body.safari .popup .popup-body {7body.safari .popup .popup-body {
8 height: fit-content;8 height: fit-content;
9 max-height: 90vh;9 max-height: 90vh;
10 max-height: 90svh;10 max-height: 90dvh;
11}11}
public/css/popup.css+4 -4
@@ -16,8 +16,8 @@ dialog {
16 display: flex;16 display: flex;
17 flex-direction: column;17 flex-direction: column;
1818
19 max-height: calc(100svh - 2em);19 max-height: calc(100dvh - 2em);
20 max-width: calc(100svw - 2em);20 max-width: calc(100dvw - 2em);
21 min-height: fit-content;21 min-height: fit-content;
2222
23 /* Overflow visible so elements (like toasts) can appear outside of the dialog. '.popup-body' is hiding overflow for the real content. */23 /* 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
104.popup #toast-container {104.popup #toast-container {
105 /* Fix toastr in dialogs by actually placing it at the top of the screen via transform */105 /* Fix toastr in dialogs by actually placing it at the top of the screen via transform */
106 height: 100svh;106 height: 100dvh;
107 top: calc(50% + var(--topBarBlockSize));107 top: calc(50% + var(--topBarBlockSize));
108 left: 50%;108 left: 50%;
109 transform: translate(-50%, -50%);109 transform: translate(-50%, -50%);
@@ -115,7 +115,7 @@ body.no-blur .popup[open]::backdrop {
115.popup-crop-wrap {115.popup-crop-wrap {
116 margin: 10px auto;116 margin: 10px auto;
117 max-height: 75vh;117 max-height: 75vh;
118 max-height: 75svh;118 max-height: 75dvh;
119 max-width: 100%;119 max-width: 100%;
120}120}
121121
public/css/tags.css+6 -6
@@ -209,20 +209,20 @@
209 opacity: 1;209 opacity: 1;
210}210}
211211
212.tag_as_folder {212.tag_as_folder.right_menu_button {
213 filter: brightness(75%) saturate(0.6);213 filter: brightness(75%) saturate(0.6);
214}214}
215215
216.tag_as_folder:hover,216.tag_as_folder.right_menu_button:hover,
217.tag_as_folder.flash {217.tag_as_folder.right_menu_button.flash {
218 filter: brightness(150%) saturate(0.6) !important;218 filter: brightness(150%) saturate(0.6);
219}219}
220220
221.tag_as_folder.no_folder {221.tag_as_folder.right_menu_button.no_folder {
222 filter: brightness(25%) saturate(0.25);222 filter: brightness(25%) saturate(0.25);
223}223}
224224
225.tag_as_folder .tag_folder_indicator {225.tag_as_folder.right_menu_button .tag_folder_indicator {
226 position: absolute;226 position: absolute;
227 top: calc(var(--mainFontSize) * -0.5);227 top: calc(var(--mainFontSize) * -0.5);
228 right: calc(var(--mainFontSize) * -0.5);228 right: calc(var(--mainFontSize) * -0.5);
public/css/toggle-dependent.css+6 -2
@@ -28,6 +28,10 @@ body.hideChatAvatars .mesAvatarWrapper .avatar {
28 display: none !important;28 display: none !important;
29}29}
3030
31body.hideChatAvatars .last_mes {
32 padding-bottom: 40px !important;
33}
34
31body.square-avatars .avatar,35body.square-avatars .avatar,
32body.square-avatars .avatar img {36body.square-avatars .avatar img {
33 border-radius: var(--avatar-base-border-radius) !important;37 border-radius: var(--avatar-base-border-radius) !important;
@@ -360,7 +364,7 @@ body.waifuMode #top-bar {
360364
361body.waifuMode #sheld {365body.waifuMode #sheld {
362 height: 40vh;366 height: 40vh;
363 height: 40svh;367 height: 40dvh;
364 top: calc(100% - 40vh);368 top: calc(100% - 40vh);
365 bottom: 0;369 bottom: 0;
366}370}
@@ -450,4 +454,4 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
450454
451#smooth_streaming:checked~#smooth_streaming_speed_control {455#smooth_streaming:checked~#smooth_streaming_speed_control {
452 display: block;456 display: block;
453}457}
\ No newline at end of file457 \ No newline at end of file
public/css/world-info.css+12 -0
@@ -120,6 +120,14 @@
120 flex-wrap: wrap;120 flex-wrap: wrap;
121}121}
122122
123.world_entry .inline-drawer-header {
124 cursor: initial;
125}
126
127.world_entry .killSwitch {
128 cursor: pointer;
129}
130
123.world_entry_form_control input[type=button] {131.world_entry_form_control input[type=button] {
124 cursor: pointer;132 cursor: pointer;
125}133}
@@ -173,6 +181,10 @@
173 width: 7em;181 width: 7em;
174}182}
175183
184.world_entry .killSwitch.fa-toggle-on {
185 color: var(--SmartThemeQuoteColor);
186}
187
176.wi-card-entry {188.wi-card-entry {
177 border: 1px solid;189 border: 1px solid;
178 border-color: var(--SmartThemeBorderColor);190 border-color: var(--SmartThemeBorderColor);
public/global.d.ts+7 -0
@@ -14,8 +14,15 @@ declare var isProbablyReaderable;
14declare var ePub;14declare var ePub;
15declare var ai;15declare var ai;
1616
17declare var SillyTavern: {
18 getContext(): any;
19 llm: any;
20};
21
17// Jquery plugins22// Jquery plugins
18interface JQuery {23interface JQuery {
24 nanogallery2(options?: any): JQuery;
25 nanogallery2(method: string, options?: any): JQuery;
19 pagination(method: 'getCurrentPageNum'): number;26 pagination(method: 'getCurrentPageNum'): number;
20 pagination(method: string, options?: any): JQuery;27 pagination(method: string, options?: any): JQuery;
21 pagination(options?: any): JQuery;28 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
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:none"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:inline"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:none"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-out.svg+149 -0
@@ -0,0 +1,149 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-out.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.313708"
29 inkscape:cx="58.910834"
30 inkscape:cy="25.323262"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="g2714" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:none"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:none"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:inline"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-over.svg+149 -0
@@ -0,0 +1,149 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-over.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.313708"
29 inkscape:cx="58.910834"
30 inkscape:cy="25.323262"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="g2714" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:inline"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:none"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:none"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-resume.svg+218 -0
@@ -0,0 +1,218 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-resume.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.315"
29 inkscape:cx="4.7282369"
30 inkscape:cy="24.259832"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="layer8" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /><image
91 width="460"
92 height="71"
93 preserveAspectRatio="none"
94 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcwAAABHCAYAAACK23cpAAAABHNCSVQICAgIfAhkiAAACuZJREFU
95eJzt3T9o3FgeB/Cv77aZBHInYZPD+JLF4iDxDl44ZNiAvVfNhC2vCEqxWQJpJn3MwlRXTeP0drNF
96NtfIKa5cYnVxkQMLDoztbU6GgBkwDiPOEHuOK/aK7FM0Gs3M01gzkp6+HzBknPFEkSV93z/9NLO6
97uvoLiIiIaKjfZL0BRERERcDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIi
98ksDAJCIiksDAJCIiksDAJCIiksDAJCIikvBZ1htAlIZmswlN0wAAvu+j1WplvEX5YVkWOp0OHMfJ
99elMoBYuLi8Gfj4+PM9yS8mFgEinMsiyYphm8ZmgSjY9DskSKioZlvV5HrVbLcIuoyEzTDEZxyoqB
100SaSgaFgKDE0aR6PRCI6pMocmA5NIMYPCUmBoUhKNRgOGYQD4eOyUOTQZmEQKGRWWAkOTZPm+3/O6
101zKHJwCRShGxYCgxNkmHbNlzX7fleWUNzqqtk5xaWMbuwHLx+d+Tg4vw0eH3txs2e19RPXBA1TQuW
102lHc6nb5WYFj4ouj7ft/BT8UXF5ae50HX9Z7bbQD0XOTq9ToArp6l4WzbBoC+RWQA4Lru0OuPSqYa
103mLMLy1i69yh4/f5kvycgv3nyI47evuwL0jKzLCuYPxjWmvM8D47jxIanOLDF+xiYahkUlltbW2g2
104mz3fb7VaPfesAgxNksPQzOGQ7NK9R/jLgw3MhXqiZWSaJjY2NoJhj1FDH4ZhBCvZyjZMUmbDwnKQ
105VqsV26ji8CyNUvbh2VwWLrh24ya+frCBs5N9uK+fl6q3aRgGarVa0Ksc5+ebzSZ2dnZK0+ors06n
1060/N6VFgKcT1NVY8Vy7Lgui48z8t6Uwpr1HVpUE9T0zQ8ffp0rDAVo2F5GhHLXQ8zbG5hGd88+RG3
107l2q4duNm1pszcaKXGHdQioPHtm3Yto2tra2hoViv15XsbY77f9I0DY1GQ7n94TgOdnZ2AMiHpRDu
108acb1HFTQaDRgmmbP1AYlI86dUfsvrqfp+z42NzfHaowZhgHLsnI18pHLHmaUef+Z8r1NcVBGibnJ
109uNZx+Hvi4AofrOJ7qhAnrWEYiU5CTdOCuTzLsmDbtlK9Kcdxxl7M1Wq1YJqmkmEZnf8Xv3v2NJMJ
110r4GQfW+4IS9Cc9yeZp7m2H9769atv03rH5tbWMbcH78MXkcX94QXBEVdv3ETf/rzXwEAF+en+N9/
111P0xuQ6csfEEXfN/HixcvgovhKL7vY3d3F7quY35+Pvi+rut97yvixdGyLFSrVQBApVJBtVrFwcEB
112ut0uAGBtbQ2VSgUA0O12sbu7C6B/34r9U8R9MEy73Y79/qD9IvOzRddut1GtVoP/f6VSgWEYaLfb
113V2owaZqGSqUSHHvTNu1h9MePH/e89jwPvu/3fIWvM4Zh4PLyEr7vB/uo2+3i4OAA8/PzfT876Cv6
114mb7vZ36s5raHeXF+ig/np32Lf5buPcLnX9Thvn6Os5P9jLYuXdFe4FWetmHbNjqdTqJWYRG4rgvD
115MIKLhZgbGdbTjGuIAPloqdLkxfVsxu1pip8LD0v6vo+9vb1SHU+Dhv3j6hYD/T3NJFMG0Tl2y7KC
116sM5KbucwP5yf4s2r9dhhWLEo6OsHG4Wf24ybSBfLt8cVntdShed5fUOpwxYUDArLra0tDsmVSNwc
117Wlz4DVOr1dBsNvver2ka6vV634W9jCaxejauMTzusG5acjske3F+indHDv5zdox//+sfuP67P+D3
118c70HrBimFUO0RRymffjwYTBkBCS/oNdqteDADH/put43HAsUd0gWQDAkYxhGzzBbtVrtOYkqlQrW
1191tb6fr6MYSkzJKs6MRw4zvCszDoAcQxOa99Oe0g2PFo17PpxeHjYNyUUNzwra9DvLTodM0257WFG
120ua+f482r9dhFP+b9ZzDvPytcbzN6f6Xv+4kv6GIRTNyXigb1NEcpY1jSJ+P2NOMW4sXRNC1RWUJV
121pd3THPZ7y6Knmds5zDhnJ/v46YfvcPerb/H5F/WegBS3oBSpUlD0BNvb28toS4pFhKbsScOwVEfc
122MHsS0eNl2Jxm0kanqquNk0q7IpCYK15ZWQl+f4ZhBPt7mnOahelhhv38z78PXPRTpEpBKysrwZ99
1233y/V4oGriutpxmFYqkWMyoz7Negz43qaSXswcVMgZZV2T9NxnL4Ohfi8aSpUDzPsQwF6kKOMuxI2
124LMmqM9WM6mkyLEnWoIVjV/2MMptETzNq2o2UQgbm3a++HXjP5rsjB+7r51PeIsrKoNBkWKrpqsNv
125g0It7ngRTwOSxeHYfmmFZtziK3HuT1OhAnNuYRl37z2KHW49O9nHz29fKnNv5iDiwPN9P6gjqlLV
126mnFEQ5Nhqa6rjMokvdVIPPlHtucYretLH101NEXJ0LCkZSDTUpjANO8/w+2l+JqC7uvneHek9vyf
127uH0kSnYuT3Xh1ibD8pNOp8OGFca7L9f3fdi2LbVSVpSwpHjjhmZcydCkBRDSlPvAvL1Ug3n/Wezf
128qV5fVhhW+Fg8nUTV4tlJMCj7lXmOW7hKEQvP87CzszO0chYX7MlJGppiMVaYaMRkJbeBef3Xaj5x
129w68X56dKlcYbRvZRX/V6PfOyUUR5k0bFJ8dx4LouS+OlIGloRhf1ZF08P7eBee3GzdhCBGVa1CNK
130b8m+1zRNnrxEv0qzPKIYBhTzmWVqmIbncXVdly7mkITMU07ysDYht4EZVZZFPVdhGEZpA1MUZtc0
131DcfHx8GCjbIr836JK2l31YtuWfZdWKfT6SleP6nbZ4aFpmEYmYclUJDALMOinjhJb8pVtRzeMHEr
1326IBPcx15OMmywP2CYOW0OC/y0EMpItu2p1b0fFBo5mV9Rm6LrwMfe5VvXq3j/QR6lbOzs7i4uEj9
133c9Ok63rwDEgZ4pmYZVGr1QYWxq5UKkGDI+n9dEXH/fJRt9uF53mYn5/H9va2MmE57eLr3W4X7XYb
134mqZhZmam52ERk3CVgu2Tlsse5qQX9czOzuL777/H+vr6RD4/LUlP8DLdByY7v1uv16debzJL3C+9
135srwFQSWe5020wSHzPM08yF0t2XdHDn764buJhuWTJ08m8tlpE08el1Wm+ctRj1wa971Fx/1CRTSJ
13652lOwlR7mO9P9nH09mXwOloP9s2r9Yku6hFhOTs7O7F/I23RJ8YPMukWYN4kma8VC1/y1FKdFO4X
137Kqq0a89OwlQD8+xkf2ggMiz7iUUawx5llVWZqKzkqcWZJ9wvVHR5D81czmGmrahhKXieh1arFRQx
1380HUdmqbBdV24rluqnuW4sj7R8or7hfImz6GpfGAWPSzDHMcp1TzlIGJuV7ZHVZZQ4H4hVeQ1NHO3
1396CdNKoUl9Yo+TDat9xYd9wupIo8LgZQNTIal2hzHkWpllq0wNvcLqWRQaGZVpEXJwGRYlsPm5ubQ
140cMjiAbN5wP1CKomGpud5mVX+mWqln2lIEpZsYRdbt9vF7u4ufN9HpVIJqpC4rouDgwNsb2+Xcp6O
141+0Vt0670kweHh4fQdR2Xl5eZ3hEws7q6+ktm/3rKkvYs817ph4iI8kOZVbKi3B0REdEkKNXDJCIi
142mhQlF/0QERGljYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQk
1434bPFxcWst4GIiCj32MMkIiKSMHPnzh3WkiUiIhqBPUwiIiIJDEwiIiIJDEwiIiIJ/weH8+Ed/xCz
144fAAAAABJRU5ErkJggg==
145"
146 id="image2845"
147 x="-15.489594"
148 y="-8.2945251" /></g><g
149 inkscape:groupmode="layer"
150 id="layer5"
151 inkscape:label="dot" /><g
152 inkscape:label="over"
153 inkscape:groupmode="layer"
154 id="layer1"
155 style="display:none"><ellipse
156 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
157 id="path2637"
158 cx="24"
159 cy="33"
160 rx="4.3600898"
161 ry="4.3600893" /><path
162 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
163 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
164 id="path2142"
165 inkscape:path-effect="#path-effect2144"
166 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
167 sodipodi:nodetypes="csc" /><path
168 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
169 d="M 26,22 H 39 V 9"
170 id="path2626"
171 sodipodi:nodetypes="ccc" /></g><g
172 inkscape:groupmode="layer"
173 id="layer6"
174 inkscape:label="into"
175 style="display:none"><ellipse
176 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
177 id="path2637-3"
178 cx="24"
179 cy="38.5"
180 rx="4.3600898"
181 ry="4.3600893" /><path
182 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
183 d="M 24,2 V 24"
184 id="path2668"
185 sodipodi:nodetypes="cc" /><path
186 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
187 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
188 id="path2626-8"
189 sodipodi:nodetypes="ccc" /></g><g
190 inkscape:groupmode="layer"
191 id="g2714"
192 inkscape:label="out"
193 style="display:none"><ellipse
194 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
195 id="ellipse2708"
196 cx="24"
197 cy="38.5"
198 rx="4.3600898"
199 ry="4.3600893" /><path
200 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
201 d="M 24,29.722858 V 7.7228579"
202 id="path2710"
203 sodipodi:nodetypes="cc" /><path
204 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
205 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
206 id="path2712"
207 sodipodi:nodetypes="ccc" /></g><g
208 inkscape:groupmode="layer"
209 id="layer8"
210 inkscape:label="resume"><path
211 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
212 d="M 13,12 V 38"
213 id="path2850"
214 sodipodi:nodetypes="cc" /><path
215 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
216 d="M 21,16 V 34 L 36.5,25 Z"
217 id="path2852"
218 sodipodi:nodetypes="cccc" /></g></svg>
public/index.html+156 -55
@@ -383,7 +383,7 @@
383 Max Response Length (tokens)383 Max Response Length (tokens)
384 </div>384 </div>
385 <div class="wide100p">385 <div class="wide100p">
386 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="50" max="8000">386 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="16384">
387 </div>387 </div>
388 </div>388 </div>
389 <div class="range-block" data-source="openai,custom">389 <div class="range-block" data-source="openai,custom">
@@ -1273,7 +1273,7 @@
1273 <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />1273 <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />
1274 <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="max_tokens_second_textgenerationwebui" id="max_tokens_second_counter_textgenerationwebui">1274 <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="max_tokens_second_textgenerationwebui" id="max_tokens_second_counter_textgenerationwebui">
1275 </div>1275 </div>
1276 <div data-newbie-hidden data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" name="smoothingBlock" class="wide100p">1276 <div data-newbie-hidden data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" id="smoothingBlock" name="smoothingBlock" class="wide100p">
1277 <h4 class="wide100p textAlignCenter">1277 <h4 class="wide100p textAlignCenter">
1278 <label data-i18n="Smooth Sampling">Smooth Sampling</label>1278 <label data-i18n="Smooth Sampling">Smooth Sampling</label>
1279 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Smooth_Sampling_desc" title="Allows you to use quadratic/cubic transformations to adjust the distribution. Lower Smoothing Factor values will be more creative, usually between 0.2-0.3 is the sweetspot (assuming the curve = 1). Higher Smoothing Curve values will make the curve steeper, which will punish low probability choices more aggressively. 1.0 curve is equivalent to only using Smoothing Factor."></div>1279 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Smooth_Sampling_desc" title="Allows you to use quadratic/cubic transformations to adjust the distribution. Lower Smoothing Factor values will be more creative, usually between 0.2-0.3 is the sweetspot (assuming the curve = 1). Higher Smoothing Curve values will make the curve steeper, which will punish low probability choices more aggressively. 1.0 curve is equivalent to only using Smoothing Factor."></div>
@@ -1358,7 +1358,7 @@
1358 </div>1358 </div>
1359 </div>1359 </div>
1360 </div>1360 </div>
1361 <div data-newbie-hidden id="mirostat_block_ooba" class="wide100p">1361 <div data-newbie-hidden data-tg-type="ooba,aphrodite,infermaticai,koboldcpp,llamacpp,mancer,ollama,tabby" id="mirostat_block_ooba" class="wide100p">
1362 <h4 class="wide100p textAlignCenter">1362 <h4 class="wide100p textAlignCenter">
1363 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>1363 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>
1364 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat_desc" title="Mirostat is a thermostat for output perplexity.&#13;Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap&#13;(where, as the autoregressive inference produces text, the perplexity of the output tends toward zero)&#13;and the confusion trap (where the perplexity diverges).&#13;For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).&#13;Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>1364 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat_desc" title="Mirostat is a thermostat for output perplexity.&#13;Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap&#13;(where, as the autoregressive inference produces text, the perplexity of the output tends toward zero)&#13;and the confusion trap (where the perplexity diverges).&#13;For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).&#13;Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>
@@ -1387,7 +1387,7 @@
1387 </div>1387 </div>
1388 </div>1388 </div>
1389 </div>1389 </div>
1390 <div data-newbie-hidden data-tg-type="ooba, vllm" name="beamSearchBlock" class="wide100p">1390 <div data-newbie-hidden data-tg-type="ooba, vllm" id="beamSearchBlock" name="beamSearchBlock" class="wide100p">
1391 <h4 class="wide100p textAlignCenter">1391 <h4 class="wide100p textAlignCenter">
1392 <label>1392 <label>
1393 <span data-i18n="Beam search">Beam Search</span>1393 <span data-i18n="Beam search">Beam Search</span>
@@ -1413,7 +1413,7 @@
1413 </div>1413 </div>
1414 </div>1414 </div>
1415 </div>1415 </div>
1416 <div data-tg-type="ooba" data-newbie-hidden name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">1416 <div data-tg-type="ooba" data-newbie-hidden id="contrastiveSearchBlock" name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
1417 <h4 class="textAlignCenter" data-i18n="Contrastive search">Contrastive Search1417 <h4 class="textAlignCenter" data-i18n="Contrastive search">Contrastive Search
1418 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="Contrastive_search_txt" title="A sampler that encourages diversity while maintaining coherence, by exploiting the isotropicity of the representation space of most LLMs. For details, see the paper A Contrastive Framework for Neural Text Generation by Su et al. (2022)."></div>1418 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="Contrastive_search_txt" title="A sampler that encourages diversity while maintaining coherence, by exploiting the isotropicity of the representation space of most LLMs. For details, see the paper A Contrastive Framework for Neural Text Generation by Su et al. (2022)."></div>
1419 </h4>1419 </h4>
@@ -1544,7 +1544,7 @@
1544 <h4 class="wide100p textAlignCenter">1544 <h4 class="wide100p textAlignCenter">
1545 <label>1545 <label>
1546 <span data-i18n="Grammar String">Grammar String</span>1546 <span data-i18n="Grammar String">Grammar String</span>
1547 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GNBF or ENBF, depends on the backend in use. If you're using this you should know which." title="GNBF or ENBF, depends on the backend in use. If you're using this you should know which."></div>1547 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GBNF or EBNF, depends on the backend in use. If you're using this you should know which." title="GBNF or EBNF, depends on the backend in use. If you're using this you should know which."></div>
1548 <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">1548 <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">
1549 <small>1549 <small>
1550 <div class="fa-solid fa-up-right-from-square note-link-span"></div>1550 <div class="fa-solid fa-up-right-from-square note-link-span"></div>
@@ -1554,13 +1554,13 @@
1554 </h4>1554 </h4>
1555 <textarea id="grammar_string_textgenerationwebui" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea>1555 <textarea id="grammar_string_textgenerationwebui" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea>
1556 </div>1556 </div>
1557 <div id="sampler_order_block" data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">1557 <div id="sampler_order_block_kcpp" data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">
1558 <hr class="wide100p">1558 <hr class="wide100p">
1559 <div class="range-block-title">1559 <div class="range-block-title">
1560 <span data-i18n="Samplers Order">Samplers Order</span>1560 <span data-i18n="Samplers Order">Samplers Order</span>
1561 </div>1561 </div>
1562 <div class="toggle-description widthUnset" data-i18n="Samplers will be applied in a top-down order. Use with caution.">1562 <div class="toggle-description widthUnset" data-i18n="Samplers will be applied in a top-down order. Use with caution.">
1563 Samplers will be applied in a top-down order.1563 kcpp only. Samplers will be applied in a top-down order.
1564 Use with caution.1564 Use with caution.
1565 </div>1565 </div>
1566 <div id="koboldcpp_order" class="prompt_order">1566 <div id="koboldcpp_order" class="prompt_order">
@@ -1597,10 +1597,10 @@
1597 <span data-i18n="Load default order">Load default order</span>1597 <span data-i18n="Load default order">Load default order</span>
1598 </div>1598 </div>
1599 </div>1599 </div>
1600 <div data-newbie-hidden data-tg-type="llamacpp" class="range-block flexFlowColumn wide100p">1600 <div id="sampler_order_block_lcpp" data-newbie-hidden data-tg-type="llamacpp" class="range-block flexFlowColumn wide100p">
1601 <hr class="wide100p">1601 <hr class="wide100p">
1602 <h4 class="range-block-title justifyCenter">1602 <h4 class="range-block-title justifyCenter">
1603 <span data-i18n="Samplers Order">Samplers Order</span>1603 <span data-i18n="Sampler Order">Sampler Order</span>
1604 <div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored." title="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored."></div>1604 <div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored." title="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored."></div>
1605 </h4>1605 </h4>
1606 <div class="toggle-description widthUnset" data-i18n="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.">1606 <div class="toggle-description widthUnset" data-i18n="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.">
@@ -1618,7 +1618,7 @@
1618 <span data-i18n="Load default order">Load default order</span>1618 <span data-i18n="Load default order">Load default order</span>
1619 </div>1619 </div>
1620 </div>1620 </div>
1621 <div data-newbie-hidden data-tg-type="ooba" class="range-block flexFlowColumn wide100p">1621 <div id="sampler_priority_block_ooba" data-newbie-hidden data-tg-type="ooba" class="range-block flexFlowColumn wide100p">
1622 <hr class="wide100p">1622 <hr class="wide100p">
1623 <h4 class="range-block-title justifyCenter">1623 <h4 class="range-block-title justifyCenter">
1624 <span data-i18n="Sampler Priority">Sampler Priority</span>1624 <span data-i18n="Sampler Priority">Sampler Priority</span>
@@ -1660,16 +1660,24 @@
1660 </div>1660 </div>
1661 <div class="inline-drawer-content">1661 <div class="inline-drawer-content">
1662 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_none">1662 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_none">
1663 <input type="radio" id="character_names_none" name="character_names" value="0">1663 <input type="radio" id="character_names_none" name="character_names" value="-1">
1664 <span data-i18n="None">None</span>1664 <span data-i18n="None">None</span>
1665 <i class="right_menu_button fa-solid fa-circle-exclamation" title="Except for groups and past personas. Otherwise, make sure you provide names in the prompt." data-i18n="[title]character_names_none"></i>1665 <i class="right_menu_button fa-solid fa-circle-exclamation" title="Never add character name prefixes. May behave poorly in groups, choose with caution." data-i18n="[title]character_names_none"></i>
1666 <small class="flexBasis100p" data-i18n="Don't add character names.">1666 <small class="flexBasis100p" data-i18n="Never add character names.">
1667 Don't add character names.1667 Never 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.
1668 </small>1676 </small>
1669 </label>1677 </label>
1670 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_completion">1678 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_completion">
1671 <input type="radio" id="character_names_completion" name="character_names" value="1">1679 <input type="radio" id="character_names_completion" name="character_names" value="1">
1672 <span data-i18n="Completion">Completion Object</span>1680 <span data-i18n="Completion Object">Completion Object</span>
1673 <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>1681 <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>
1674 <small class="flexBasis100p" data-i18n="Add character names to completion objects.">1682 <small class="flexBasis100p" data-i18n="Add character names to completion objects.">
1675 Add character names to completion objects.1683 Add character names to completion objects.
@@ -1762,13 +1770,13 @@
1762 </div>1770 </div>
1763 </label>1771 </label>
1764 </div>1772 </div>
1765 <div class="range-block" data-source="openai,openrouter,makersuite,claude,custom">1773 <div class="range-block" data-source="openai,openrouter,makersuite,claude,custom,01ai">
1766 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">1774 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
1767 <input id="openai_image_inlining" type="checkbox" />1775 <input id="openai_image_inlining" type="checkbox" />
1768 <span data-i18n="Send inline images">Send inline images</span>1776 <span data-i18n="Send inline images">Send inline images</span>
1769 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">1777 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
1770 <span data-i18n="image_inlining_hint_1">Sends images in prompts if the model supports it (e.g. GPT-4V, Claude 3 or Llava 13B).1778 <span data-i18n="image_inlining_hint_1">Sends images in prompts if the model supports it (e.g. GPT-4V, Claude 3 or Llava 13B).
1771 Use the</span> <code><i class="fa-solid fa-paperclip"></i></code> <span data-i18n="image_inlining_hint_2">action on any message or the</span>1779 Use the</span> <code><i class="fa-solid fa-paperclip"></i></code> <span data-i18n="image_inlining_hint_2">action on any message or the</span>
1772 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code> <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>1780 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code> <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
1773 </div>1781 </div>
1774 </label>1782 </label>
@@ -1815,10 +1823,16 @@
1815 </div>1823 </div>
1816 <div data-newbie-hidden class="range-block" data-source="claude">1824 <div data-newbie-hidden class="range-block" data-source="claude">
1817 <div class="wide100p">1825 <div class="wide100p">
1818 <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>1826 <div class="flex-container alignItemsCenter">
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>1827 <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
1820 <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>1828 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_prefill" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
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>1829 </div>
1830 <textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1831 <div class="flex-container alignItemsCenter">
1832 <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
1833 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_impersonation" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
1834 </div>
1835 <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1822 </div>1836 </div>
1823 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">1837 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
1824 <input id="claude_use_sysprompt" type="checkbox" />1838 <input id="claude_use_sysprompt" type="checkbox" />
@@ -2419,6 +2433,7 @@
2419 <optgroup>2433 <optgroup>
2420 <option value="01ai">01.AI (Yi)</option>2434 <option value="01ai">01.AI (Yi)</option>
2421 <option value="ai21">AI21</option>2435 <option value="ai21">AI21</option>
2436 <option value="blockentropy">Block Entropy</option>
2422 <option value="claude">Claude</option>2437 <option value="claude">Claude</option>
2423 <option value="cohere">Cohere</option>2438 <option value="cohere">Cohere</option>
2424 <option value="groq">Groq</option>2439 <option value="groq">Groq</option>
@@ -2562,7 +2577,9 @@
2562 </optgroup>2577 </optgroup>
2563 <optgroup label="GPT-4o">2578 <optgroup label="GPT-4o">
2564 <option value="gpt-4o">gpt-4o</option>2579 <option value="gpt-4o">gpt-4o</option>
2580 <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
2565 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>2581 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2582 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2566 </optgroup>2583 </optgroup>
2567 <optgroup label="gpt-4o-mini">2584 <optgroup label="gpt-4o-mini">
2568 <option value="gpt-4o-mini">gpt-4o-mini</option>2585 <option value="gpt-4o-mini">gpt-4o-mini</option>
@@ -2794,21 +2811,26 @@
2794 <div>2811 <div>
2795 <h4 data-i18n="Google Model">Google Model</h4>2812 <h4 data-i18n="Google Model">Google Model</h4>
2796 <select id="model_google_select">2813 <select id="model_google_select">
2797 <optgroup label="Latest">2814 <optgroup label="Primary">
2798 <!-- Doesn't work without "latest". Maybe my key is scuffed? -->2815 <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
2799 <option value="gemini-1.5-flash-latest">Gemini 1.5 Flash</option>2816 <option value="gemini-1.5-flash">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>
2801 <option value="gemini-pro">Gemini Pro</option>2818 <option value="gemini-pro">Gemini Pro (1.0)</option>
2802 <option value="gemini-pro-vision">Gemini Pro Vision</option>2819 <option value="gemini-pro-vision">Gemini Pro Vision (1.0)</option>
2803 <option value="gemini-ultra">Gemini Ultra</option>2820 <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>
2811 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>2821 <option value="gemini-1.0-ultra-latest">Gemini 1.0 Ultra</option>
2822 <option value="text-bison-001">PaLM 2 (Legacy)</option>
2823 <option value="chat-bison-001">PaLM 2 Chat (Legacy)</option>
2824 </optgroup>
2825 <optgroup label="Subversions">
2826 <option value="gemini-1.5-pro-exp-0801">Gemini 1.5 Pro Experiment 2024-08-01</option>
2827 <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>
2828 <option value="gemini-1.5-pro-001">Gemini 1.5 Pro [001]</option>
2829 <option value="gemini-1.5-flash-latest">Gemini 1.5 Flash [latest]</option>
2830 <option value="gemini-1.5-flash-001">Gemini 1.5 Flash [001]</option>
2831 <option value="gemini-1.0-pro-latest">Gemini 1.0 Pro [latest]</option>
2832 <option value="gemini-1.0-pro-001">Gemini 1.0 Pro (Tuning) [001]</option>
2833 <option value="gemini-1.0-pro-vision-latest">Gemini 1.0 Pro Vision [latest]</option>
2812 </optgroup>2834 </optgroup>
2813 </select>2835 </select>
2814 </div>2836 </div>
@@ -2845,6 +2867,7 @@
2845 <option value="mistral-small-2402">mistral-small-2402</option>2867 <option value="mistral-small-2402">mistral-small-2402</option>
2846 <option value="mistral-medium-2312">mistral-medium-2312</option>2868 <option value="mistral-medium-2312">mistral-medium-2312</option>
2847 <option value="mistral-large-2402">mistral-large-2402</option>2869 <option value="mistral-large-2402">mistral-large-2402</option>
2870 <option value="mistral-large-2407">mistral-large-2407</option>
2848 <option value="codestral-2405">codestral-2405</option>2871 <option value="codestral-2405">codestral-2405</option>
2849 <option value="codestral-mamba-2407">codestral-mamba-2407</option>2872 <option value="codestral-mamba-2407">codestral-mamba-2407</option>
2850 </optgroup>2873 </optgroup>
@@ -2885,7 +2908,20 @@
2885 </div>2908 </div>
2886 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>2909 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
2887 <select id="model_perplexity_select">2910 <select id="model_perplexity_select">
2888 <optgroup label="Perplexity Models">2911 <optgroup label="Perplexity Sonar Models">
2912 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
2913 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
2914 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
2915 </optgroup>
2916 <optgroup label="Perplexity Chat Models">
2917 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
2918 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
2919 </optgroup>
2920 <optgroup label="Open-Source Models">
2921 <option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option>
2922 <option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option>
2923 </optgroup>
2924 <optgroup label="Deprecated Models">
2889 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>2925 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
2890 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>2926 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
2891 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>2927 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
@@ -2894,8 +2930,6 @@
2894 <option value="sonar-small-online">sonar-small-online</option>2930 <option value="sonar-small-online">sonar-small-online</option>
2895 <option value="sonar-medium-chat">sonar-medium-chat</option>2931 <option value="sonar-medium-chat">sonar-medium-chat</option>
2896 <option value="sonar-medium-online">sonar-medium-online</option>2932 <option value="sonar-medium-online">sonar-medium-online</option>
2897 </optgroup>
2898 <optgroup label="Open-Source Models">
2899 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>2933 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
2900 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>2934 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
2901 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>2935 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
@@ -2929,6 +2963,20 @@
2929 </select>2963 </select>
2930 </div>2964 </div>
2931 </form>2965 </form>
2966 <form id="blockentropy_form" data-source="blockentropy">
2967 <h4 data-i18n="Block Entropy API Key">Block Entropy API Key</h4>
2968 <div class="flex-container">
2969 <input id="api_key_blockentropy" name="api_key_blockentropy" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
2970 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_blockentropy"></div>
2971 </div>
2972 <div data-for="api_key_blockentropy" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
2973 For privacy reasons, your API key will be hidden after you reload the page.
2974 </div>
2975 <h4 data-i18n="Select a Model">Select a Model</h4>
2976 <div class="flex-container">
2977 <select id="model_blockentropy_select" class="text_pole"></select>
2978 </div>
2979 </form>
2932 <form id="custom_form" data-source="custom">2980 <form id="custom_form" data-source="custom">
2933 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>2981 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
2934 <div class="flex-container">2982 <div class="flex-container">
@@ -3060,7 +3108,11 @@
3060 </div>3108 </div>
3061 <label class="checkbox_label" title="Add Chat Start and Example Separator to a list of stopping strings." data-i18n="[title]Add Chat Start and Example Separator to a list of stopping strings.">3109 <label class="checkbox_label" title="Add Chat Start and Example Separator to a list of stopping strings." data-i18n="[title]Add Chat Start and Example Separator to a list of stopping strings.">
3062 <input id="context_use_stop_strings" type="checkbox" />3110 <input id="context_use_stop_strings" type="checkbox" />
3063 <small data-i18n="Use as Stop Strings">Use as Stop Strings</small>3111 <small data-i18n="Separators as Stop Strings">Separators as Stop Strings</small>
3112 </label>
3113 <label class="checkbox_label" title="Add Character and User names to a list of stopping strings." data-i18n="[title]Add Character and User names to a list of stopping strings.">
3114 <input id="context_names_as_stop_strings" type="checkbox" />
3115 <small data-i18n="Names as Stop Strings">Names as Stop Strings</small>
3064 </label>3116 </label>
3065 <label class="checkbox_label" title="Includes Post-History Instructions at the end of the prompt, if defined in the character card AND ''Prefer Char. Instructions'' is enabled.&#10;THIS IS NOT RECOMMENDED FOR TEXT COMPLETION MODELS, CAN LEAD TO BAD OUTPUT." data-i18n="[title]context_allow_post_history_instructions">3117 <label class="checkbox_label" title="Includes Post-History Instructions at the end of the prompt, if defined in the character card AND ''Prefer Char. Instructions'' is enabled.&#10;THIS IS NOT RECOMMENDED FOR TEXT COMPLETION MODELS, CAN LEAD TO BAD OUTPUT." data-i18n="[title]context_allow_post_history_instructions">
3066 <input id="context_allow_jailbreak" type="checkbox" />3118 <input id="context_allow_jailbreak" type="checkbox" />
@@ -3288,6 +3340,24 @@
3288 </div>3340 </div>
3289 </div>3341 </div>
3290 <div class="flex-container">3342 <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">
3291 <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.">3361 <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.">
3292 <label for="instruct_last_system_sequence">3362 <label for="instruct_last_system_sequence">
3293 <small data-i18n="System Instruction Prefix">System Instruction Prefix</small>3363 <small data-i18n="System Instruction Prefix">System Instruction Prefix</small>
@@ -3379,6 +3449,7 @@
3379 <!-- Option #2 was a legacy GPT-2/3 tokenizer -->3449 <!-- Option #2 was a legacy GPT-2/3 tokenizer -->
3380 <option value="3">Llama 1/2</option>3450 <option value="3">Llama 1/2</option>
3381 <option value="12">Llama 3</option>3451 <option value="12">Llama 3</option>
3452 <option value="13">Gemma / Gemini</option>
3382 <option value="4">NerdStash (NovelAI Clio)</option>3453 <option value="4">NerdStash (NovelAI Clio)</option>
3383 <option value="5">NerdStash v2 (NovelAI Kayra)</option>3454 <option value="5">NerdStash v2 (NovelAI Kayra)</option>
3384 <option value="7">Mistral</option>3455 <option value="7">Mistral</option>
@@ -3635,8 +3706,8 @@
3635 <div id="OpenAllWIEntries" class="menu_button fa-solid fa-expand" title="Open all Entries" data-i18n="[title]Open all Entries"></div>3706 <div id="OpenAllWIEntries" class="menu_button fa-solid fa-expand" title="Open all Entries" data-i18n="[title]Open all Entries"></div>
3636 <div id="CloseAllWIEntries" class="menu_button fa-solid fa-compress" title="Close all Entries" data-i18n="[title]Close all Entries"></div>3707 <div id="CloseAllWIEntries" class="menu_button fa-solid fa-compress" title="Close all Entries" data-i18n="[title]Close all Entries"></div>
3637 <div id="world_popup_new" class="menu_button fa-solid fa-plus" title="New Entry" data-i18n="[title]New Entry"></div>3708 <div id="world_popup_new" class="menu_button fa-solid fa-plus" title="New Entry" data-i18n="[title]New Entry"></div>
3638 <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"3709 <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>
3639 title="Apply custom sorting as Order" data-i18n="[title]Apply custom sorting as Order"></div>3710 <div id="world_apply_current_sorting" class="menu_button fa-solid fa-solid fa-arrow-down-9-1" title="Apply current sorting as Order" data-i18n="[title]Apply current sorting as Order"></div>
3640 <div id="world_import_button" class="menu_button fa-solid fa-file-import" title="Import World Info" data-i18n="[title]Import World Info"></div>3711 <div id="world_import_button" class="menu_button fa-solid fa-file-import" title="Import World Info" data-i18n="[title]Import World Info"></div>
3641 <div id="world_popup_export" class="menu_button fa-solid fa-file-export" title="Export World Info" data-i18n="[title]Export World Info"></div>3712 <div id="world_popup_export" class="menu_button fa-solid fa-file-export" title="Export World Info" data-i18n="[title]Export World Info"></div>
3642 <div id="world_duplicate" class="menu_button fa-solid fa-paste" title="Duplicate World Info" data-i18n="[title]Duplicate World Info"></div>3713 <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 @@
3999 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">4070 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">
4000 <div class="slider_hint">4071 <div class="slider_hint">
4001 <span data-i18n="Slow">Slow</span>4072 <span data-i18n="Slow">Slow</span>
4002 <span data-i18n=""></span>4073 <span></span>
4003 <span data-i18n="Fast">Fast</span>4074 <span data-i18n="Fast">Fast</span>
4004 </div>4075 </div>
4005 </div>4076 </div>
@@ -4031,21 +4102,23 @@
4031 <input id="restore_user_input" type="checkbox" />4102 <input id="restore_user_input" type="checkbox" />
4032 <small data-i18n="Restore User Input">Restore User Input</small>4103 <small data-i18n="Restore User Input">Restore User Input</small>
4033 </label>4104 </label>
4034 <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">4105 <div class="flex-container alignItemsCenter">
4035 <input id="movingUImode" type="checkbox" />4106 <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">
4036 <small data-i18n="Movable UI Panels">MovingUI&nbsp;<i class="fa-solid fa-desktop"></i></small>4107 <input id="movingUImode" type="checkbox" />
4037 </label>4108 <small data-i18n="Movable UI Panels">MovingUI&nbsp;<i class="fa-solid fa-desktop"></i></small>
4109 </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>
4038 <div data-newbie-hidden id="MovingUI-presets-block" class="flex-container alignitemscenter">4112 <div data-newbie-hidden id="MovingUI-presets-block" class="flex-container alignitemscenter">
4039 <div class="flex-container alignItemsFlexEnd">4113 <div class="flex-container alignItemsFlexEnd">
4040 <label for="movingUIPresets" title="MovingUI preset. Predefined/saved draggable positions." data-i18n="[title]MovingUI preset. Predefined/saved draggable positions">4114 <label for="movingUIPresets" title="MovingUI preset. Predefined/saved draggable positions." data-i18n="[title]MovingUI preset. Predefined/saved draggable positions">
4041 <small data-i18n="MUI Preset">MUI Preset:</small>4115 <small data-i18n="MUI Preset">MovingUI Preset:</small>
4042 <div class="flex-container flexnowrap">4116 <div class="flex-container flexnowrap">
4043 <select id="movingUIPresets" class="widthNatural flex1 margin0">4117 <select id="movingUIPresets" class="widthNatural flex1 margin0">
4044 </select>4118 </select>
4045 </div>4119 </div>
4046 </label>4120 </label>
4047 <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>4121 <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>
4049 </div>4122 </div>
4050 </div>4123 </div>
4051 <div data-newbie-hidden id="CustomCSS-block" class="flex-container flexFlowColumn">4124 <div data-newbie-hidden id="CustomCSS-block" class="flex-container flexFlowColumn">
@@ -4117,6 +4190,12 @@
4117 Quick "Continue" button4190 Quick "Continue" button
4118 </small>4191 </small>
4119 </label>4192 </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>
4120 <div class="checkbox-container flex-container">4199 <div class="checkbox-container flex-container">
4121 <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">4200 <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">
4122 <input id="swipes-checkbox" type="checkbox" />4201 <input id="swipes-checkbox" type="checkbox" />
@@ -4237,6 +4316,16 @@
4237 </div>4316 </div>
4238 </div>4317 </div>
4239 </div>4318 </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>
4240 <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">4329 <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">
4241 <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>4330 <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>
4242 <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">4331 <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">
@@ -4275,7 +4364,7 @@
4275 <span class="fa-solid fa-circle-question note-link-span"></span>4364 <span class="fa-solid fa-circle-question note-link-span"></span>
4276 </a>4365 </a>
4277 </label>4366 </label>
4278 <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.)">4367 <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]stscript_parser_flag_replace_getvar_label">
4279 <input id="stscript_parser_flag_replace_getvar" type="checkbox" />4368 <input id="stscript_parser_flag_replace_getvar" type="checkbox" />
4280 <span data-i18n="REPLACE_GETVAR"><small>REPLACE_GETVAR</small></span>4369 <span data-i18n="REPLACE_GETVAR"><small>REPLACE_GETVAR</small></span>
4281 <a href="https://docs.sillytavern.app/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link">4370 <a href="https://docs.sillytavern.app/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link">
@@ -4578,7 +4667,7 @@
4578 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>4667 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>
4579 <input type="hidden" id="fav_checkbox" name="fav" />4668 <input type="hidden" id="fav_checkbox" name="fav" />
4580 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>4669 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
4581 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore" data-i18n="[title]Character Lore"></div>4670 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div>
4582 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>4671 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
4583 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>4672 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
4584 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->4673 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
@@ -5235,21 +5324,22 @@
5235 <div class="world_entry">5324 <div class="world_entry">
5236 <form class="world_entry_form wi-card-entry">5325 <form class="world_entry_form wi-card-entry">
5237 <div class="inline-drawer wide100p">5326 <div class="inline-drawer wide100p">
5238 <div class="inline-drawer-toggle inline-drawer-header gap5px padding0">5327 <div class="inline-drawer-header gap5px padding0">
5239 <span class="drag-handle">&#9776;</span>5328 <span class="drag-handle">&#9776;</span>
5240 <div class="gap5px world_entry_thin_controls wide100p alignitemscenter">5329 <div class="gap5px world_entry_thin_controls wide100p alignitemscenter">
5241 <div class="fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>5330 <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>
5242 <div class="flex-container alignitemscenter wide100p">5332 <div class="flex-container alignitemscenter wide100p">
5333
5243 <div class="WIEntryTitleAndStatus flex-container flex1 alignitemscenter">5334 <div class="WIEntryTitleAndStatus flex-container flex1 alignitemscenter">
5335
5244 <div class="flex-container flex1">5336 <div class="flex-container flex1">
5245 <textarea class="text_pole" rows="1" name="comment" maxlength="5000" data-i18n="[placeholder]Entry Title/Memo" placeholder="Entry Title/Memo"></textarea>5337 <textarea class="text_pole" rows="1" name="comment" maxlength="5000" data-i18n="[placeholder]Entry Title/Memo" placeholder="Entry Title/Memo"></textarea>
5246 </div>5338 </div>
5247 <!-- <span class="world_entry_form_position_value"></span> -->
5248 <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">5339 <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">
5249 <option value="constant" title="Constant" data-i18n="[title]WI_Entry_Status_Constant">🔵</option>5340 <option value="constant" title="Constant" data-i18n="[title]WI_Entry_Status_Constant">🔵</option>
5250 <option value="normal" title="Normal" data-i18n="[title]WI_Entry_Status_Normal">🟢</option>5341 <option value="normal" title="Normal" data-i18n="[title]WI_Entry_Status_Normal">🟢</option>
5251 <option value="vectorized" title="Vectorized" data-i18n="[title]WI_Entry_Status_Vectorized">🔗</option>5342 <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>
5253 </select>5343 </select>
5254 </div>5344 </div>
5255 <div class="WIEnteryHeaderControls flex-container">5345 <div class="WIEnteryHeaderControls flex-container">
@@ -5722,6 +5812,11 @@
5722 <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div>5812 <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div>
5723 <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div>5813 <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div>
5724 </div>5814 </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>
5725 <img class="mes_img" src="" />5820 <img class="mes_img" src="" />
5726 </div>5821 </div>
5727 <div class="mes_bias"></div>5822 <div class="mes_bias"></div>
@@ -5784,6 +5879,7 @@
5784 Enable simple UI mode5879 Enable simple UI mode
5785 </span>5880 </span>
5786 </label>5881 </label>
5882 <div class="expander"></div>
5787 <div class="textAlignCenter">5883 <div class="textAlignCenter">
5788 <h3 data-i18n="Looking for AI characters?">5884 <h3 data-i18n="Looking for AI characters?">
5789 Looking for AI characters?5885 Looking for AI characters?
@@ -5802,6 +5898,7 @@
5802 </span>5898 </span>
5803 </span>5899 </span>
5804 </div>5900 </div>
5901 <div class="expander"></div>
5805 <h3 data-i18n="Your Persona">5902 <h3 data-i18n="Your Persona">
5806 Your Persona5903 Your Persona
5807 </h3>5904 </h3>
@@ -6315,6 +6412,7 @@
6315 <div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">6412 <div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
6316 <i class="fa-solid fa-circle-stop"></i>6413 <i class="fa-solid fa-circle-stop"></i>
6317 </div>6414 </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>
6318 <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>6416 <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>
6319 <div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>6417 <div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
6320 </div>6418 </div>
@@ -6431,6 +6529,9 @@
6431 <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.">6529 <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.">
6432 <i class="fa-fw fa-solid fa-crown"></i>6530 <i class="fa-fw fa-solid fa-crown"></i>
6433 </button>6531 </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>
6434 <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona">6535 <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona">
6435 <i class="fa-fw fa-solid fa-trash-alt"></i>6536 <i class="fa-fw fa-solid fa-trash-alt"></i>
6436 </button>6537 </button>
public/locales/ar-sa.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "مخطط جيسون",207 "JSON Schema": "مخطط جيسون",
208 "Type in the desired JSON schema": "اكتب مخطط JSON المطلوب",208 "Type in the desired JSON schema": "اكتب مخطط JSON المطلوب",
209 "Grammar String": "سلسلة القواعد",209 "Grammar String": "سلسلة القواعد",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "يعتمد GNBF أو ENBF على الواجهة الخلفية المستخدمة. إذا كنت تستخدم هذا يجب أن تعرف أي.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "يعتمد GBNF أو EBNF على الواجهة الخلفية المستخدمة. إذا كنت تستخدم هذا يجب أن تعرف أي.",
211 "Top P & Min P": "أعلى ع وأدنى ص",211 "Top P & Min P": "أعلى ع وأدنى ص",
212 "Load default order": "تحميل الترتيب الافتراضي",212 "Load default order": "تحميل الترتيب الافتراضي",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp فقط. تحديد ترتيب أخذ العينات. إذا لم يكن وضع Mirostat 0، فسيتم تجاهل ترتيب أخذ العينات.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp فقط. تحديد ترتيب أخذ العينات. إذا لم يكن وضع Mirostat 0، فسيتم تجاهل ترتيب أخذ العينات.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "سلوك أسماء الشخصيات",216 "Character Names Behavior": "سلوك أسماء الشخصيات",
217 "Helps the model to associate messages with characters.": "يساعد النموذج على ربط الرسائل بالأحرف.",217 "Helps the model to associate messages with characters.": "يساعد النموذج على ربط الرسائل بالأحرف.",
218 "None": "لا شيء",218 "None": "لا شيء",
219 "character_names_none": "باستثناء المجموعات والشخصيات السابقة. بخلاف ذلك، تأكد من تقديم الأسماء في المطالبة.",219 "character_names_default": "باستثناء المجموعات والشخصيات السابقة. بخلاف ذلك، تأكد من تقديم الأسماء في المطالبة.",
220 "Don't add character names.": "لا تضيف أسماء الشخصيات.",220 "Don't add character names.": "لا تضيف أسماء الشخصيات.",
221 "Completion": "كائن الإكمال",221 "Completion": "كائن الإكمال",
222 "character_names_completion": "تنطبق القيود: فقط الحروف الأبجدية اللاتينية والأرقام والشرطات السفلية. لا يعمل مع جميع المصادر، ولا سيما: Claude وMistralAI وGoogle.",222 "character_names_completion": "تنطبق القيود: فقط الحروف الأبجدية اللاتينية والأرقام والشرطات السفلية. لا يعمل مع جميع المصادر، ولا سيما: Claude وMistralAI وGoogle.",
public/locales/de-de.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "JSON-Schema",207 "JSON Schema": "JSON-Schema",
208 "Type in the desired JSON schema": "Geben Sie das gewünschte JSON-Schema ein",208 "Type in the desired JSON schema": "Geben Sie das gewünschte JSON-Schema ein",
209 "Grammar String": "Grammatikzeichenfolge",209 "Grammar String": "Grammatikzeichenfolge",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF oder ENBF, hängt vom verwendeten Backend ab. Wenn Sie dieses verwenden, sollten Sie wissen, welches.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF oder EBNF, hängt vom verwendeten Backend ab. Wenn Sie dieses verwenden, sollten Sie wissen, welches.",
211 "Top P & Min P": "Top P und Min P",211 "Top P & Min P": "Top P und Min P",
212 "Load default order": "Standardreihenfolge laden",212 "Load default order": "Standardreihenfolge laden",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Nur llama.cpp. Bestimmt die Reihenfolge der Sampler. Wenn der Mirostat-Modus nicht 0 ist, wird die Sampler-Reihenfolge ignoriert.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Nur llama.cpp. Bestimmt die Reihenfolge der Sampler. Wenn der Mirostat-Modus nicht 0 ist, wird die Sampler-Reihenfolge ignoriert.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Charakternamen Verhalten",216 "Character Names Behavior": "Charakternamen Verhalten",
217 "Helps the model to associate messages with characters.": "Hilft dem Modell, Nachrichten mit Zeichen zu verknüpfen.",217 "Helps the model to associate messages with characters.": "Hilft dem Modell, Nachrichten mit Zeichen zu verknüpfen.",
218 "None": "Keins",218 "None": "Keins",
219 "character_names_none": "Außer für Gruppen und frühere Personas. Andernfalls stellen Sie sicher, dass Sie in der Eingabeaufforderung Namen angeben.",219 "character_names_default": "Außer für Gruppen und frühere Personas. Andernfalls stellen Sie sicher, dass Sie in der Eingabeaufforderung Namen angeben.",
220 "Don't add character names.": "Fügen Sie keine Charakternamen hinzu.",220 "Don't add character names.": "Fügen Sie keine Charakternamen hinzu.",
221 "Completion": "Vervollständigungsobjekt",221 "Completion": "Vervollständigungsobjekt",
222 "character_names_completion": "Es gelten Einschränkungen: nur lateinische alphanumerische Zeichen und Unterstriche. Funktioniert nicht für alle Quellen, insbesondere: Claude, MistralAI, Google.",222 "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 @@
207 "JSON Schema": "Esquema JSON",207 "JSON Schema": "Esquema JSON",
208 "Type in the desired JSON schema": "Escriba el esquema JSON deseado",208 "Type in the desired JSON schema": "Escriba el esquema JSON deseado",
209 "Grammar String": "Cadena de gramática",209 "Grammar String": "Cadena de gramática",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF o ENBF, depende del backend en uso. Si estás usando esto, debes saber cuál.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF o EBNF, depende del backend en uso. Si estás usando esto, debes saber cuál.",
211 "Top P & Min P": "P superior y P mínima",211 "Top P & Min P": "P superior y P mínima",
212 "Load default order": "Cargar orden predeterminado",212 "Load default order": "Cargar orden predeterminado",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp únicamente. Determina el orden de los muestreadores. Si el modo Mirostat no es 0, se ignora el orden de las muestras.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp únicamente. Determina el orden de los muestreadores. Si el modo Mirostat no es 0, se ignora el orden de las muestras.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Comportamiento de los nombres de personajes",216 "Character Names Behavior": "Comportamiento de los nombres de personajes",
217 "Helps the model to associate messages with characters.": "Ayuda al modelo a asociar mensajes con personajes.",217 "Helps the model to associate messages with characters.": "Ayuda al modelo a asociar mensajes con personajes.",
218 "None": "Ninguno",218 "None": "Ninguno",
219 "character_names_none": "Excepto grupos y personas pasadas. De lo contrario, asegúrese de proporcionar nombres en el mensaje.",219 "character_names_default": "Excepto grupos y personas pasadas. De lo contrario, asegúrese de proporcionar nombres en el mensaje.",
220 "Don't add character names.": "No agregues nombres de personajes.",220 "Don't add character names.": "No agregues nombres de personajes.",
221 "Completion": "Objeto de finalización",221 "Completion": "Objeto de finalización",
222 "character_names_completion": "Aplican restricciones: solo caracteres alfanuméricos latinos y guiones bajos. No funciona para todas las fuentes, en particular: Claude, MistralAI, Google.",222 "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 @@
207 "JSON Schema": "Schéma JSON",207 "JSON Schema": "Schéma JSON",
208 "Type in the desired JSON schema": "Tapez le schéma JSON souhaité",208 "Type in the desired JSON schema": "Tapez le schéma JSON souhaité",
209 "Grammar String": "Chaîne de grammaire",209 "Grammar String": "Chaîne de grammaire",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF ou ENBF dépend du backend utilisé. Si vous l'utilisez, vous devez savoir lequel.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF ou EBNF dépend du backend utilisé. Si vous l'utilisez, vous devez savoir lequel.",
211 "Top P & Min P": "P supérieur et P minimal",211 "Top P & Min P": "P supérieur et P minimal",
212 "Load default order": "Charger l'ordre par défaut",212 "Load default order": "Charger l'ordre par défaut",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "lama.cpp uniquement. Détermine l’ordre des échantillonneurs. Si le mode Mirostat n'est pas 0, l'ordre de l'échantillonneur est ignoré.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "lama.cpp uniquement. Détermine l’ordre des échantillonneurs. Si le mode Mirostat n'est pas 0, l'ordre de l'échantillonneur est ignoré.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Comportement des noms de personnages",216 "Character Names Behavior": "Comportement des noms de personnages",
217 "Helps the model to associate messages with characters.": "Aide le modèle à associer des messages à des personnages.",217 "Helps the model to associate messages with characters.": "Aide le modèle à associer des messages à des personnages.",
218 "None": "Aucun",218 "None": "Aucun",
219 "character_names_none": "Sauf pour les groupes et les personnages passés. Sinon, assurez-vous de fournir des noms dans l'invite.",219 "character_names_default": "Sauf pour les groupes et les personnages passés. Sinon, assurez-vous de fournir des noms dans l'invite.",
220 "Don't add character names.": "N'ajoutez pas de noms de personnages.",220 "Don't add character names.": "N'ajoutez pas de noms de personnages.",
221 "Completion": "Objet d'achèvement",221 "Completion": "Objet d'achèvement",
222 "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.",222 "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 @@
207 "JSON Schema": "JSON kerfi",207 "JSON Schema": "JSON kerfi",
208 "Type in the desired JSON schema": "Sláðu inn æskilegt JSON skema",208 "Type in the desired JSON schema": "Sláðu inn æskilegt JSON skema",
209 "Grammar String": "Málfræðistrengur",209 "Grammar String": "Málfræðistrengur",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF eða ENBF, fer eftir bakendanum sem er í notkun. Ef þú ert að nota þetta ættir þú að vita hvaða.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF eða EBNF, fer eftir bakendanum sem er í notkun. Ef þú ert að nota þetta ættir þú að vita hvaða.",
211 "Top P & Min P": "Efstu P & Min P",211 "Top P & Min P": "Efstu P & Min P",
212 "Load default order": "Hlaða sjálfgefna röð",212 "Load default order": "Hlaða sjálfgefna röð",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp eingöngu. Ákveður röð sýnataka. Ef Mirostat hamur er ekki 0, er röð sýnatöku hunsuð.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp eingöngu. Ákveður röð sýnataka. Ef Mirostat hamur er ekki 0, er röð sýnatöku hunsuð.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Hegðun persónunafna",216 "Character Names Behavior": "Hegðun persónunafna",
217 "Helps the model to associate messages with characters.": "Hjálpar líkaninu að tengja skilaboð við stafi.",217 "Helps the model to associate messages with characters.": "Hjálpar líkaninu að tengja skilaboð við stafi.",
218 "None": "Enginn",218 "None": "Enginn",
219 "character_names_none": "Nema hópar og fyrri persónur. Annars, vertu viss um að gefa upp nöfn í hvetjunni.",219 "character_names_default": "Nema hópar og fyrri persónur. Annars, vertu viss um að gefa upp nöfn í hvetjunni.",
220 "Don't add character names.": "Ekki bæta við persónunöfnum.",220 "Don't add character names.": "Ekki bæta við persónunöfnum.",
221 "Completion": "Lokunarhlutur",221 "Completion": "Lokunarhlutur",
222 "character_names_completion": "Takmarkanir gilda: aðeins latneskar tölustafir og undirstrik. Virkar ekki fyrir allar heimildir, sérstaklega: Claude, MistralAI, Google.",222 "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 @@
207 "JSON Schema": "Schema JSON",207 "JSON Schema": "Schema JSON",
208 "Type in the desired JSON schema": "Digita lo schema JSON desiderato",208 "Type in the desired JSON schema": "Digita lo schema JSON desiderato",
209 "Grammar String": "Stringa grammaticale",209 "Grammar String": "Stringa grammaticale",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF o ENBF, dipende dal backend in uso. Se stai usando questo dovresti sapere quale.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF o EBNF, dipende dal backend in uso. Se stai usando questo dovresti sapere quale.",
211 "Top P & Min P": "P massimo e P minimo",211 "Top P & Min P": "P massimo e P minimo",
212 "Load default order": "Carica ordine predefinito",212 "Load default order": "Carica ordine predefinito",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Solo lama.cpp. Determina l'ordine dei campionatori. Se la modalità Mirostat non è 0, l'ordine del campionatore viene ignorato.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Solo lama.cpp. Determina l'ordine dei campionatori. Se la modalità Mirostat non è 0, l'ordine del campionatore viene ignorato.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Comportamento dei nomi dei personaggi",216 "Character Names Behavior": "Comportamento dei nomi dei personaggi",
217 "Helps the model to associate messages with characters.": "Aiuta il modello ad associare i messaggi ai personaggi.",217 "Helps the model to associate messages with characters.": "Aiuta il modello ad associare i messaggi ai personaggi.",
218 "None": "Nessuno",218 "None": "Nessuno",
219 "character_names_none": "Fatta eccezione per i gruppi e i personaggi passati. Altrimenti, assicurati di fornire i nomi nel prompt.",219 "character_names_default": "Fatta eccezione per i gruppi e i personaggi passati. Altrimenti, assicurati di fornire i nomi nel prompt.",
220 "Don't add character names.": "Non aggiungere nomi di personaggi.",220 "Don't add character names.": "Non aggiungere nomi di personaggi.",
221 "Completion": "Oggetto di completamento",221 "Completion": "Oggetto di completamento",
222 "character_names_completion": "Si applicano restrizioni: solo caratteri alfanumerici latini e trattini bassi. Non funziona con tutte le fonti, in particolare: Claude, MistralAI, Google.",222 "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 @@
207 "JSON Schema": "JSONスキーマ",207 "JSON Schema": "JSONスキーマ",
208 "Type in the desired JSON schema": "希望するJSONスキーマを入力します",208 "Type in the desired JSON schema": "希望するJSONスキーマを入力します",
209 "Grammar String": "文法文字列",209 "Grammar String": "文法文字列",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF または ENBF は、使用するバックエンドによって異なります。これを使用する場合は、どちらであるかを知っておく必要があります。",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF または EBNF は、使用するバックエンドによって異なります。これを使用する場合は、どちらであるかを知っておく必要があります。",
211 "Top P & Min P": "トップPと最小P",211 "Top P & Min P": "トップPと最小P",
212 "Load default order": "デフォルトの順序を読み込む",212 "Load default order": "デフォルトの順序を読み込む",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp のみ。サンプラーの順序を決定します。Mirostat モードが 0 でない場合、サンプラーの順序は無視されます。",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp のみ。サンプラーの順序を決定します。Mirostat モードが 0 でない場合、サンプラーの順序は無視されます。",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "キャラクター名の動作",216 "Character Names Behavior": "キャラクター名の動作",
217 "Helps the model to associate messages with characters.": "モデルがメッセージをキャラクターに関連付けるのに役立ちます。",217 "Helps the model to associate messages with characters.": "モデルがメッセージをキャラクターに関連付けるのに役立ちます。",
218 "None": "なし",218 "None": "なし",
219 "character_names_none": "グループと過去のペルソナを除きます。それ以外の場合は、プロンプトに名前を必ず入力してください。",219 "character_names_default": "グループと過去のペルソナを除きます。それ以外の場合は、プロンプトに名前を必ず入力してください。",
220 "Don't add character names.": "キャラクター名を追加しないでください。",220 "Don't add character names.": "キャラクター名を追加しないでください。",
221 "Completion": "完了オブジェクト",221 "Completion": "完了オブジェクト",
222 "character_names_completion": "制限事項: ラテン英数字とアンダースコアのみ。すべてのソースで機能するわけではありません。特に、Claude、MistralAI、Google では機能しません。",222 "character_names_completion": "制限事項: ラテン英数字とアンダースコアのみ。すべてのソースで機能するわけではありません。特に、Claude、MistralAI、Google では機能しません。",
public/locales/ko-kr.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "JSON 스키마",207 "JSON Schema": "JSON 스키마",
208 "Type in the desired JSON schema": "원하는 JSON 스키마를 입력하세요.",208 "Type in the desired JSON schema": "원하는 JSON 스키마를 입력하세요.",
209 "Grammar String": "문법 문자열",209 "Grammar String": "문법 문자열",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 또는 ENBF는 사용 중인 백엔드에 따라 다릅니다. 이것을 사용한다면 어느 것이 무엇인지 알아야합니다.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 또는 EBNF는 사용 중인 백엔드에 따라 다릅니다. 이것을 사용한다면 어느 것이 무엇인지 알아야합니다.",
211 "Top P & Min P": "상위 P 및 최소 P",211 "Top P & Min P": "상위 P 및 최소 P",
212 "Load default order": "기본 순서로 로드",212 "Load default order": "기본 순서로 로드",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp만 가능합니다. 샘플러의 순서를 결정합니다. Mirostat 모드가 0이 아닌 경우 샘플러 순서는 무시됩니다.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp만 가능합니다. 샘플러의 순서를 결정합니다. Mirostat 모드가 0이 아닌 경우 샘플러 순서는 무시됩니다.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "캐릭터 이름 행동",216 "Character Names Behavior": "캐릭터 이름 행동",
217 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",217 "Helps the model to associate messages with characters.": "모델이 메시지를 캐릭터와 연관시키는 데 도움이 됩니다.",
218 "None": "없음",218 "None": "없음",
219 "character_names_none": "그룹 및 과거 페르소나는 제외됩니다. 그렇지 않으면 프롬프트에 이름을 제공해야 합니다.",219 "character_names_default": "그룹 및 과거 페르소나는 제외됩니다. 그렇지 않으면 프롬프트에 이름을 제공해야 합니다.",
220 "Don't add character names.": "캐릭터 이름을 추가하지 마세요.",220 "Don't add character names.": "캐릭터 이름을 추가하지 마세요.",
221 "Completion": "완료 객체",221 "Completion": "완료 객체",
222 "character_names_completion": "제한 사항이 적용됩니다. 라틴 영숫자 및 밑줄만 사용할 수 있습니다. 모든 소스, 특히 Claude, MistralAI, Google에서 작동하지 않습니다.",222 "character_names_completion": "제한 사항이 적용됩니다. 라틴 영숫자 및 밑줄만 사용할 수 있습니다. 모든 소스, 특히 Claude, MistralAI, Google에서 작동하지 않습니다.",
public/locales/nl-nl.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "JSON-schema",207 "JSON Schema": "JSON-schema",
208 "Type in the desired JSON schema": "Typ het gewenste JSON-schema",208 "Type in the desired JSON schema": "Typ het gewenste JSON-schema",
209 "Grammar String": "Grammaticareeks",209 "Grammar String": "Grammaticareeks",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF of ENBF, hangt af van de gebruikte backend. Als u dit gebruikt, moet u weten welke.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF of EBNF, hangt af van de gebruikte backend. Als u dit gebruikt, moet u weten welke.",
211 "Top P & Min P": "Top P & Min P",211 "Top P & Min P": "Top P & Min P",
212 "Load default order": "Standaardvolgorde laden",212 "Load default order": "Standaardvolgorde laden",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "alleen lama.cpp. Bepaalt de volgorde van de samplers. Als de Mirostat-modus niet 0 is, wordt de samplervolgorde genegeerd.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "alleen lama.cpp. Bepaalt de volgorde van de samplers. Als de Mirostat-modus niet 0 is, wordt de samplervolgorde genegeerd.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Karakternamen Gedrag",216 "Character Names Behavior": "Karakternamen Gedrag",
217 "Helps the model to associate messages with characters.": "Helpt het model berichten aan karakters te koppelen.",217 "Helps the model to associate messages with characters.": "Helpt het model berichten aan karakters te koppelen.",
218 "None": "Geen",218 "None": "Geen",
219 "character_names_none": "Behalve voor groepen en vroegere persona's. Zorg er anders voor dat u namen opgeeft in de prompt.",219 "character_names_default": "Behalve voor groepen en vroegere persona's. Zorg er anders voor dat u namen opgeeft in de prompt.",
220 "Don't add character names.": "Voeg geen namen van personages toe.",220 "Don't add character names.": "Voeg geen namen van personages toe.",
221 "Completion": "Voltooiingsobject",221 "Completion": "Voltooiingsobject",
222 "character_names_completion": "Er zijn beperkingen van toepassing: alleen Latijnse alfanumerieke tekens en onderstrepingstekens. Werkt niet voor alle bronnen, met name: Claude, MistralAI, Google.",222 "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 @@
207 "JSON Schema": "Esquema JSON",207 "JSON Schema": "Esquema JSON",
208 "Type in the desired JSON schema": "Digite o esquema JSON desejado",208 "Type in the desired JSON schema": "Digite o esquema JSON desejado",
209 "Grammar String": "Cadeia de Gramática",209 "Grammar String": "Cadeia de Gramática",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF ou ENBF, depende do backend em uso. Se você estiver usando isso, você deve saber qual.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF ou EBNF, depende do backend em uso. Se você estiver usando isso, você deve saber qual.",
211 "Top P & Min P": "P superior e P mínimo",211 "Top P & Min P": "P superior e P mínimo",
212 "Load default order": "Carregar ordem padrão",212 "Load default order": "Carregar ordem padrão",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "apenas lhama.cpp. Determina a ordem dos amostradores. Se o modo Mirostat não for 0, a ordem do amostrador será ignorada.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "apenas lhama.cpp. Determina a ordem dos amostradores. Se o modo Mirostat não for 0, a ordem do amostrador será ignorada.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Comportamento dos nomes dos personagens",216 "Character Names Behavior": "Comportamento dos nomes dos personagens",
217 "Helps the model to associate messages with characters.": "Ajuda o modelo a associar mensagens a personagens.",217 "Helps the model to associate messages with characters.": "Ajuda o modelo a associar mensagens a personagens.",
218 "None": "Nenhum",218 "None": "Nenhum",
219 "character_names_none": "Exceto para grupos e personas passadas. Caso contrário, certifique-se de fornecer nomes no prompt.",219 "character_names_default": "Exceto para grupos e personas passadas. Caso contrário, certifique-se de fornecer nomes no prompt.",
220 "Don't add character names.": "Não adicione nomes de personagens.",220 "Don't add character names.": "Não adicione nomes de personagens.",
221 "Completion": "Objeto de conclusão",221 "Completion": "Objeto de conclusão",
222 "character_names_completion": "Aplicam-se restrições: apenas alfanuméricos latinos e sublinhados. Não funciona para todas as fontes, nomeadamente: Claude, MistralAI, Google.",222 "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+3 -3
@@ -978,7 +978,7 @@
978 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",978 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
979 "char_import_7": ")",979 "char_import_7": ")",
980 "Grammar String": "Грамматика",980 "Grammar String": "Грамматика",
981 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF или ENBF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",981 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
982 "Account": "Аккаунт",982 "Account": "Аккаунт",
983 "Hi,": "Привет,",983 "Hi,": "Привет,",
984 "To enable multi-account features, restart the SillyTavern server with": "Чтобы активировать систему аккаунтов, перезапустите SillyTavern, выставив",984 "To enable multi-account features, restart the SillyTavern server with": "Чтобы активировать систему аккаунтов, перезапустите SillyTavern, выставив",
@@ -1246,7 +1246,7 @@
1246 "Top P & Min P": "Top P & Min P",1246 "Top P & Min P": "Top P & Min P",
1247 "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.",1247 "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.",
1248 "Helps the model to associate messages with characters.": "Помогает модели связывать сообщения с персонажами.",1248 "Helps the model to associate messages with characters.": "Помогает модели связывать сообщения с персонажами.",
1249 "character_names_none": "Except for groups and past personas. Otherwise, make sure you provide names in the prompt.",1249 "character_names_default": "Except for groups and past personas. Otherwise, make sure you provide names in the prompt.",
1250 "Completion": "Completion Object",1250 "Completion": "Completion Object",
1251 "character_names_completion": "Только латинские буквы, цифры и знак подчёркивания. Работает не для всех бэкендов, в частности для Claude, MistralAI, Google.",1251 "character_names_completion": "Только латинские буквы, цифры и знак подчёркивания. Работает не для всех бэкендов, в частности для Claude, MistralAI, Google.",
1252 "Use AI21 Tokenizer": "Использовать токенайзер AI21",1252 "Use AI21 Tokenizer": "Использовать токенайзер AI21",
@@ -1662,4 +1662,4 @@
1662 "ext_regex_sts_desc": "Сообщения, отправленные с помощью команд STscript",1662 "ext_regex_sts_desc": "Сообщения, отправленные с помощью команд STscript",
1663 "ext_regex_wi_desc": "Содержимое лорбуков и миров. Для работы требует включения флажка \"Только промпт\"!",1663 "ext_regex_wi_desc": "Содержимое лорбуков и миров. Для работы требует включения флажка \"Только промпт\"!",
1664 "ext_regex_only_format_display_desc": "История чата не изменится, замена будет осуществляться только в отображаемом сообщении (в UI)"1664 "ext_regex_only_format_display_desc": "История чата не изменится, замена будет осуществляться только в отображаемом сообщении (в UI)"
1665}
1665 \ No newline at end of file \ No newline at end of file
1665}
public/locales/uk-ua.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "Схема JSON",207 "JSON Schema": "Схема JSON",
208 "Type in the desired JSON schema": "Введіть потрібну схему JSON",208 "Type in the desired JSON schema": "Введіть потрібну схему JSON",
209 "Grammar String": "Граматичний рядок",209 "Grammar String": "Граматичний рядок",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF або ENBF, залежить від серверної частини, яка використовується. Якщо ви використовуєте це, ви повинні знати, який.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF або EBNF, залежить від серверної частини, яка використовується. Якщо ви використовуєте це, ви повинні знати, який.",
211 "Top P & Min P": "Верхній P & Min P",211 "Top P & Min P": "Верхній P & Min P",
212 "Load default order": "Завантажити типовий порядок",212 "Load default order": "Завантажити типовий порядок",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "лише llama.cpp. Визначає порядок пробовідбірників. Якщо режим Mirostat не 0, порядок вибірки ігнорується.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "лише llama.cpp. Визначає порядок пробовідбірників. Якщо режим Mirostat не 0, порядок вибірки ігнорується.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Поведінка імен персонажів",216 "Character Names Behavior": "Поведінка імен персонажів",
217 "Helps the model to associate messages with characters.": "Допомагає моделі пов’язувати повідомлення з символами.",217 "Helps the model to associate messages with characters.": "Допомагає моделі пов’язувати повідомлення з символами.",
218 "None": "Немає",218 "None": "Немає",
219 "character_names_none": "За винятком груп і минулих персонажів. В іншому випадку переконайтеся, що ви вказали імена в підказці.",219 "character_names_default": "За винятком груп і минулих персонажів. В іншому випадку переконайтеся, що ви вказали імена в підказці.",
220 "Don't add character names.": "Не додавайте імена персонажів.",220 "Don't add character names.": "Не додавайте імена персонажів.",
221 "Completion": "Об'єкт завершення",221 "Completion": "Об'єкт завершення",
222 "character_names_completion": "Застосовуються обмеження: лише латинські букви та цифри підкреслення. Працює не для всіх джерел, зокрема: Claude, MistralAI, Google.",222 "character_names_completion": "Застосовуються обмеження: лише латинські букви та цифри підкреслення. Працює не для всіх джерел, зокрема: Claude, MistralAI, Google.",
public/locales/vi-vn.json+2 -2
@@ -207,7 +207,7 @@
207 "JSON Schema": "Lược đồ JSON",207 "JSON Schema": "Lược đồ JSON",
208 "Type in the desired JSON schema": "Nhập lược đồ JSON mong muốn",208 "Type in the desired JSON schema": "Nhập lược đồ JSON mong muốn",
209 "Grammar String": "Chuỗi ngữ pháp",209 "Grammar String": "Chuỗi ngữ pháp",
210 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF hoặc ENBF, tùy thuộc vào backend đang sử dụng. Nếu bạn đang sử dụng cái này, bạn nên biết cái nào.",210 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF hoặc EBNF, tùy thuộc vào backend đang sử dụng. Nếu bạn đang sử dụng cái này, bạn nên biết cái nào.",
211 "Top P & Min P": "P & P tối thiểu hàng đầu",211 "Top P & Min P": "P & P tối thiểu hàng đầu",
212 "Load default order": "Tải thứ tự mặc định",212 "Load default order": "Tải thứ tự mặc định",
213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "chỉ llama.cpp. Xác định thứ tự lấy mẫu. Nếu chế độ Mirostat khác 0, thứ tự lấy mẫu sẽ bị bỏ qua.",213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "chỉ llama.cpp. Xác định thứ tự lấy mẫu. Nếu chế độ Mirostat khác 0, thứ tự lấy mẫu sẽ bị bỏ qua.",
@@ -216,7 +216,7 @@
216 "Character Names Behavior": "Tên nhân vật Hành vi",216 "Character Names Behavior": "Tên nhân vật Hành vi",
217 "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ự.",217 "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ự.",
218 "None": "Không",218 "None": "Không",
219 "character_names_none": "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.",219 "character_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.",
220 "Don't add character names.": "Không thêm tên nhân vật.",220 "Don't add character names.": "Không thêm tên nhân vật.",
221 "Completion": "Đối tượng hoàn thành",221 "Completion": "Đối tượng hoàn thành",
222 "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.",222 "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 @@
69 "Top A": "Top A",69 "Top A": "Top A",
70 "Quick Prompts Edit": "快速提示词编辑",70 "Quick Prompts Edit": "快速提示词编辑",
71 "Main": "主要",71 "Main": "主要",
72 "NSFW": "NSFW",72 "Auxiliary": "辅助的",
73 "Jailbreak": "越狱",73 "Post-History Instructions": "后续历史指令",
74 "Utility Prompts": "实用提示词",74 "Utility Prompts": "实用提示词",
75 "Impersonation prompt": "AI帮答提示词",75 "Impersonation prompt": "AI帮答提示词",
76 "Restore default prompt": "恢复默认提示词",76 "Restore default prompt": "恢复默认提示词",
@@ -208,19 +208,21 @@
208 "JSON Schema": "JSON 结构",208 "JSON Schema": "JSON 结构",
209 "Type in the desired JSON schema": "输入所需的 JSON 结构",209 "Type in the desired JSON schema": "输入所需的 JSON 结构",
210 "Grammar String": "语法字符串",210 "Grammar String": "语法字符串",
211 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 或 ENBF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",211 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
212 "Top P & Min P": "Top P 和 Min P",212 "Top P & Min P": "Top P 和 Min P",
213 "Load default order": "加载默认顺序",213 "Load default order": "加载默认顺序",
214 "Sampler Order": "取样器顺序",
214 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。",215 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。",
215 "Sampler Priority": "采样器优先级",216 "Sampler Priority": "采样器优先级",
216 "Ooba only. Determines the order of samplers.": "确定采样器的顺序(仅适用于Ooba)",217 "Ooba only. Determines the order of samplers.": "确定采样器的顺序(仅适用于Ooba)",
217 "Character Names Behavior": "角色名称行为",218 "Character Names Behavior": "角色名称行为",
218 "Helps the model to associate messages with characters.": "有助于模型将消息与角色关联起来。",219 "Helps the model to associate messages with characters.": "有助于模型将消息与角色关联起来。",
219 "None": "无",220 "None": "无",
220 "tag_import_none": "无",221 "character_names_none": "不添加角色名称前缀。在群聊中可能导致错误行为,谨慎勾选。",
221 "character_names_none": "群聊和过去的角色除外。否则,请确保在提示词中提供了姓名。",222 "Never add character names.": "不添加角色名称。",
222 "Don't add character names.": "不添加角色名称。",223 "character_names_default": "群聊和过去的角色除外。否则,请确保在提示词中提供了姓名。",
223 "Completion": "补全对象",224 "Don't add character names unless necessary.": "如非必要,否则不添加角色名称。",
225 "Completion Object": "补全对象",
224 "character_names_completion": "适用限制:仅限拉丁字母数字和下划线。不适用于所有补全源,尤其是:Claude、MistralAI、Google。",226 "character_names_completion": "适用限制:仅限拉丁字母数字和下划线。不适用于所有补全源,尤其是:Claude、MistralAI、Google。",
225 "Add character names to completion objects.": "在补全对象中添加角色名称。",227 "Add character names to completion objects.": "在补全对象中添加角色名称。",
226 "Message Content": "消息内容",228 "Message Content": "消息内容",
@@ -318,6 +320,7 @@
318 "View Remaining Credits": "查看剩余额度",320 "View Remaining Credits": "查看剩余额度",
319 "OpenRouter Model": "OpenRouter 模型",321 "OpenRouter Model": "OpenRouter 模型",
320 "Model Providers": "模型提供者",322 "Model Providers": "模型提供者",
323 "Allow fallback providers": "允许后备提供者",
321 "InfermaticAI API Key": "InfermaticAI API 密钥",324 "InfermaticAI API Key": "InfermaticAI API 密钥",
322 "InfermaticAI Model": "InfermaticAI 模型",325 "InfermaticAI Model": "InfermaticAI 模型",
323 "DreamGen API key": "DreamGen API 密钥",326 "DreamGen API key": "DreamGen API 密钥",
@@ -346,6 +349,7 @@
346 "Ollama Model": "Ollama 模型",349 "Ollama Model": "Ollama 模型",
347 "Download": "下载",350 "Download": "下载",
348 "Tabby API key": "Tabby API 密钥",351 "Tabby API key": "Tabby API 密钥",
352 "Tabby Model": "Tabby 模型",
349 "koboldcpp API key (optional)": "koboldcpp API 密钥(可选)",353 "koboldcpp API key (optional)": "koboldcpp API 密钥(可选)",
350 "Example: 127.0.0.1:5001": "示例:127.0.0.1:5001",354 "Example: 127.0.0.1:5001": "示例:127.0.0.1:5001",
351 "Authorize": "授权",355 "Authorize": "授权",
@@ -363,13 +367,14 @@
363 "This will show up as your saved preset.": "这将显示为您保存的预设。",367 "This will show up as your saved preset.": "这将显示为您保存的预设。",
364 "Proxy Server URL": "代理服务器 URL",368 "Proxy Server URL": "代理服务器 URL",
365 "Alternative server URL (leave empty to use the default value).": "备用服务器 URL(留空以使用默认值)。",369 "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 代理时遇到的问题提供支持",
368 "Doesn't work? Try adding": "不起作用?尝试在最后添加",370 "Doesn't work? Try adding": "不起作用?尝试在最后添加",
369 "at the end!": "!",371 "at the end!": "!",
370 "Proxy Password": "代理密码",372 "Proxy Password": "代理密码",
371 "Will be used as a password for the proxy instead of API key.": "将用作代理的密码,而不是 API 密钥。",373 "Will be used as a password for the proxy instead of API key.": "将用作代理的密码,而不是 API 密钥。",
372 "Peek a password": "查看密码",374 "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!": "如果您不同意,请不要继续!",
373 "OpenAI API key": "OpenAI API 密钥",378 "OpenAI API key": "OpenAI API 密钥",
374 "View API Usage Metrics": "查看API使用情况",379 "View API Usage Metrics": "查看API使用情况",
375 "Follow": "跟随",380 "Follow": "跟随",
@@ -384,14 +389,14 @@
384 "Slack and Poe cookies will not work here, do not bother trying.": "Slack和Poe的cookie在这里不起作用,请不要尝试。",389 "Slack and Poe cookies will not work here, do not bother trying.": "Slack和Poe的cookie在这里不起作用,请不要尝试。",
385 "Claude Model": "Claude 模型",390 "Claude Model": "Claude 模型",
386 "Window AI Model": "Window AI 模型",391 "Window AI Model": "Window AI 模型",
392 "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。",
393 "Allow fallback models": "允许后备模型",
387 "Model Order": "OpenRouter 模型顺序",394 "Model Order": "OpenRouter 模型顺序",
388 "Alphabetically": "按字母顺序",395 "Alphabetically": "按字母顺序",
389 "Price": "价格(最便宜)",396 "Price": "价格(最便宜)",
390 "Context Size": "上下文大小",397 "Context Size": "上下文大小",
391 "Group by vendors": "按供应商分组",398 "Group by vendors": "按供应商分组",
392 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",399 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",
393 "Allow fallback routes": "允许后备方案",
394 "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。",
395 "openrouter_force_instruct": "此选项已过时,将来会被删除。要使用指令格式,请改用文本完成 API 下的 OpenRouter。",400 "openrouter_force_instruct": "此选项已过时,将来会被删除。要使用指令格式,请改用文本完成 API 下的 OpenRouter。",
396 "LEGACY": "旧版",401 "LEGACY": "旧版",
397 "Force Instruct Mode formatting": "强制指令模式格式化",402 "Force Instruct Mode formatting": "强制指令模式格式化",
@@ -439,9 +444,11 @@
439 "Example Separator": "示例分隔符",444 "Example Separator": "示例分隔符",
440 "Chat Start": "聊天开始",445 "Chat Start": "聊天开始",
441 "Add Chat Start and Example Separator to a list of stopping strings.": "将聊天开始和示例分隔符添加到停止字符串列表中。",446 "Add Chat Start and Example Separator to a list of stopping strings.": "将聊天开始和示例分隔符添加到停止字符串列表中。",
442 "Use as Stop Strings": "用作停止字符串",447 "Separators as Stop Strings": "分隔符作为终止字符串",
443 "context_allow_jailbreak": "如果在角色卡中定义并且启用了“首选角色越狱”,则在提示词末尾包含越狱。\n不建议在文本完成模型中使用此功能,否则会导致输出错误。",448 "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",
444 "Allow Jailbreak": "允许越狱",449 "Names as Stop Strings": "名称作为终止字符串",
450 "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。",
451 "Allow Post-History Instructions": "允许后历史说明",
445 "Context Order": "上下文顺序",452 "Context Order": "上下文顺序",
446 "Summary": "总结",453 "Summary": "总结",
447 "Author's Note": "作者注释",454 "Author's Note": "作者注释",
@@ -487,6 +494,10 @@
487 "First Assistant Prefix": "第一个助理前缀",494 "First Assistant Prefix": "第一个助理前缀",
488 "instruct_last_output_sequence": "插入到最后一条助手消息之前或作为生成 AI 回复时的最后一行提示词(中立/系统角色除外)。",495 "instruct_last_output_sequence": "插入到最后一条助手消息之前或作为生成 AI 回复时的最后一行提示词(中立/系统角色除外)。",
489 "Last Assistant Prefix": "最后一个助理前缀",496 "Last Assistant Prefix": "最后一个助理前缀",
497 "Inserted before the first User's message.": "插入在第一个用户的消息之前。",
498 "First User Prefix": "第一个用户前缀",
499 "instruct_last_input_sequence": "插入到最后一条用户消息之前。",
500 "Last User Prefix": "上次用户前缀",
490 "Will be inserted as a last prompt line when using system/neutral generation.": "当使用系统/中性生成时将作为最后的一行提示词插入。",501 "Will be inserted as a last prompt line when using system/neutral generation.": "当使用系统/中性生成时将作为最后的一行提示词插入。",
491 "System Instruction Prefix": "系统指令前缀",502 "System Instruction Prefix": "系统指令前缀",
492 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "如果生成了停止序列,则该序列之后的所有内容都将从输出中删除(包括在内)。",503 "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "如果生成了停止序列,则该序列之后的所有内容都将从输出中删除(包括在内)。",
@@ -534,6 +545,7 @@
534 "Sorted Evenly": "均匀排序",545 "Sorted Evenly": "均匀排序",
535 "Character Lore First": "角色世界书优先",546 "Character Lore First": "角色世界书优先",
536 "Global Lore First": "全局世界书优先",547 "Global Lore First": "全局世界书优先",
548 "Include names with each message into the context for scanning": "将每条消息的名称纳入上下文中以供扫描",
537 "Entries can activate other entries by mentioning their keywords": "条目可以通过提及它们的关键字来激活其他条目",549 "Entries can activate other entries by mentioning their keywords": "条目可以通过提及它们的关键字来激活其他条目",
538 "Recursive Scan": "递归扫描",550 "Recursive Scan": "递归扫描",
539 "Lookup for the entry keys in the context will respect the case": "在上下文中查找条目键将保持大小写敏感",551 "Lookup for the entry keys in the context will respect the case": "在上下文中查找条目键将保持大小写敏感",
@@ -552,6 +564,7 @@
552 "Close all Entries": "关闭所有条目",564 "Close all Entries": "关闭所有条目",
553 "New Entry": "新条目",565 "New Entry": "新条目",
554 "Fill empty Memo/Titles with Keywords": "使用关键字填充空的备忘录/标题",566 "Fill empty Memo/Titles with Keywords": "使用关键字填充空的备忘录/标题",
567 "Apply current sorting as Order": "应用当前排序作为顺序",
555 "Import World Info": "导入世界书",568 "Import World Info": "导入世界书",
556 "Export World Info": "导出世界书",569 "Export World Info": "导出世界书",
557 "Duplicate World Info": "复制世界书",570 "Duplicate World Info": "复制世界书",
@@ -659,14 +672,15 @@
659 "Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定义在导入卡片时应选择哪种操作来导入其列出的标签。“询问”将始终显示对话框。",672 "Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定义在导入卡片时应选择哪种操作来导入其列出的标签。“询问”将始终显示对话框。",
660 "Import Card Tags": "导入卡片标签",673 "Import Card Tags": "导入卡片标签",
661 "Ask": "询问",674 "Ask": "询问",
675 "tag_import_none": "无",
662 "tag_import_all": "全部",676 "tag_import_all": "全部",
663 "Existing": "现存的",677 "Existing": "现存的",
664 "Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊匹配,在列表中通过所有数据字段搜索角色,而不仅仅是名称子字符串",678 "Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊匹配,在列表中通过所有数据字段搜索角色,而不仅仅是名称子字符串",
665 "Advanced Character Search": "高级角色搜索",679 "Advanced Character Search": "高级角色搜索",
666 "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果角色卡包含提示词,则使用它替代系统提示词",680 "If checked and the character card contains a prompt override (System Prompt), use that instead": "如果角色卡包含提示词,则使用它替代系统提示词",
667 "Prefer Character Card Prompt": "角色卡提示词优先",681 "Prefer Character Card Prompt": "角色卡提示词优先",
668 "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead": "如果角色卡包含越狱(后置历史记录指令),则使用它替代系统越狱",682 "If checked and the character card contains a Post-History Instructions override, use that instead": "如果选中并且角色卡包含后历史指令覆盖,则使用它。",
669 "Prefer Character Card Jailbreak": "角色卡越狱优先",683 "Prefer Character Card Instructions": "首选角色卡说明",
670 "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和调整导入的角色图像的大小。关闭时,裁剪/调整大小为 512x768。",684 "Avoid cropping and resizing imported character images. When off, crop/resize to 512x768": "避免裁剪和调整导入的角色图像的大小。关闭时,裁剪/调整大小为 512x768。",
671 "Never resize avatars": "永不调整头像大小",685 "Never resize avatars": "永不调整头像大小",
672 "Show actual file names on the disk, in the characters list display only": "在角色列表显示中,显示磁盘上实际的文件名。",686 "Show actual file names on the disk, in the characters list display only": "在角色列表显示中,显示磁盘上实际的文件名。",
@@ -693,10 +707,10 @@
693 "Restore User Input": "恢复用户输入",707 "Restore User Input": "恢复用户输入",
694 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",708 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",
695 "Movable UI Panels": "可移动 UI 面板",709 "Movable UI Panels": "可移动 UI 面板",
710 "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
696 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",711 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",
697 "MUI Preset": "可移动 UI 预设",712 "MUI Preset": "可移动 UI 预设",
698 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",713 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",
699 "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
700 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",714 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",
701 "Custom CSS": "自定义 CSS",715 "Custom CSS": "自定义 CSS",
702 "Expand the editor": "展开编辑器",716 "Expand the editor": "展开编辑器",
@@ -716,6 +730,8 @@
716 "Press Send to continue": "按发送键以继续",730 "Press Send to continue": "按发送键以继续",
717 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",731 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",
718 "Quick 'Continue' button": "快速“继续”按钮",732 "Quick 'Continue' button": "快速“继续”按钮",
733 "Show a button in the input area to ask the AI to impersonate your character for a single message": "在输入区域中显示一个按钮,让 AI 模仿你的角色发送一条消息。",
734 "Quick 'Impersonate' button": "快速“模仿”按钮",
719 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",735 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",
720 "Swipes": "刷新回复按钮",736 "Swipes": "刷新回复按钮",
721 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",737 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",
@@ -739,6 +755,8 @@
739 "Log prompts to console": "将提示词记录到控制台",755 "Log prompts to console": "将提示词记录到控制台",
740 "Requests logprobs from the API for the Token Probabilities feature": "从API请求对数概率数据,用于实现词符概率功能。",756 "Requests logprobs from the API for the Token Probabilities feature": "从API请求对数概率数据,用于实现词符概率功能。",
741 "Request token probabilities": "请求词符概率",757 "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": "显示群聊队列",
742 "Automatically reject and re-generate AI message based on configurable criteria": "根据可配置的条件自动拒绝并重新生成AI消息",760 "Automatically reject and re-generate AI message based on configurable criteria": "根据可配置的条件自动拒绝并重新生成AI消息",
743 "Auto-swipe": "自动滑动",761 "Auto-swipe": "自动滑动",
744 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",762 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",
@@ -759,6 +777,10 @@
759 "Autocomplete Style": "风格",777 "Autocomplete Style": "风格",
760 "Follow Theme": "关注主题",778 "Follow Theme": "关注主题",
761 "Dark": "黑暗的",779 "Dark": "黑暗的",
780 "Keyboard": "键盘:",
781 "Select with Tab or Enter": "使用 Tab 或 Enter 选择",
782 "Select with Tab": "使用 Tab 选择",
783 "Select with Enter": "按 Enter 键选择",
762 "Sets the font size of the autocomplete.": "设置自动完成的字体大小。",784 "Sets the font size of the autocomplete.": "设置自动完成的字体大小。",
763 "Sets the width of the autocomplete.": "设置自动完成的宽度。",785 "Sets the width of the autocomplete.": "设置自动完成的宽度。",
764 "Autocomplete Width": "宽度",786 "Autocomplete Width": "宽度",
@@ -770,7 +792,7 @@
770 "Parser Flags": "解析器标志",792 "Parser Flags": "解析器标志",
771 "Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "切换到更严格的转义,允许所有分隔字符用反斜杠转义,并且反斜杠也可以转义。",793 "Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "切换到更严格的转义,允许所有分隔字符用反斜杠转义,并且反斜杠也可以转义。",
772 "STRICT_ESCAPING": "严格转义",794 "STRICT_ESCAPING": "严格转义",
773 "Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.": "用范围变量替换所有 {{getvar::}} 和 {{getglobalvar::}} 宏,以避免双重宏替换。",795 "stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} {{getglobalvar::}} 宏具有自动评估的文字宏类值。\n例如,“{{newline}}”保留为文字字符串“{{newline}}”\n\n(这是通过在内部用范围变量替换 {{getvar::}} {{getglobalvar::}} 宏来实现的。)",
774 "REPLACE_GETVAR": "替换GETVAR",796 "REPLACE_GETVAR": "替换GETVAR",
775 "Change Background Image": "更改背景图片",797 "Change Background Image": "更改背景图片",
776 "Filter": "搜索",798 "Filter": "搜索",
@@ -921,7 +943,7 @@
921 "Insert {{original}} into either box to include the respective default prompt from system settings.": "将{{original}}插入到任一框中,以包含系统设置中的相应默认提示词。",943 "Insert {{original}} into either box to include the respective default prompt from system settings.": "将{{original}}插入到任一框中,以包含系统设置中的相应默认提示词。",
922 "Main Prompt": "主要提示词",944 "Main Prompt": "主要提示词",
923 "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此处的任何内容都将替换用于此角色的默认主提示词。(v2规范:system_prompt)",945 "Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)": "此处的任何内容都将替换用于此角色的默认主提示词。(v2规范:system_prompt)",
924 "Any contents here will replace the default Jailbreak Prompt used for this character. (v2 spec: post_history_instructions)": "此处的任何内容都将替换用于此角色的默认越狱提示词。(v2规范:post_history_instructions)",946 "Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "此处的任何内容都将替换此角色使用的默认后历史说明。\n(v2 规范:post_history_instructions)",
925 "Creator's Metadata (Not sent with the AI prompt)": "创作者的元数据(不与AI提示词一起发送)",947 "Creator's Metadata (Not sent with the AI prompt)": "创作者的元数据(不与AI提示词一起发送)",
926 "Creator's Metadata": "创作者的元数据",948 "Creator's Metadata": "创作者的元数据",
927 "(Not sent with the AI Prompt)": "(不随 AI 提示词发送)",949 "(Not sent with the AI Prompt)": "(不随 AI 提示词发送)",
@@ -957,9 +979,6 @@
957 "Lock": "加锁",979 "Lock": "加锁",
958 "Unlock": "解锁",980 "Unlock": "解锁",
959 "Delete background": "删除背景",981 "Delete background": "删除背景",
960 "Chat Scenario Override": "聊天场景覆盖",
961 "Remove": "移除",
962 "Type here...": "在此处输入...",
963 "Chat Lorebook": "聊天知识书",982 "Chat Lorebook": "聊天知识书",
964 "Chat Lorebook for": "聊天知识书",983 "Chat Lorebook for": "聊天知识书",
965 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",984 "chat_world_template_txt": "选定的世界信息将绑定到此聊天。生成 AI 回复时,\n它将与全球和角色传说书中的条目相结合。",
@@ -1047,6 +1066,7 @@
1047 "Use Probability": "使用概率",1066 "Use Probability": "使用概率",
1048 "Add Memo": "添加备忘录",1067 "Add Memo": "添加备忘录",
1049 "Text or token ids": "文本或 [token ID]",1068 "Text or token ids": "文本或 [token ID]",
1069 "Type here...": "在此处输入...",
1050 "close": "关闭",1070 "close": "关闭",
1051 "prompt_manager_edit": "编辑",1071 "prompt_manager_edit": "编辑",
1052 "prompt_manager_name": "姓名",1072 "prompt_manager_name": "姓名",
@@ -1054,8 +1074,9 @@
1054 "To whom this message will be attributed.": "此消息应归于谁。",1074 "To whom this message will be attributed.": "此消息应归于谁。",
1055 "AI Assistant": "AI助手",1075 "AI Assistant": "AI助手",
1056 "prompt_manager_position": "位置",1076 "prompt_manager_position": "位置",
1057 "Injection position. Next to other prompts (relative) or in-chat (absolute).": "注入位置。其他提示词旁边(相对)或在聊天中(绝对)。",1077 "Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "注入位置。相对(相对于提示管理器中的其他提示)或在聊天中@深度。",
1058 "prompt_manager_relative": "相对",1078 "prompt_manager_relative": "相对",
1079 "prompt_manager_in_chat": "聊天中",
1059 "prompt_manager_depth": "深度",1080 "prompt_manager_depth": "深度",
1060 "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最后一条消息之后,1 = 在最后一条消息之前,等等。",1081 "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最后一条消息之后,1 = 在最后一条消息之前,等等。",
1061 "Prompt": "提示词",1082 "Prompt": "提示词",
@@ -1164,6 +1185,7 @@
1164 "Pause script execution": "暂停执行脚本",1185 "Pause script execution": "暂停执行脚本",
1165 "Abort script execution": "中止执行脚本",1186 "Abort script execution": "中止执行脚本",
1166 "Abort request": "中止请求",1187 "Abort request": "中止请求",
1188 "Ask AI to write your message for you": "让AI为您撰写消息",
1167 "Continue the last message": "继续上一条消息",1189 "Continue the last message": "继续上一条消息",
1168 "Send a message": "发送消息",1190 "Send a message": "发送消息",
1169 "Close chat": "关闭聊天",1191 "Close chat": "关闭聊天",
@@ -1175,19 +1197,24 @@
1175 "Manage chat files": "管理聊天文件",1197 "Manage chat files": "管理聊天文件",
1176 "Delete messages": "删除消息",1198 "Delete messages": "删除消息",
1177 "Regenerate": "重新生成",1199 "Regenerate": "重新生成",
1178 "Ask AI to write your message for you": "请求AI为您撰写消息",
1179 "Impersonate": "AI 帮答",1200 "Impersonate": "AI 帮答",
1180 "Continue": "继续",1201 "Continue": "继续",
1181 "Bind user name to that avatar": "将用户名称绑定到该头像",1202 "Bind user name to that avatar": "将用户名称绑定到该头像",
1182 "Change persona image": "更改用户角色头像",1203 "Change persona image": "更改用户角色头像",
1183 "Select this as default persona for the new chats.": "选择此项作为新聊天的默认用户角色。",1204 "Select this as default persona for the new chats.": "选择此项作为新聊天的默认用户角色。",
1205 "Duplicate persona": "复制用户角色",
1184 "Delete persona": "删除用户角色",1206 "Delete persona": "删除用户角色",
1185 "These characters are the winners of character design contests and have outstandable quality.": "这些角色都是角色设计大赛的获奖者,品质非常出色。",1207 "These characters are the winners of character design contests and have outstandable quality.": "这些角色都是角色设计大赛的获奖者,品质非常出色。",
1186 "Contest Winners": "比赛获胜者",1208 "Contest Winners": "比赛获胜者",
1187 "These characters are the finalists of character design contests and have remarkable quality.": "这些角色都是角色设计大赛的入围作品,品质十分出色。",1209 "These characters are the finalists of character design contests and have remarkable quality.": "这些角色都是角色设计大赛的入围作品,品质十分出色。",
1188 "Featured Characters": "特色角色",1210 "Featured Characters": "特色角色",
1189 "Download Extensions & Assets": "下载扩展和资源菜单",1211 "Download Extensions & Assets": "下载扩展和资源菜单",
1212 "Load a custom asset list or select": "加载自定义资产列表或选择",
1213 "to install 3rd party extensions.": "安装第三方扩展。",
1190 "Assets URL": "资产网址",1214 "Assets URL": "资产网址",
1215 "load_asset_list_desc": "根据资产列表文件加载扩展和资产列表。\n\n此字段中的默认资产 URL 指向官方第一方扩展和资产列表。\n如果您有自定义资产列表,可以在此处插入。\n\n要安装单个第三方扩展,请使用右上角的“安装扩展”按钮。",
1216 "Load an asset list": "加载资产列表",
1217 "Load Asset List": "加载资产列表",
1191 "Characters": "人物",1218 "Characters": "人物",
1192 "Attach a File": "附加文件",1219 "Attach a File": "附加文件",
1193 "Enter a URL or the ID of a Fandom wiki page to scrape:": "输入要抓取的 Fandom wiki 页面的 URL 或 ID:",1220 "Enter a URL or the ID of a Fandom wiki page to scrape:": "输入要抓取的 Fandom wiki 页面的 URL 或 ID:",
@@ -1271,6 +1298,7 @@
1271 "Put images with expressions there. File names should follow the pattern:": "将带有表情的图像放在那里。文件名应遵循以下模式:",1298 "Put images with expressions there. File names should follow the pattern:": "将带有表情的图像放在那里。文件名应遵循以下模式:",
1272 "expression_label_pattern": "[表达式标签].[图像格式]",1299 "expression_label_pattern": "[表达式标签].[图像格式]",
1273 "Sprite set:": "表情集:",1300 "Sprite set:": "表情集:",
1301 "Show Gallery": "展示图库",
1274 "ext_sum_title": "总结",1302 "ext_sum_title": "总结",
1275 "ext_sum_with": "总结如下:",1303 "ext_sum_with": "总结如下:",
1276 "ext_sum_main_api": "主要 API",1304 "ext_sum_main_api": "主要 API",
@@ -1315,9 +1343,14 @@
1315 "ext_sum_injection_template": "插入模板",1343 "ext_sum_injection_template": "插入模板",
1316 "ext_sum_memory_template_placeholder": "{{summary}} 将解析当前摘要内容。",1344 "ext_sum_memory_template_placeholder": "{{summary}} 将解析当前摘要内容。",
1317 "ext_sum_injection_position": "插入位置",1345 "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}} 宏访问它。",
1318 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",1350 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",
1319 "Labels and Message": "标签和信息",1351 "Labels and Message": "标签和信息",
1320 "Label": "标签",1352 "Label": "标签",
1353 "(label of the button, if no icon is chosen) ": "(如果没有选择图标,则为按钮的标签)",
1321 "Title": "标题",1354 "Title": "标题",
1322 "(tooltip, leave empty to show message or /command)": "(工具提示,留空以显示消息或/命令)",1355 "(tooltip, leave empty to show message or /command)": "(工具提示,留空以显示消息或/命令)",
1323 "Message / Command:": "消息/命令:",1356 "Message / Command:": "消息/命令:",
@@ -1348,6 +1381,8 @@
1348 "Inject user input automatically": "自动注入用户输入",1381 "Inject user input automatically": "自动注入用户输入",
1349 "(if disabled, use ": "(如果禁用,使用",1382 "(if disabled, use ": "(如果禁用,使用",
1350 "macro for manual injection)": "宏用于手动注入)",1383 "macro for manual injection)": "宏用于手动注入)",
1384 "Color": "颜色",
1385 "Only apply color as accent": "仅应用颜色作为强调",
1351 "ext_regex_title": "正则",1386 "ext_regex_title": "正则",
1352 "ext_regex_new_global_script_desc": "新的全局正则表达式脚本",1387 "ext_regex_new_global_script_desc": "新的全局正则表达式脚本",
1353 "ext_regex_new_global_script": "新建全局正则",1388 "ext_regex_new_global_script": "新建全局正则",
@@ -1361,7 +1396,7 @@
1361 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",1396 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
1362 "Regex Editor": "正则表达式编辑器",1397 "Regex Editor": "正则表达式编辑器",
1363 "Test Mode": "测试模式",1398 "Test Mode": "测试模式",
1364 "ext_regex_desc": "Regex 是一款使用正则表达式查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的 ?。",1399 "ext_regex_desc": "正则是一款使用正则表达式查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的 ?。",
1365 "Input": "输入",1400 "Input": "输入",
1366 "ext_regex_test_input_placeholder": "在此输入...",1401 "ext_regex_test_input_placeholder": "在此输入...",
1367 "Output": "输出",1402 "Output": "输出",
@@ -1396,6 +1431,7 @@
1396 "ext_regex_export_script": "导出脚本",1431 "ext_regex_export_script": "导出脚本",
1397 "ext_regex_delete_script": "删除脚本",1432 "ext_regex_delete_script": "删除脚本",
1398 "Trigger Stable Diffusion": "触发Stable Diffusion",1433 "Trigger Stable Diffusion": "触发Stable Diffusion",
1434 "Abort current image generation task": "中止当前图像生成",
1399 "sd_Yourself": "你自己",1435 "sd_Yourself": "你自己",
1400 "sd_Your_Face": "你的脸",1436 "sd_Your_Face": "你的脸",
1401 "sd_Me": "我",1437 "sd_Me": "我",
@@ -1445,6 +1481,10 @@
1445 "Delete workflow": "删除工作流",1481 "Delete workflow": "删除工作流",
1446 "Enhance": "提高",1482 "Enhance": "提高",
1447 "Refine": "优化",1483 "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": "风格预设",
1448 "Sampling method": "采样方法",1488 "Sampling method": "采样方法",
1449 "Scheduler": "调度器",1489 "Scheduler": "调度器",
1450 "Resolution": "分辨率",1490 "Resolution": "分辨率",
@@ -1545,6 +1585,10 @@
1545 "Only used when Main API is selected.": "仅在选择主 API 时使用。",1585 "Only used when Main API is selected.": "仅在选择主 API 时使用。",
1546 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",1586 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",
1547 "View Stats": "查看统计数据",1587 "View Stats": "查看统计数据",
1588 "Title/Memo": "标题/备忘录",
1589 "Status": "状态",
1590 "Position": "位置",
1591 "Trigger %": "触发率 %",
1548 "Manager Users": "管理用户",1592 "Manager Users": "管理用户",
1549 "New User": "新用户",1593 "New User": "新用户",
1550 "Status:": "地位:",1594 "Status:": "地位:",
@@ -1564,12 +1608,10 @@
1564 "New Tags": "新标签",1608 "New Tags": "新标签",
1565 "Folder Tags": "文件夹标签",1609 "Folder Tags": "文件夹标签",
1566 "The following tags will be auto-imported based on the currently selected folders": "根据当前选定的文件夹将自动导入以下标签",1610 "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如果选择了“导入”选项,则全局设置将保留为“询问”。",
1569 "Import None": "不导入",1611 "Import None": "不导入",
1570 "Import All": "全部导入",1612 "Import All": "全部导入",
1571 "Import Existing": "导入现有",1613 "Import Existing": "导入现有",
1572 "Import tags button": "导入",1614 "Import": "导入",
1573 "Include Body Parameters": "包括主体参数",1615 "Include Body Parameters": "包括主体参数",
1574 "custom_include_body_desc": "聊天完成请求主体中要包含的参数(YAML 对象)\n\n示例:\n- top_k:20\n- repetition_penalty:1.1",1616 "custom_include_body_desc": "聊天完成请求主体中要包含的参数(YAML 对象)\n\n示例:\n- top_k:20\n- repetition_penalty:1.1",
1575 "Exclude Body Parameters": "排除主体参数",1617 "Exclude Body Parameters": "排除主体参数",
@@ -1671,6 +1713,9 @@
1671 "char_import_8": "RisuRealm 角色(直链)",1713 "char_import_8": "RisuRealm 角色(直链)",
1672 "Supports importing multiple characters.": "支持导入多个角色。",1714 "Supports importing multiple characters.": "支持导入多个角色。",
1673 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",1715 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
1716 "Show Raw Prompt": "显示原始提示",
1717 "Copy Prompt": "复制提示",
1718 "Show Prompt Differences": "显示提示差异",
1674 "System-wide Replacement Macros (in order of evaluation):": "系统范围的替换宏(按评估顺序):",1719 "System-wide Replacement Macros (in order of evaluation):": "系统范围的替换宏(按评估顺序):",
1675 "help_macros_1": "仅适用于斜线命令批处理。替换为上一个命令的返回结果。",1720 "help_macros_1": "仅适用于斜线命令批处理。替换为上一个命令的返回结果。",
1676 "help_macros_2": "仅插入一个换行符。",1721 "help_macros_2": "仅插入一个换行符。",
@@ -1687,6 +1732,7 @@
1687 "help_macros_13": "角色对话示例",1732 "help_macros_13": "角色对话示例",
1688 "help_macros_14": "未格式化的对话示例",1733 "help_macros_14": "未格式化的对话示例",
1689 "(only for Story String)": "(仅适用于故事字符串)",1734 "(only for Story String)": "(仅适用于故事字符串)",
1735 "help_macros_summary": "“Summarize”扩展生成的最新聊天摘要(如果有)。",
1690 "help_macros_15": "您当前的 Persona 用户名",1736 "help_macros_15": "您当前的 Persona 用户名",
1691 "help_macros_16": "角色的名字",1737 "help_macros_16": "角色的名字",
1692 "help_macros_17": "角色的版本号",1738 "help_macros_17": "角色的版本号",
@@ -1700,6 +1746,7 @@
1700 "help_macros_22": "上下文中包含的第一条消息的 ID。要求在当前会话中至少运行一次生成。",1746 "help_macros_22": "上下文中包含的第一条消息的 ID。要求在当前会话中至少运行一次生成。",
1701 "help_macros_23": "最后一条聊天消息中当前滑动的 ID(以 1 为基数)。如果最后一条消息是用户或提示隐藏的,则为空字符串。",1747 "help_macros_23": "最后一条聊天消息中当前滑动的 ID(以 1 为基数)。如果最后一条消息是用户或提示隐藏的,则为空字符串。",
1702 "help_macros_24": "最后一条聊天消息中的滑动次数。如果最后一条消息是用户隐藏或提示隐藏的,则为空字符串。",1748 "help_macros_24": "最后一条聊天消息中的滑动次数。如果最后一条消息是用户隐藏或提示隐藏的,则为空字符串。",
1749 "help_macros_reverse": "反转宏的内容。",
1703 "help_macros_25": "您可以在此处留言,宏将被替换为空白内容。AI 看不到。",1750 "help_macros_25": "您可以在此处留言,宏将被替换为空白内容。AI 看不到。",
1704 "help_macros_26": "当前时间",1751 "help_macros_26": "当前时间",
1705 "help_macros_27": "当前日期",1752 "help_macros_27": "当前日期",
@@ -1738,6 +1785,8 @@
1738 "help_macros_56": "指示系统指令前缀",1785 "help_macros_56": "指示系统指令前缀",
1739 "help_macros_57": "指示第一个用户消息填充器",1786 "help_macros_57": "指示第一个用户消息填充器",
1740 "help_macros_58": "指示停止顺序",1787 "help_macros_58": "指示停止顺序",
1788 "help_macros_first_user": "指示用户第一个输入序列",
1789 "help_macros_last_user": "指示用户最后输入序列",
1741 "Chat variables Macros:": "聊天变量宏:",1790 "Chat variables Macros:": "聊天变量宏:",
1742 "Local variables = unique to the current chat": "局部变量 = 当前聊天所独有",1791 "Local variables = unique to the current chat": "局部变量 = 当前聊天所独有",
1743 "Global variables = works in any chat for any character": "全局变量 = 适用于任何角色的任何聊天",1792 "Global variables = works in any chat for any character": "全局变量 = 适用于任何角色的任何聊天",
@@ -1769,10 +1818,22 @@
1769 "prompt_manager_tokens": "词符",1818 "prompt_manager_tokens": "词符",
1770 "Are you sure you want to reset your settings to factory defaults?": "您确定要将设置重置为出厂默认设置吗?",1819 "Are you sure you want to reset your settings to factory defaults?": "您确定要将设置重置为出厂默认设置吗?",
1771 "Don't forget to save a snapshot of your settings before proceeding.": "在继续之前,不要忘记保存您的设置快照。",1820 "Don't forget to save a snapshot of your settings before proceeding.": "在继续之前,不要忘记保存您的设置快照。",
1821 "Chat Scenario Override": "聊天场景覆盖",
1822 "Remove": "移除",
1772 "Settings Snapshots": "设置快照",1823 "Settings Snapshots": "设置快照",
1773 "Record a snapshot of your current settings.": "记录当前设置的快照。",1824 "Record a snapshot of your current settings.": "记录当前设置的快照。",
1774 "Make a Snapshot": "制作快照",1825 "Make a Snapshot": "制作快照",
1775 "Restore this snapshot": "恢复此快照",1826 "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 模式。每个模式用换行符分隔。",
1776 "Hi,": "嘿,",1837 "Hi,": "嘿,",
1777 "To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器",1838 "To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器",
1778 "set to true in the config.yaml file.": "在 config.yaml 文件中设置为 true。",1839 "set to true in the config.yaml file.": "在 config.yaml 文件中设置为 true。",
public/locales/zh-tw.json+2 -2
@@ -208,7 +208,7 @@
208 "JSON Schema": "JSON 結構",208 "JSON Schema": "JSON 結構",
209 "Type in the desired JSON schema": "輸入所需的 JSON 結構",209 "Type in the desired JSON schema": "輸入所需的 JSON 結構",
210 "Grammar String": "語法字串",210 "Grammar String": "語法字串",
211 "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 或 ENBF,取決於所使用的後端。如果您使用此功能,應該知道是哪一種",211 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取決於所使用的後端。如果您使用此功能,應該知道是哪一種",
212 "Top P & Min P": "Top P 和 Min P",212 "Top P & Min P": "Top P 和 Min P",
213 "Load default order": "載入預設順序",213 "Load default order": "載入預設順序",
214 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。",214 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。",
@@ -217,7 +217,7 @@
217 "Character Names Behavior": "角色人物名稱行為",217 "Character Names Behavior": "角色人物名稱行為",
218 "Helps the model to associate messages with characters.": "幫助模型將訊息與角色人物關聯起來。",218 "Helps the model to associate messages with characters.": "幫助模型將訊息與角色人物關聯起來。",
219 "None": "無",219 "None": "無",
220 "character_names_none": "除了團體和過去的玩家角色人物外。否則,請確保在提示中提供名字。",220 "character_names_default": "除了團體和過去的玩家角色人物外。否則,請確保在提示中提供名字。",
221 "Don't add character names.": "不要新增角色人物名稱",221 "Don't add character names.": "不要新增角色人物名稱",
222 "Completion": "補充",222 "Completion": "補充",
223 "character_names_completion": "字元限制:僅限拉丁字母數字和底線。不適用於所有來源,特別是:Claude、MistralAI、Google。",223 "character_names_completion": "字元限制:僅限拉丁字母數字和底線。不適用於所有來源,特別是:Claude、MistralAI、Google。",
public/script.js+236 -222
@@ -156,6 +156,7 @@ import {
156 ensureImageFormatSupported,156 ensureImageFormatSupported,
157 flashHighlight,157 flashHighlight,
158 isTrueBoolean,158 isTrueBoolean,
159 toggleDrawer,
159} from './scripts/utils.js';160} from './scripts/utils.js';
160import { debounce_timeout } from './scripts/constants.js';161import { debounce_timeout } from './scripts/constants.js';
161162
@@ -224,10 +225,10 @@ import {
224import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';225import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
225import { hideLoader, showLoader } from './scripts/loader.js';226import { hideLoader, showLoader } from './scripts/loader.js';
226import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';227import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
227import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js';228import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels } from './scripts/textgen-models.js';
228import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';229import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
229import { initPresetManager } from './scripts/preset-manager.js';230import { initPresetManager } from './scripts/preset-manager.js';
230import { MacrosParser, evaluateMacros } from './scripts/macros.js';231import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros.js';
231import { currentUser, setUserControls } from './scripts/user.js';232import { currentUser, setUserControls } from './scripts/user.js';
232import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';233import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
233import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';234import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
@@ -424,6 +425,8 @@ export const event_types = {
424 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',425 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
425 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',426 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
426 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',427 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',
428 OAI_PRESET_EXPORT_READY: 'oai_preset_export_ready',
429 OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready',
427 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',430 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
428 WORLDINFO_UPDATED: 'worldinfo_updated',431 WORLDINFO_UPDATED: 'worldinfo_updated',
429 CHARACTER_EDITED: 'character_edited',432 CHARACTER_EDITED: 'character_edited',
@@ -439,6 +442,7 @@ export const event_types = {
439 GROUP_CHAT_CREATED: 'group_chat_created',442 GROUP_CHAT_CREATED: 'group_chat_created',
440 GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',443 GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
441 GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',444 GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
445 GENERATE_AFTER_DATA: 'generate_after_data',
442 GROUP_MEMBER_DRAFTED: 'group_member_drafted',446 GROUP_MEMBER_DRAFTED: 'group_member_drafted',
443 WORLD_INFO_ACTIVATED: 'world_info_activated',447 WORLD_INFO_ACTIVATED: 'world_info_activated',
444 TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',448 TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
@@ -455,6 +459,7 @@ export const event_types = {
455 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',459 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
456 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',460 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
457 ONLINE_STATUS_CHANGED: 'online_status_changed',461 ONLINE_STATUS_CHANGED: 'online_status_changed',
462 IMAGE_SWIPED: 'image_swiped',
458};463};
459464
460export const eventSource = new EventEmitter();465export const eventSource = new EventEmitter();
@@ -570,6 +575,7 @@ export const system_message_types = {
570 * @enum {number} Extension prompt types575 * @enum {number} Extension prompt types
571 */576 */
572export const extension_prompt_types = {577export const extension_prompt_types = {
578 NONE: -1,
573 IN_PROMPT: 0,579 IN_PROMPT: 0,
574 IN_CHAT: 1,580 IN_CHAT: 1,
575 BEFORE_PROMPT: 2,581 BEFORE_PROMPT: 2,
@@ -909,6 +915,7 @@ async function firstLoadInit() {
909 await readSecretState();915 await readSecretState();
910 initLocales();916 initLocales();
911 initDefaultSlashCommands();917 initDefaultSlashCommands();
918 initTextGenModels();
912 await getSystemMessages();919 await getSystemMessages();
913 sendSystemMessage(system_message_types.WELCOME);920 sendSystemMessage(system_message_types.WELCOME);
914 await getSettings();921 await getSettings();
@@ -929,10 +936,17 @@ async function firstLoadInit() {
929 initCfg();936 initCfg();
930 initLogprobs();937 initLogprobs();
931 doDailyExtensionUpdatesCheck();938 doDailyExtensionUpdatesCheck();
932 hideLoader();939 await hideLoader();
940 await fixViewport();
933 await eventSource.emit(event_types.APP_READY);941 await eventSource.emit(event_types.APP_READY);
934}942}
935943
944async function fixViewport() {
945 document.body.style.position = 'absolute';
946 await delay(1);
947 document.body.style.position = '';
948}
949
936function cancelStatusCheck() {950function cancelStatusCheck() {
937 abortStatusCheck?.abort();951 abortStatusCheck?.abort();
938 abortStatusCheck = new AbortController();952 abortStatusCheck = new AbortController();
@@ -1712,16 +1726,24 @@ export async function replaceCurrentChat() {
1712}1726}
17131727
1714export function showMoreMessages() {1728export function showMoreMessages() {
1715 let messageId = Number($('#chat').children('.mes').first().attr('mesid'));1729 const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid');
1730 let messageId = Number(firstDisplayedMesId);
1716 let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER;1731 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
1718 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);1739 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
1719 const prevHeight = $('#chat').prop('scrollHeight');1740 const prevHeight = $('#chat').prop('scrollHeight');
17201741
1721 while (messageId > 0 && count > 0) {1742 while (messageId > 0 && count > 0) {
1743 let newMessageId = messageId - 1;
1744 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId });
1722 count--;1745 count--;
1723 messageId--;1746 messageId--;
1724 addOneMessage(chat[messageId], { insertBefore: messageId + 1, scroll: false, forceId: messageId });
1725 }1747 }
17261748
1727 if (messageId == 0) {1749 if (messageId == 0) {
@@ -2091,6 +2113,7 @@ export function updateMessageBlock(messageId, message) {
2091export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {2113export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
2092 // Add image to message2114 // Add image to message
2093 if (mes.extra?.image) {2115 if (mes.extra?.image) {
2116 const container = messageElement.find('.mes_img_container');
2094 const chatHeight = $('#chat').prop('scrollHeight');2117 const chatHeight = $('#chat').prop('scrollHeight');
2095 const image = messageElement.find('.mes_img');2118 const image = messageElement.find('.mes_img');
2096 const text = messageElement.find('.mes_text');2119 const text = messageElement.find('.mes_text');
@@ -2106,9 +2129,27 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
2106 });2129 });
2107 image.attr('src', mes.extra?.image);2130 image.attr('src', mes.extra?.image);
2108 image.attr('title', mes.extra?.title || mes.title || '');2131 image.attr('title', mes.extra?.title || mes.title || '');
2109 messageElement.find('.mes_img_container').addClass('img_extra');2132 container.addClass('img_extra');
2110 image.toggleClass('img_inline', isInline);2133 image.toggleClass('img_inline', isInline);
2111 text.toggleClass('displayNone', !isInline);2134 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 }
2112 }2153 }
21132154
2114 // Add file to message2155 // Add file to message
@@ -2462,26 +2503,30 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
2462 * @returns {string[]} Array of stopping strings2503 * @returns {string[]} Array of stopping strings
2463 */2504 */
2464export function getStoppingStrings(isImpersonate, isContinue) {2505export function getStoppingStrings(isImpersonate, isContinue) {
2465 const charString = `\n${name2}:`;2506 const result = [];
2466 const userString = `\n${name1}:`;
2467 const result = isImpersonate ? [charString] : [userString];
24682507
2469 result.push(userString);2508 if (power_user.context.names_as_stop_strings) {
2509 const charString = `\n${name2}:`;
2510 const userString = `\n${name1}:`;
2511 result.push(isImpersonate ? charString : userString);
24702512
2471 if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) {2513 result.push(userString);
2472 result.push(charString);
2473 }
24742514
2475 // Add other group members as the stopping strings2515 if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) {
2476 if (selected_group) {2516 result.push(charString);
2477 const group = groups.find(x => x.id === selected_group);2517 }
2518
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)
2520 if (selected_group && (name2 || isImpersonate)) {
2521 const group = groups.find(x => x.id === selected_group);
24782522
2479 if (group && Array.isArray(group.members)) {2523 if (group && Array.isArray(group.members)) {
2480 const names = group.members2524 const names = group.members
2481 .map(x => characters.find(y => y.avatar == x))2525 .map(x => characters.find(y => y.avatar == x))
2482 .filter(x => x && x.name && x.name !== name2)2526 .filter(x => x && x.name && x.name !== name2)
2483 .map(x => `\n${x.name}:`);2527 .map(x => `\n${x.name}:`);
2484 result.push(...names);2528 result.push(...names);
2529 }
2485 }2530 }
2486 }2531 }
24872532
@@ -2800,6 +2845,8 @@ class StreamingProcessor {
2800 this.messageDom = null;2845 this.messageDom = null;
2801 this.messageTextDom = null;2846 this.messageTextDom = null;
2802 this.messageTimerDom = null;2847 this.messageTimerDom = null;
2848 this.messageTokenCounterDom = null;
2849 /** @type {HTMLTextAreaElement} */
2803 this.sendTextarea = document.querySelector('#send_textarea');2850 this.sendTextarea = document.querySelector('#send_textarea');
2804 this.type = type;2851 this.type = type;
2805 this.force_name2 = force_name2;2852 this.force_name2 = force_name2;
@@ -2815,6 +2862,15 @@ class StreamingProcessor {
2815 this.messageLogprobs = [];2862 this.messageLogprobs = [];
2816 }2863 }
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
2818 showMessageButtons(messageId) {2874 showMessageButtons(messageId) {
2819 if (messageId == -1) {2875 if (messageId == -1) {
2820 return;2876 return;
@@ -2843,9 +2899,7 @@ class StreamingProcessor {
2843 else {2899 else {
2844 await saveReply(this.type, text, true);2900 await saveReply(this.type, text, true);
2845 messageId = chat.length - 1;2901 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');
2849 this.showMessageButtons(messageId);2903 this.showMessageButtons(messageId);
2850 }2904 }
28512905
@@ -2881,9 +2935,10 @@ class StreamingProcessor {
2881 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));2935 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
2882 }2936 }
2883 else {2937 else {
2884 let currentTime = new Date();2938 this.#checkDomElements(messageId);
2939 const currentTime = new Date();
2885 // Don't waste time calculating token count for streaming2940 // Don't waste time calculating token count for streaming
2886 let currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;2941 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
2887 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);2942 const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
2888 chat[messageId]['mes'] = processedText;2943 chat[messageId]['mes'] = processedText;
2889 chat[messageId]['gen_started'] = this.timeStarted;2944 chat[messageId]['gen_started'] = this.timeStarted;
@@ -2895,8 +2950,9 @@ class StreamingProcessor {
2895 }2950 }
28962951
2897 chat[messageId]['extra']['token_count'] = currentTokenCount;2952 chat[messageId]['extra']['token_count'] = currentTokenCount;
2898 const tokenCounter = $(`#chat .mes[mesid="${messageId}"] .tokenCounterDisplay`);2953 if (this.messageTokenCounterDom instanceof HTMLElement) {
2899 tokenCounter.text(`${currentTokenCount}t`);2954 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
2955 }
2900 }2956 }
29012957
2902 if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId]['swipes'])) {2958 if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId]['swipes'])) {
@@ -2904,16 +2960,20 @@ class StreamingProcessor {
2904 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'])) };2960 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'])) };
2905 }2961 }
29062962
2907 let formattedText = messageFormatting(2963 const formattedText = messageFormatting(
2908 processedText,2964 processedText,
2909 chat[messageId].name,2965 chat[messageId].name,
2910 chat[messageId].is_system,2966 chat[messageId].is_system,
2911 chat[messageId].is_user,2967 chat[messageId].is_user,
2912 messageId,2968 messageId,
2913 );2969 );
2914 this.messageTextDom.innerHTML = formattedText;2970 if (this.messageTextDom instanceof HTMLElement) {
2915 this.messageTimerDom.textContent = timePassed.timerValue;2971 this.messageTextDom.innerHTML = formattedText;
2916 this.messageTimerDom.title = timePassed.timerTitle;2972 }
2973 if (this.messageTimerDom instanceof HTMLElement) {
2974 this.messageTimerDom.textContent = timePassed.timerValue;
2975 this.messageTimerDom.title = timePassed.timerTitle;
2976 }
2917 this.setFirstSwipe(messageId);2977 this.setFirstSwipe(messageId);
2918 }2978 }
29192979
@@ -3200,6 +3260,23 @@ function restoreResponseLength(api, responseLength) {
3200}3260}
32013261
3202/**3262/**
3263 * Removes last message from the chat DOM.
3264 * @returns {Promise<void>} Resolves when the message is removed.
3265 */
3266function 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/**
3203 * Runs a generation using the current chat context.3280 * Runs a generation using the current chat context.
3204 * @param {string} type Generation type3281 * @param {string} type Generation type
3205 * @param {GenerateOptions} options Generation options3282 * @param {GenerateOptions} options Generation options
@@ -3331,9 +3408,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3331 }3408 }
3332 else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && chat.length) {3409 else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && chat.length) {
3333 chat.length = chat.length - 1;3410 chat.length = chat.length - 1;
3334 $('#chat').children().last().hide(250, function () {3411 await removeLastMessage();
3335 $(this).remove();
3336 });
3337 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);3412 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
3338 }3413 }
3339 }3414 }
@@ -3581,6 +3656,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3581 let chat2 = [];3656 let chat2 = [];
3582 let continue_mag = '';3657 let continue_mag = '';
3583 const userMessageIndices = [];3658 const userMessageIndices = [];
3659 const lastUserMessageIndex = coreChat.findLastIndex(x => x.is_user);
35843660
3585 for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) {3661 for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) {
3586 if (main_api == 'openai') {3662 if (main_api == 'openai') {
@@ -3599,6 +3675,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3599 chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST);3675 chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST);
3600 }3676 }
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
3602 // Do not suffix the message for continuation3683 // Do not suffix the message for continuation
3603 if (i === 0 && isContinue) {3684 if (i === 0 && isContinue) {
3604 if (isInstruct) {3685 if (isInstruct) {
@@ -3624,7 +3705,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3624 mes: power_user.instruct.user_alignment_message,3705 mes: power_user.instruct.user_alignment_message,
3625 is_user: true,3706 is_user: true,
3626 };3707 };
3627 userAlignmentMessage = formatMessageHistoryItem(alignmentMessage, isInstruct, false);3708 userAlignmentMessage = formatMessageHistoryItem(alignmentMessage, isInstruct, force_output_sequence.FIRST);
3628 }3709 }
36293710
3630 // Call combined AN into Generate3711 // Call combined AN into Generate
@@ -3886,7 +3967,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38863967
3887 // Get instruct mode line3968 // Get instruct mode line
3888 if (isInstruct && !isContinue) {3969 if (isInstruct && !isContinue) {
3889 const name = (quiet_prompt && !quietToLoud) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);3970 const name = (quiet_prompt && !quietToLoud && !isImpersonate) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
3890 const isQuiet = quiet_prompt && type == 'quiet';3971 const isQuiet = quiet_prompt && type == 'quiet';
3891 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);3972 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);
3892 }3973 }
@@ -4167,6 +4248,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4167 }4248 }
4168 }4249 }
41694250
4251 await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data);
4252
4170 if (dryRun) {4253 if (dryRun) {
4171 generatedPromptCache = '';4254 generatedPromptCache = '';
4172 return Promise.resolve();4255 return Promise.resolve();
@@ -4698,6 +4781,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
4698 await eventSource.emit(event_types.MESSAGE_SENT, chat_id);4781 await eventSource.emit(event_types.MESSAGE_SENT, chat_id);
4699 addOneMessage(message);4782 addOneMessage(message);
4700 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);4783 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
4784 await saveChatConditional();
4701 }4785 }
4702}4786}
47034787
@@ -5029,7 +5113,7 @@ function setInContextMessages(lastmsg, type) {
5029 * @param {object} data Generation data5113 * @param {object} data Generation data
5030 * @returns {Promise<object>} Response data from the API5114 * @returns {Promise<object>} Response data from the API
5031 */5115 */
5032async function sendGenerationRequest(type, data) {5116export async function sendGenerationRequest(type, data) {
5033 if (main_api === 'openai') {5117 if (main_api === 'openai') {
5034 return await sendOpenAIRequest(type, data.prompt, abortController.signal);5118 return await sendOpenAIRequest(type, data.prompt, abortController.signal);
5035 }5119 }
@@ -5061,7 +5145,7 @@ async function sendGenerationRequest(type, data) {
5061 * @param {object} data Generation data5145 * @param {object} data Generation data
5062 * @returns {Promise<any>} Streaming generator5146 * @returns {Promise<any>} Streaming generator
5063 */5147 */
5064async function sendStreamingRequest(type, data) {5148export async function sendStreamingRequest(type, data) {
5065 if (abortController?.signal?.aborted) {5149 if (abortController?.signal?.aborted) {
5066 throw new Error('Generation was aborted.');5150 throw new Error('Generation was aborted.');
5067 }5151 }
@@ -5342,9 +5426,11 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5342 getMessage = fixMarkdown(getMessage, false);5426 getMessage = fixMarkdown(getMessage, false);
5343 }5427 }
53445428
5345 const nameToTrim2 = isImpersonate ? name1 : name2;5429 const nameToTrim2 = isImpersonate
5430 ? (!power_user.allow_name1_display ? name1 : '')
5431 : (!power_user.allow_name2_display ? name2 : '');
53465432
5347 if (getMessage.startsWith(nameToTrim2 + ':')) {5433 if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) {
5348 getMessage = getMessage.replace(nameToTrim2 + ':', '');5434 getMessage = getMessage.replace(nameToTrim2 + ':', '');
5349 getMessage = getMessage.trimStart();5435 getMessage = getMessage.trimStart();
5350 }5436 }
@@ -5565,6 +5651,7 @@ export function activateSendButtons() {
5565 is_send_press = false;5651 is_send_press = false;
5566 $('#send_but').removeClass('displayNone');5652 $('#send_but').removeClass('displayNone');
5567 $('#mes_continue').removeClass('displayNone');5653 $('#mes_continue').removeClass('displayNone');
5654 $('#mes_impersonate').removeClass('displayNone');
5568 $('.mes_buttons:last').show();5655 $('.mes_buttons:last').show();
5569 hideStopButton();5656 hideStopButton();
5570}5657}
@@ -5572,6 +5659,7 @@ export function activateSendButtons() {
5572export function deactivateSendButtons() {5659export function deactivateSendButtons() {
5573 $('#send_but').addClass('displayNone');5660 $('#send_but').addClass('displayNone');
5574 $('#mes_continue').addClass('displayNone');5661 $('#mes_continue').addClass('displayNone');
5662 $('#mes_impersonate').addClass('displayNone');
5575 showStopButton();5663 showStopButton();
5576}5664}
55775665
@@ -6022,9 +6110,10 @@ async function getChatResult() {
6022 const message = getFirstMessage();6110 const message = getFirstMessage();
6023 if (message.mes) {6111 if (message.mes) {
6024 chat.push(message);6112 chat.push(message);
6025 await saveChatConditional();
6026 freshChat = true;6113 freshChat = true;
6027 }6114 }
6115 // Make sure the chat appears on the server
6116 await saveChatConditional();
6028 }6117 }
6029 await loadItemizedPrompts(getCurrentChatId());6118 await loadItemizedPrompts(getCurrentChatId());
6030 await printMessages();6119 await printMessages();
@@ -6076,7 +6165,7 @@ export async function openCharacterChat(file_name) {
6076 chat_metadata = {};6165 chat_metadata = {};
6077 await getChat();6166 await getChat();
6078 $('#selected_chat_pole').val(file_name);6167 $('#selected_chat_pole').val(file_name);
6079 await createOrEditCharacter();6168 await createOrEditCharacter(new CustomEvent('newChat'));
6080}6169}
60816170
6082////////// OPTIMZED MAIN API CHANGE FUNCTION ////////////6171////////// OPTIMZED MAIN API CHANGE FUNCTION ////////////
@@ -6361,7 +6450,7 @@ export async function getSettings() {
6361 loadHordeSettings(settings);6450 loadHordeSettings(settings);
63626451
6363 // Load power user settings6452 // Load power user settings
6364 loadPowerUserSettings(settings, data);6453 await loadPowerUserSettings(settings, data);
63656454
6366 // Load character tags6455 // Load character tags
6367 loadTagsSettings(settings);6456 loadTagsSettings(settings);
@@ -6778,6 +6867,11 @@ export async function displayPastChats() {
6778 const fileName = chat['file_name'];6867 const fileName = chat['file_name'];
6779 const chatContent = rawChats[fileName];6868 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
6781 // // Uncomment this to return to old behavior (classical full-substring search).6875 // // Uncomment this to return to old behavior (classical full-substring search).
6782 // return chatContent && Object.values(chatContent).some(message => message?.mes?.toLowerCase()?.includes(searchQuery.toLowerCase()));6876 // return chatContent && Object.values(chatContent).some(message => message?.mes?.toLowerCase()?.includes(searchQuery.toLowerCase()));
67836877
@@ -7866,6 +7960,8 @@ window['SillyTavern'].getContext = function () {
7866 eventTypes: event_types,7960 eventTypes: event_types,
7867 addOneMessage: addOneMessage,7961 addOneMessage: addOneMessage,
7868 generate: Generate,7962 generate: Generate,
7963 sendStreamingRequest: sendStreamingRequest,
7964 sendGenerationRequest: sendGenerationRequest,
7869 stopGeneration: stopGeneration,7965 stopGeneration: stopGeneration,
7870 getTokenCount: getTokenCount,7966 getTokenCount: getTokenCount,
7871 extensionPrompts: extension_prompts,7967 extensionPrompts: extension_prompts,
@@ -8259,6 +8355,7 @@ const swipe_right = () => {
8259};8355};
82608356
8261const CONNECT_API_MAP = {8357const CONNECT_API_MAP = {
8358 // Default APIs not contined inside text gen / chat gen
8262 'kobold': {8359 'kobold': {
8263 selected: 'kobold',8360 selected: 'kobold',
8264 button: '#api_button',8361 button: '#api_button',
@@ -8270,147 +8367,48 @@ const CONNECT_API_MAP = {
8270 selected: 'novel',8367 selected: 'novel',
8271 button: '#api_button_novel',8368 button: '#api_button_novel',
8272 },8369 },
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 },
8313 'kcpp': {8371 'kcpp': {
8314 selected: 'textgenerationwebui',8372 selected: 'textgenerationwebui',
8315 button: '#api_button_textgenerationwebui',8373 button: '#api_button_textgenerationwebui',
8316 type: textgen_types.KOBOLDCPP,8374 type: textgen_types.KOBOLDCPP,
8317 },8375 },
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 },
8328 'oai': {8377 'oai': {
8329 selected: 'openai',8378 selected: 'openai',
8330 button: '#api_button_openai',8379 button: '#api_button_openai',
8331 source: chat_completion_sources.OPENAI,8380 source: chat_completion_sources.OPENAI,
8332 },8381 },
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 },
8343 'openrouter': {8383 'openrouter': {
8344 selected: 'openai',8384 selected: 'openai',
8345 button: '#api_button_openai',8385 button: '#api_button_openai',
8346 source: chat_completion_sources.OPENROUTER,8386 source: chat_completion_sources.OPENROUTER,
8347 },8387 },
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 },
8403 'openrouter-text': {8388 'openrouter-text': {
8404 selected: 'textgenerationwebui',8389 selected: 'textgenerationwebui',
8405 button: '#api_button_textgenerationwebui',8390 button: '#api_button_textgenerationwebui',
8406 type: textgen_types.OPENROUTER,8391 type: textgen_types.OPENROUTER,
8407 },8392 },
8408 'huggingface': {8393};
8394
8395// Fill connections map from textgen_types and chat_completion_sources
8396for (const textGenType of Object.values(textgen_types)) {
8397 if (CONNECT_API_MAP[textGenType]) continue;
8398 CONNECT_API_MAP[textGenType] = {
8409 selected: 'textgenerationwebui',8399 selected: 'textgenerationwebui',
8410 button: '#api_button_textgenerationwebui',8400 button: '#api_button_textgenerationwebui',
8411 type: textgen_types.HUGGINGFACE,8401 type: textGenType,
8412 },8402 };
8413};8403}
8404for (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
8415async function selectContextCallback(_, name) {8413async function selectContextCallback(_, name) {
8416 if (!name) {8414 if (!name) {
@@ -8543,7 +8541,7 @@ export async function processDroppedFiles(files, data = new Map()) {
85438541
8544 for (const file of files) {8542 for (const file of files) {
8545 const extension = file.name.split('.').pop().toLowerCase();8543 const extension = file.name.split('.').pop().toLowerCase();
8546 if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) {8544 if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) {
8547 const preservedName = data instanceof Map && data.get(file);8545 const preservedName = data instanceof Map && data.get(file);
8548 await importCharacter(file, preservedName);8546 await importCharacter(file, preservedName);
8549 } else {8547 } else {
@@ -9142,14 +9140,14 @@ jQuery(async function () {
9142 $('#send_textarea').on('focusin focus click', () => {9140 $('#send_textarea').on('focusin focus click', () => {
9143 S_TAPreviouslyFocused = true;9141 S_TAPreviouslyFocused = true;
9144 });9142 });
9145 $('#send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => {9143 $('#send_but, #option_regenerate, #option_continue, #mes_continue, #mes_impersonate').on('click', () => {
9146 if (S_TAPreviouslyFocused) {9144 if (S_TAPreviouslyFocused) {
9147 $('#send_textarea').focus();9145 $('#send_textarea').focus();
9148 }9146 }
9149 });9147 });
9150 $(document).click(event => {9148 $(document).click(event => {
9151 if ($(':focus').attr('id') !== 'send_textarea') {9149 if ($(':focus').attr('id') !== 'send_textarea') {
9152 var validIDs = ['options_button', 'send_but', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];9150 var validIDs = ['options_button', 'send_but', 'mes_impersonate', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];
9153 if (!validIDs.includes($(event.target).attr('id'))) {9151 if (!validIDs.includes($(event.target).attr('id'))) {
9154 S_TAPreviouslyFocused = false;9152 S_TAPreviouslyFocused = false;
9155 }9153 }
@@ -9185,6 +9183,9 @@ jQuery(async function () {
9185 debouncedCharacterSearch(searchQuery);9183 debouncedCharacterSearch(searchQuery);
9186 });9184 });
91879185
9186 $('#mes_impersonate').on('click', function () {
9187 $('#option_impersonate').trigger('click');
9188 });
91889189
9189 $('#mes_continue').on('click', function () {9190 $('#mes_continue').on('click', function () {
9190 $('#option_continue').trigger('click');9191 $('#option_continue').trigger('click');
@@ -9806,8 +9807,8 @@ jQuery(async function () {
9806 hideMenu();9807 hideMenu();
9807 });9808 });
98089809
9809 $('#newChatFromManageScreenButton').on('click', function () {9810 $('#newChatFromManageScreenButton').on('click', async function () {
9810 doNewChat({ deleteCurrentChat: false });9811 await doNewChat({ deleteCurrentChat: false });
9811 $('#select_chat_cross').trigger('click');9812 $('#select_chat_cross').trigger('click');
9812 });9813 });
98139814
@@ -10482,8 +10483,9 @@ jQuery(async function () {
10482 }10483 }
1048310484
10484 // Set the height of "autoSetHeight" textareas within the drawer to their scroll height10485 // Set the height of "autoSetHeight" textareas within the drawer to their scroll height
10485 $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(function () {10486 $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {
10486 resetScrollHeight($(this));10487 await resetScrollHeight($(this));
10488 return;
10487 });10489 });
1048810490
10489 } else if (drawerWasOpenAlready) { //to close manually10491 } else if (drawerWasOpenAlready) { //to close manually
@@ -10556,8 +10558,9 @@ jQuery(async function () {
10556 $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle();10558 $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle();
1055710559
10558 // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height10560 // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height
10559 $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(function () {10561 $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(async function () {
10560 resetScrollHeight($(this));10562 await resetScrollHeight($(this));
10563 return;
10561 });10564 });
10562 });10565 });
1056310566
@@ -10638,12 +10641,19 @@ jQuery(async function () {
10638 }10641 }
10639 });10642 });
1064010643
10641 $(document).on('click', '#OpenAllWIEntries', function () {10644 document.addEventListener('click', function (e) {
10642 $('#world_popup_entries_list').children().find('.down').click();10645 if (!(e.target instanceof HTMLElement)) return;
10643 });10646 if (e.target.matches('#OpenAllWIEntries')) {
10644 $(document).on('click', '#CloseAllWIEntries', function () {10647 document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10645 $('#world_popup_entries_list').children().find('.up').click();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);
10653 });
10654 }
10646 });10655 });
10656
10647 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);10657 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
10648 /* $('#set_character_world').on('click', openCharacterWorldPopup); */10658 /* $('#set_character_world').on('click', openCharacterWorldPopup); */
1064910659
@@ -10738,7 +10748,7 @@ jQuery(async function () {
10738 }10748 }
10739 } break;10749 } break;
10740 case 'import_tags': {10750 case 'import_tags': {
10741 await importTags(characters[this_chid], { forceShow: true });10751 await importTags(characters[this_chid], { importSetting: tag_import_setting.ASK });
10742 } break;10752 } break;
10743 /*case 'delete_button':10753 /*case 'delete_button':
10744 popup_type = "del_ch";10754 popup_type = "del_ch";
@@ -10767,62 +10777,66 @@ jQuery(async function () {
10767 var isManualInput = false;10777 var isManualInput = false;
10768 var valueBeforeManualInput;10778 var valueBeforeManualInput;
1076910779
10770 $('.range-block-counter input, .neo-range-input').on('click', function () {10780 $(document).on('input', '.range-block-counter input, .neo-range-input', function () {
10771 valueBeforeManualInput = $(this).val();10781 valueBeforeManualInput = $(this).val();
10772 console.log(valueBeforeManualInput);10782 console.log(valueBeforeManualInput);
10773 })10783 });
10774 .on('change', function (e) {10784
10775 e.target.focus();10785 $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {
10776 e.target.dispatchEvent(new Event('keyup'));10786 e.target.focus();
10777 })10787 e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
10778 .on('keydown', function (e) {10788 });
10779 const masterSelector = '#' + $(this).data('for');10789
10780 const masterElement = $(masterSelector);10790 $(document).on('keydown', '.range-block-counter input, .neo-range-input', function (e) {
10781 if (e.key === 'Enter') {10791 const masterSelector = '#' + $(this).data('for');
10782 let manualInput = Number($(this).val());10792 const masterElement = $(masterSelector);
10783 if (isManualInput) {10793 if (e.key === 'Enter') {
10784 //disallow manual inputs outside acceptable range
10785 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10786 //if value is ok, assign to slider and update handle text and position
10787 //newSlider.val(manualInput)
10788 //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
10789 valueBeforeManualInput = manualInput;
10790 $(masterElement).val($(this).val()).trigger('input');
10791 } else {
10792 //if value not ok, warn and reset to last known valid value
10793 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10794 console.log(valueBeforeManualInput);
10795 //newSlider.val(valueBeforeManualInput)
10796 $(this).val(valueBeforeManualInput);
10797 }
10798 }
10799 }
10800 })
10801 .on('keyup', function () {
10802 valueBeforeManualInput = $(this).val();
10803 console.log(valueBeforeManualInput);
10804 isManualInput = true;
10805 })
10806 //trigger slider changes when user clicks away
10807 .on('mouseup blur', function () {
10808 const masterSelector = '#' + $(this).data('for');
10809 const masterElement = $(masterSelector);
10810 let manualInput = Number($(this).val());10794 let manualInput = Number($(this).val());
10811 if (isManualInput) {10795 if (isManualInput) {
10812 //if value is between correct range for the slider10796 //disallow manual inputs outside acceptable range
10813 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {10797 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10798 //if value is ok, assign to slider and update handle text and position
10799 //newSlider.val(manualInput)
10800 //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
10814 valueBeforeManualInput = manualInput;10801 valueBeforeManualInput = manualInput;
10815 //set the slider value to input value10802 $(masterElement).val($(this).val()).trigger('input', { forced: true });
10816 $(masterElement).val($(this).val()).trigger('input');
10817 } else {10803 } else {
10818 //if value not ok, warn and reset to last known valid value10804 //if value not ok, warn and reset to last known valid value
10819 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);10805 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10820 console.log(valueBeforeManualInput);10806 console.log(valueBeforeManualInput);
10807 //newSlider.val(valueBeforeManualInput)
10821 $(this).val(valueBeforeManualInput);10808 $(this).val(valueBeforeManualInput);
10822 }10809 }
10823 }10810 }
10824 isManualInput = false;10811 }
10825 });10812 });
10813
10814 $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {
10815 valueBeforeManualInput = $(this).val();
10816 console.log(valueBeforeManualInput);
10817 isManualInput = true;
10818 });
10819
10820 //trigger slider changes when user clicks away
10821 $(document).on('mouseup blur', '.range-block-counter input, .neo-range-input', function () {
10822 const masterSelector = '#' + $(this).data('for');
10823 const masterElement = $(masterSelector);
10824 let manualInput = Number($(this).val());
10825 if (isManualInput) {
10826 //if value is between correct range for the slider
10827 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10828 valueBeforeManualInput = manualInput;
10829 //set the slider value to input value
10830 $(masterElement).val($(this).val()).trigger('input', { forced: true });
10831 } else {
10832 //if value not ok, warn and reset to last known valid value
10833 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10834 console.log(valueBeforeManualInput);
10835 $(this).val(valueBeforeManualInput);
10836 }
10837 }
10838 isManualInput = false;
10839 });
1082610840
10827 $('.user_stats_button').on('click', function () {10841 $('.user_stats_button').on('click', function () {
10828 userStatsHandler();10842 userStatsHandler();
public/scripts/BulkEditOverlay.js+34 -4
@@ -18,7 +18,7 @@ import {
18import { favsToHotswap } from './RossAscends-mods.js';18import { favsToHotswap } from './RossAscends-mods.js';
19import { hideLoader, showLoader } from './loader.js';19import { hideLoader, showLoader } from './loader.js';
20import { convertCharacterToPersona } from './personas.js';20import { convertCharacterToPersona } from './personas.js';
21import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap } from './tags.js';21import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
2222
23/**23/**
24 * Static object representing the actions of the24 * Static object representing the actions of the
@@ -197,10 +197,10 @@ class BulkTagPopupHandler {
197 #getHtml = () => {197 #getHtml = () => {
198 const characterData = JSON.stringify({ characterIds: this.characterIds });198 const characterData = JSON.stringify({ characterIds: this.characterIds });
199 return `<div id="bulk_tag_shadow_popup">199 return `<div id="bulk_tag_shadow_popup">
200 <div id="bulk_tag_popup">200 <div id="bulk_tag_popup" class="wider_dialogue_popup">
201 <div id="bulk_tag_popup_holder">201 <div id="bulk_tag_popup_holder">
202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters.</small>203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters. Import all or existing tags for all selected characters.</small>
204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
205 <br>205 <br>
206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
@@ -219,6 +219,12 @@ class BulkTagPopupHandler {
219 <i class="fa-solid fa-trash-can margin-right-10px"></i>219 <i class="fa-solid fa-trash-can margin-right-10px"></i>
220 Mutual220 Mutual
221 </div>221 </div>
222 <div id="bulk_tag_popup_import_all_tags" class="menu_button" title="Import all tags from selected characters" data-i18n="[title]Import all tags from selected characters">
223 Import All
224 </div>
225 <div id="bulk_tag_popup_import_existing_tags" class="menu_button" title="Import existing tags from selected characters" data-i18n="[title]Import existing tags from selected characters">
226 Import Existing
227 </div>
222 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>228 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
223 </div>229 </div>
224 </div>230 </div>
@@ -254,6 +260,30 @@ class BulkTagPopupHandler {
254 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));260 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
255 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));261 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
256 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));262 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
263 document.querySelector('#bulk_tag_popup_import_all_tags').addEventListener('click', this.importAllTags.bind(this));
264 document.querySelector('#bulk_tag_popup_import_existing_tags').addEventListener('click', this.importExistingTags.bind(this));
265 }
266
267 /**
268 * Import existing tags for all selected characters
269 */
270 async importExistingTags() {
271 for (const characterId of this.characterIds) {
272 await importTags(characters[characterId], { importSetting: tag_import_setting.ONLY_EXISTING });
273 }
274
275 $('#bulkTagList').empty();
276 }
277
278 /**
279 * Import all tags for all selected characters
280 */
281 async importAllTags() {
282 for (const characterId of this.characterIds) {
283 await importTags(characters[characterId], { importSetting: tag_import_setting.ALL });
284 }
285
286 $('#bulkTagList').empty();
257 }287 }
258288
259 /**289 /**
@@ -570,7 +600,7 @@ class BulkEditOverlay {
570 this.container.removeEventListener('mouseup', cancelHold);600 this.container.removeEventListener('mouseup', cancelHold);
571 this.container.removeEventListener('touchend', cancelHold);601 this.container.removeEventListener('touchend', cancelHold);
572 },602 },
573 BulkEditOverlay.longPressDelay);603 BulkEditOverlay.longPressDelay);
574 };604 };
575605
576 handleLongPressEnd = (event) => {606 handleLongPressEnd = (event) => {
public/scripts/RossAscends-mods.js+12 -12
@@ -157,18 +157,15 @@ export function shouldSendOnEnter() {
157//Does not break old characters/chats, as the code just uses whatever timestamp exists in the chat.157//Does not break old characters/chats, as the code just uses whatever timestamp exists in the chat.
158//New chats made with characters will use this new formatting.158//New chats made with characters will use this new formatting.
159export function humanizedDateTime() {159export function humanizedDateTime() {
160 let baseDate = new Date(Date.now());160 const now = 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;
172}169}
173170
174//this is a common format version to display a timestamp on each chat message171//this is a common format version to display a timestamp on each chat message
@@ -314,6 +311,7 @@ function RA_checkOnlineStatus() {
314 $('#send_form').addClass('no-connection'); //entire input form area is red when not connected311 $('#send_form').addClass('no-connection'); //entire input form area is red when not connected
315 $('#send_but').addClass('displayNone'); //send button is hidden when not connected;312 $('#send_but').addClass('displayNone'); //send button is hidden when not connected;
316 $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;313 $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
314 $('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected;
317 $('#API-status-top').removeClass('fa-plug');315 $('#API-status-top').removeClass('fa-plug');
318 $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow');316 $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow');
319 connection_made = false;317 connection_made = false;
@@ -330,6 +328,7 @@ function RA_checkOnlineStatus() {
330 if (!is_send_press && !(selected_group && is_group_generating)) {328 if (!is_send_press && !(selected_group && is_group_generating)) {
331 $('#send_but').removeClass('displayNone'); //on connect, send button shows329 $('#send_but').removeClass('displayNone'); //on connect, send button shows
332 $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected330 $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected
331 $('#mes_impersonate').removeClass('displayNone'); //continue button is shown when connected
333 }332 }
334 }333 }
335 }334 }
@@ -381,6 +380,7 @@ function RA_autoconnect(PrevApi) {
381 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)380 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
382 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)381 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
383 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)382 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
383 || (secret_state[SECRET_KEYS.BLOCKENTROPY] && oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY)
384 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)384 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
385 ) {385 ) {
386 $('#api_button_openai').trigger('click');386 $('#api_button_openai').trigger('click');
public/scripts/autocomplete/AutoComplete.js+71 -48
@@ -16,8 +16,15 @@ export const AUTOCOMPLETE_WIDTH = {
16 'FULL': 2,16 'FULL': 2,
17};17};
1818
19/**@readonly*/
20/**@enum {Number}*/
21export const AUTOCOMPLETE_SELECT_KEY = {
22 'TAB': 1, // 2^0
23 'ENTER': 2, // 2^1
24};
25
19export class AutoComplete {26export class AutoComplete {
20 /**@type {HTMLTextAreaElement}*/ textarea;27 /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
21 /**@type {boolean}*/ isFloating = false;28 /**@type {boolean}*/ isFloating = false;
22 /**@type {()=>boolean}*/ checkIfActivate;29 /**@type {()=>boolean}*/ checkIfActivate;
23 /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;30 /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
@@ -56,6 +63,8 @@ export class AutoComplete {
56 /**@type {function}*/ updateDetailsPositionDebounced;63 /**@type {function}*/ updateDetailsPositionDebounced;
57 /**@type {function}*/ updateFloatingPositionDebounced;64 /**@type {function}*/ updateFloatingPositionDebounced;
5865
66 /**@type {(item:AutoCompleteOption)=>any}*/ onSelect;
67
59 get matchType() {68 get matchType() {
60 return power_user.stscript.matching ?? 'fuzzy';69 return power_user.stscript.matching ?? 'fuzzy';
61 }70 }
@@ -68,7 +77,7 @@ export class AutoComplete {
6877
6978
70 /**79 /**
71 * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete.80 * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete.
72 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')81 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
73 * @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.82 * @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.
74 * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.83 * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
@@ -102,10 +111,15 @@ export class AutoComplete {
102 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);111 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
103 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);112 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
104113
105 textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced));114 textarea.addEventListener('input', ()=>{
115 this.selectionStart = this.textarea.selectionStart;
116 if (this.text != this.textarea.value) this.show(true, this.wasForced);
117 });
106 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));118 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
107 textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);119 textarea.addEventListener('click', ()=>{
108 textarea.addEventListener('selectionchange', ()=>this.show());120 this.selectionStart = this.textarea.selectionStart;
121 if (this.isActive) this.show();
122 });
109 textarea.addEventListener('blur', ()=>this.hide());123 textarea.addEventListener('blur', ()=>this.hide());
110 if (isFloating) {124 if (isFloating) {
111 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());125 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
@@ -189,6 +203,11 @@ export class AutoComplete {
189 * @returns The option.203 * @returns The option.
190 */204 */
191 fuzzyScore(option) {205 fuzzyScore(option) {
206 // might have been matched by the options matchProvider function instead
207 if (!this.fuzzyRegex.test(option.name)) {
208 option.score = new AutoCompleteFuzzyScore(Number.MAX_SAFE_INTEGER, -1);
209 return option;
210 }
192 const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);211 const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
193 let start = null;212 let start = null;
194 let consecutive = [];213 let consecutive = [];
@@ -339,7 +358,7 @@ export class AutoComplete {
339358
340 this.result = this.effectiveParserResult.optionList359 this.result = this.effectiveParserResult.optionList
341 // filter the list of options by the partial name according to the matching type360 // filter the list of options by the partial name according to the matching type
342 .filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)361 .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)
343 // remove aliases362 // remove aliases
344 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);363 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
345364
@@ -357,10 +376,11 @@ export class AutoComplete {
357 // build element376 // build element
358 option.dom = this.makeItem(option);377 option.dom = this.makeItem(option);
359 // update replacer and add quotes if necessary378 // update replacer and add quotes if necessary
379 const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name;
360 if (this.effectiveParserResult.canBeQuoted) {380 if (this.effectiveParserResult.canBeQuoted) {
361 option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;381 option.replacer = optionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionName.replace(/"/g, '\\"')}"` : `${optionName}`;
362 } else {382 } else {
363 option.replacer = option.name;383 option.replacer = optionName;
364 }384 }
365 // calculate fuzzy score if matching is fuzzy385 // calculate fuzzy score if matching is fuzzy
366 if (this.matchType == 'fuzzy') this.fuzzyScore(option);386 if (this.matchType == 'fuzzy') this.fuzzyScore(option);
@@ -399,7 +419,7 @@ export class AutoComplete {
399 ,419 ,
400 );420 );
401 this.result.push(option);421 this.result.push(option);
402 } else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) {422 } else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) {
403 // only one result that is exactly the current value? just show hint, no autocomplete423 // only one result that is exactly the current value? just show hint, no autocomplete
404 this.isReplaceable = false;424 this.isReplaceable = false;
405 this.isShowingDetails = false;425 this.isShowingDetails = false;
@@ -439,11 +459,14 @@ export class AutoComplete {
439 } else {459 } else {
440 item.dom.classList.remove('selected');460 item.dom.classList.remove('selected');
441 }461 }
462 if (!item.isSelectable) {
463 item.dom.classList.add('not-selectable');
464 }
442 frag.append(item.dom);465 frag.append(item.dom);
443 }466 }
444 this.dom.append(frag);467 this.dom.append(frag);
445 this.updatePosition();468 this.updatePosition();
446 getTopmostModalLayer().append(this.domWrap);469 this.getLayer().append(this.domWrap);
447 } else {470 } else {
448 this.domWrap.remove();471 this.domWrap.remove();
449 }472 }
@@ -458,10 +481,17 @@ export class AutoComplete {
458 if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();481 if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
459 this.detailsDom.innerHTML = '';482 this.detailsDom.innerHTML = '';
460 this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');483 this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
461 getTopmostModalLayer().append(this.detailsWrap);484 this.getLayer().append(this.detailsWrap);
462 this.updateDetailsPositionDebounced();485 this.updateDetailsPositionDebounced();
463 }486 }
464487
488 /**
489 * @returns {HTMLElement} closest ancestor dialog or body
490 */
491 getLayer() {
492 return this.textarea.closest('dialog, body');
493 }
494
465495
466496
467 /**497 /**
@@ -474,7 +504,7 @@ export class AutoComplete {
474 const rect = {};504 const rect = {};
475 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();505 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
476 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();506 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
477 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();507 rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect();
478 this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);508 this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
479 this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);509 this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
480 this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;510 this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
@@ -501,7 +531,7 @@ export class AutoComplete {
501 const rect = {};531 const rect = {};
502 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();532 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
503 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();533 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
504 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();534 rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect();
505 if (this.isReplaceable) {535 if (this.isReplaceable) {
506 this.detailsWrap.classList.remove('full');536 this.detailsWrap.classList.remove('full');
507 const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();537 const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
@@ -527,32 +557,34 @@ export class AutoComplete {
527 updateFloatingPosition() {557 updateFloatingPosition() {
528 const location = this.getCursorPosition();558 const location = this.getCursorPosition();
529 const rect = this.textarea.getBoundingClientRect();559 const rect = this.textarea.getBoundingClientRect();
560 const layerRect = this.getLayer().getBoundingClientRect();
530 // cursor is out of view -> hide561 // cursor is out of view -> hide
531 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {562 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
532 return this.hide();563 return this.hide();
533 }564 }
534 const left = Math.max(rect.left, location.left);565 const left = Math.max(rect.left, location.left) - layerRect.left;
535 this.domWrap.style.setProperty('--targetOffset', `${left}`);566 this.domWrap.style.setProperty('--targetOffset', `${left}`);
536 if (location.top <= window.innerHeight / 2) {567 if (location.top <= window.innerHeight / 2) {
537 // if cursor is in lower half of window, show list above line568 // if cursor is in lower half of window, show list above line
538 this.domWrap.style.top = `${location.bottom}px`;569 this.domWrap.style.top = `${location.bottom - layerRect.top}px`;
539 this.domWrap.style.bottom = 'auto';570 this.domWrap.style.bottom = 'auto';
540 this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;571 this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
541 } else {572 } else {
542 // if cursor is in upper half of window, show list below line573 // if cursor is in upper half of window, show list below line
543 this.domWrap.style.top = 'auto';574 this.domWrap.style.top = 'auto';
544 this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`;575 this.domWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`;
545 this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;576 this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
546 }577 }
547 }578 }
548579
549 updateFloatingDetailsPosition(location = null) {580 updateFloatingDetailsPosition(location = null) {
550 if (!location) location = this.getCursorPosition();581 if (!location) location = this.getCursorPosition();
551 const rect = this.textarea.getBoundingClientRect();582 const rect = this.textarea.getBoundingClientRect();
583 const layerRect = this.getLayer().getBoundingClientRect();
552 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {584 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
553 return this.hide();585 return this.hide();
554 }586 }
555 const left = Math.max(rect.left, location.left);587 const left = Math.max(rect.left, location.left) - layerRect.left;
556 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);588 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
557 if (this.isReplaceable) {589 if (this.isReplaceable) {
558 this.detailsWrap.classList.remove('full');590 this.detailsWrap.classList.remove('full');
@@ -572,14 +604,14 @@ export class AutoComplete {
572 }604 }
573 if (location.top <= window.innerHeight / 2) {605 if (location.top <= window.innerHeight / 2) {
574 // if cursor is in lower half of window, show list above line606 // if cursor is in lower half of window, show list above line
575 this.detailsWrap.style.top = `${location.bottom}px`;607 this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`;
576 this.detailsWrap.style.bottom = 'auto';608 this.detailsWrap.style.bottom = 'auto';
577 this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;609 this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
578 } else {610 } else {
579 // if cursor is in upper half of window, show list below line611 // if cursor is in upper half of window, show list below line
580 this.detailsWrap.style.top = 'auto';612 this.detailsWrap.style.top = 'auto';
581 this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`;613 this.detailsWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`;
582 this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;614 this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
583 }615 }
584 }616 }
585617
@@ -597,7 +629,7 @@ export class AutoComplete {
597 }629 }
598 this.clone.style.position = 'fixed';630 this.clone.style.position = 'fixed';
599 this.clone.style.visibility = 'hidden';631 this.clone.style.visibility = 'hidden';
600 getTopmostModalLayer().append(this.clone);632 document.body.append(this.clone);
601 const mo = new MutationObserver(muts=>{633 const mo = new MutationObserver(muts=>{
602 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {634 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
603 this.clone.remove();635 this.clone.remove();
@@ -656,6 +688,7 @@ export class AutoComplete {
656 }688 }
657 this.wasForced = false;689 this.wasForced = false;
658 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));690 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
691 this.onSelect?.(this.selectedItem);
659 }692 }
660693
661694
@@ -708,8 +741,10 @@ export class AutoComplete {
708 }741 }
709 case 'Enter': {742 case 'Enter': {
710 // pick the selected item to autocomplete743 // pick the selected item to autocomplete
744 if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break;
711 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;745 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
712 if (this.selectedItem.name == this.name) break;746 if (this.selectedItem.name == this.name) break;
747 if (!this.selectedItem.isSelectable) break;
713 evt.preventDefault();748 evt.preventDefault();
714 evt.stopImmediatePropagation();749 evt.stopImmediatePropagation();
715 this.select();750 this.select();
@@ -717,9 +752,11 @@ export class AutoComplete {
717 }752 }
718 case 'Tab': {753 case 'Tab': {
719 // pick the selected item to autocomplete754 // pick the selected item to autocomplete
755 if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break;
720 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;756 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
721 evt.preventDefault();757 evt.preventDefault();
722 evt.stopImmediatePropagation();758 evt.stopImmediatePropagation();
759 if (!this.selectedItem.isSelectable) break;
723 this.select();760 this.select();
724 return;761 return;
725 }762 }
@@ -772,30 +809,16 @@ export class AutoComplete {
772 // ignore keydown on modifier keys809 // ignore keydown on modifier keys
773 return;810 return;
774 }811 }
775 switch (evt.key) {812 // await keyup to see if cursor position or text has changed
776 case 'ArrowUp':813 const oldText = this.textarea.value;
777 case 'ArrowDown':814 await new Promise(resolve=>{
778 case 'ArrowRight':815 window.addEventListener('keyup', resolve, { once:true });
779 case 'ArrowLeft': {816 });
780 if (this.isActive) {817 if (this.selectionStart != this.textarea.selectionStart) {
781 // keyboard navigation, wait for keyup to complete cursor move818 this.selectionStart = this.textarea.selectionStart;
782 const oldText = this.textarea.value;819 this.show(this.isReplaceable || oldText != this.textarea.value);
783 await new Promise(resolve=>{820 } else if (this.isActive) {
784 window.addEventListener('keyup', resolve, { once:true });821 this.text != this.textarea.value && this.show(this.isReplaceable);
785 });
786 if (this.selectionStart != this.textarea.selectionStart) {
787 this.selectionStart = this.textarea.selectionStart;
788 this.show(this.isReplaceable || oldText != this.textarea.value);
789 }
790 }
791 break;
792 }
793 default: {
794 if (this.isActive) {
795 this.text != this.textarea.value && this.show(this.isReplaceable);
796 }
797 break;
798 }
799 }822 }
800 }823 }
801}824}
public/scripts/autocomplete/AutoCompleteNameResult.js+3 -30
@@ -1,36 +1,9 @@
1import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';1import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
2import { AutoCompleteOption } from './AutoCompleteOption.js';2import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
3// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
43
54
65
7export class AutoCompleteNameResult {6export class AutoCompleteNameResult extends AutoCompleteNameResultBase {
8 /**@type {string} */ name;
9 /**@type {number} */ start;
10 /**@type {AutoCompleteOption[]} */ optionList = [];
11 /**@type {boolean} */ canBeQuoted = false;
12 /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
13 /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
14
15
16 /**
17 * @param {string} name Name (potentially partial) of the name at the requested index.
18 * @param {number} start Index where the name starts.
19 * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
20 * @param {boolean} canBeQuoted Whether the name can be inside quotes.
21 * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
22 * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
23 */
24 constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
25 this.name = name;
26 this.start = start;
27 this.optionList = optionList;
28 this.canBeQuoted = canBeQuoted;
29 this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
30 this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
31 }
32
33
34 /**7 /**
35 *8 *
36 * @param {string} text The whole text9 * @param {string} text The whole text
public/scripts/autocomplete/AutoCompleteNameResultBase.js+31 -0
@@ -0,0 +1,31 @@
1import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
2import { AutoCompleteOption } from './AutoCompleteOption.js';
3
4
5
6export class AutoCompleteNameResultBase {
7 /**@type {string} */ name;
8 /**@type {number} */ start;
9 /**@type {AutoCompleteOption[]} */ optionList = [];
10 /**@type {boolean} */ canBeQuoted = false;
11 /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
12 /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
13
14
15 /**
16 * @param {string} name Name (potentially partial) of the name at the requested index.
17 * @param {number} start Index where the name starts.
18 * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
19 * @param {boolean} canBeQuoted Whether the name can be inside quotes.
20 * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
21 * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
22 */
23 constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
24 this.name = name;
25 this.start = start;
26 this.optionList = optionList;
27 this.canBeQuoted = canBeQuoted;
28 this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
29 this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
30 }
31}
public/scripts/autocomplete/AutoCompleteOption.js+11 -1
@@ -11,6 +11,9 @@ export class AutoCompleteOption {
11 /**@type {AutoCompleteFuzzyScore}*/ score;11 /**@type {AutoCompleteFuzzyScore}*/ score;
12 /**@type {string}*/ replacer;12 /**@type {string}*/ replacer;
13 /**@type {HTMLElement}*/ dom;13 /**@type {HTMLElement}*/ dom;
14 /**@type {(input:string)=>boolean}*/ matchProvider;
15 /**@type {(input:string)=>string}*/ valueProvider;
16 /**@type {boolean}*/ makeSelectable = false;
1417
1518
16 /**19 /**
@@ -21,14 +24,21 @@ export class AutoCompleteOption {
21 return this.name;24 return this.name;
22 }25 }
2326
27 get isSelectable() {
28 return this.makeSelectable || !this.valueProvider;
29 }
30
2431
25 /**32 /**
26 * @param {string} name33 * @param {string} name
27 */34 */
28 constructor(name, typeIcon = ' ', type = '') {35 constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) {
29 this.name = name;36 this.name = name;
30 this.typeIcon = typeIcon;37 this.typeIcon = typeIcon;
31 this.type = type;38 this.type = type;
39 this.matchProvider = matchProvider;
40 this.valueProvider = valueProvider;
41 this.makeSelectable = makeSelectable;
32 }42 }
3343
3444
public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js+2 -2
@@ -1,6 +1,6 @@
1import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';1import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
22
3export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {3export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultBase {
4 /**@type {boolean}*/ isRequired = false;4 /**@type {boolean}*/ isRequired = false;
5 /**@type {boolean}*/ forceMatch = true;5 /**@type {boolean}*/ forceMatch = true;
6}6}
public/scripts/bookmarks.js+2 -2
@@ -14,7 +14,7 @@ import {
14 saveChatConditional,14 saveChatConditional,
15 saveItemizedPrompts,15 saveItemizedPrompts,
16} from '../script.js';16} from '../script.js';
17import { humanizedDateTime } from './RossAscends-mods.js';17import { humanizedDateTime, getMessageTimeStamp } from './RossAscends-mods.js';
18import {18import {
19 getGroupPastChats,19 getGroupPastChats,
20 group_activation_strategy,20 group_activation_strategy,
@@ -297,7 +297,7 @@ async function convertSoloToGroupChat() {
297 if (groupChat.length === 0) {297 if (groupChat.length === 0) {
298 const newMessage = {298 const newMessage = {
299 ...system_messages[system_message_types.GROUP],299 ...system_messages[system_message_types.GROUP],
300 send_date: humanizedDateTime(),300 send_date: getMessageTimeStamp(),
301 extra: { type: system_message_types.GROUP },301 extra: { type: system_message_types.GROUP },
302 };302 };
303 groupChat.push(newMessage);303 groupChat.push(newMessage);
public/scripts/extensions.js+23 -21
@@ -989,6 +989,28 @@ export async function writeExtensionField(characterId, key, value) {
989 }989 }
990}990}
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 */
1001export 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
992jQuery(async function () {1014jQuery(async function () {
993 await addExtensionsButtonAndMenu();1015 await addExtensionsButtonAndMenu();
994 $('#extensionsMenuButton').css('display', 'flex');1016 $('#extensionsMenuButton').css('display', 'flex');
@@ -1004,28 +1026,8 @@ jQuery(async function () {
10041026
1005 /**1027 /**
1006 * Handles the click event for the third-party extension import button.1028 * Handles the click event for the third-party extension import button.
1007 * Prompts the user to enter the Git URL of the extension to import.
1008 * After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
1009 * If the extension is imported successfully, a success message is displayed.
1010 * If the extension import fails, an error message is displayed and the error is logged to the console.
1011 * After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
1012 *1029 *
1013 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.1030 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
1014 */1031 */
1015 $('#third_party_extension_button').on('click', async () => {1032 $('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
1016 const html = `<h3>Enter the Git URL of the extension to install</h3>
1017 <br>
1018 <p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
1019 <br>
1020 <p>Example: <tt> https://github.com/author/extension-name </tt></p>`;
1021 const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '');
1022
1023 if (!input) {
1024 console.debug('Extension install cancelled');
1025 return;
1026 }
1027
1028 const url = String(input).trim();
1029 await installExtension(url);
1030 });
1031});1033});
public/scripts/extensions/caption/index.js+7 -3
@@ -8,13 +8,12 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
8import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';8import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
9import { SlashCommand } from '../../slash-commands/SlashCommand.js';9import { SlashCommand } from '../../slash-commands/SlashCommand.js';
10import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';10import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
11import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
12import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';11import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
13export { MODULE_NAME };12export { MODULE_NAME };
1413
15const MODULE_NAME = 'caption';14const MODULE_NAME = 'caption';
1615
17const PROMPT_DEFAULT = 'What’s in this image?';16const PROMPT_DEFAULT = 'What\'s in this image?';
18const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';17const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';
1918
20/**19/**
@@ -170,7 +169,11 @@ async function sendCaptionedMessage(caption, image) {
170 },169 },
171 };170 };
172 context.chat.push(message);171 context.chat.push(message);
172 const messageId = context.chat.length - 1;
173 await eventSource.emit(event_types.MESSAGE_SENT, messageId);
173 context.addOneMessage(message);174 context.addOneMessage(message);
175 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
176 await context.saveChat();
174}177}
175178
176/**179/**
@@ -334,7 +337,7 @@ async function getCaptionForFile(file, prompt, quiet) {
334 }337 }
335 catch (error) {338 catch (error) {
336 const errorMessage = error.message || 'Unknown error';339 const errorMessage = error.message || 'Unknown error';
337 toastr.error(errorMessage, "Failed to caption image.");340 toastr.error(errorMessage, 'Failed to caption image.');
338 console.error(error);341 console.error(error);
339 return '';342 return '';
340 }343 }
@@ -399,6 +402,7 @@ jQuery(async function () {
399 (modules.includes('caption') && extension_settings.caption.source === 'extras') ||402 (modules.includes('caption') && extension_settings.caption.source === 'extras') ||
400 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
401 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||404 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
405 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||
402 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||406 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||407 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
404 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||408 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
public/scripts/extensions/caption/settings.html+5 -0
@@ -17,6 +17,7 @@
17 <div class="flex1 flex-container flexFlowColumn flexNoGap">17 <div class="flex1 flex-container flexFlowColumn flexNoGap">
18 <label for="caption_multimodal_api" data-i18n="API">API</label>18 <label for="caption_multimodal_api" data-i18n="API">API</label>
19 <select id="caption_multimodal_api" class="flex1 text_pole">19 <select id="caption_multimodal_api" class="flex1 text_pole">
20 <option value="zerooneai">01.AI (Yi)</option>
20 <option value="anthropic">Anthropic</option>21 <option value="anthropic">Anthropic</option>
21 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>22 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
22 <option value="google">Google MakerSuite</option>23 <option value="google">Google MakerSuite</option>
@@ -32,16 +33,20 @@
32 <div class="flex1 flex-container flexFlowColumn flexNoGap">33 <div class="flex1 flex-container flexFlowColumn flexNoGap">
33 <label for="caption_multimodal_model" data-i18n="Model">Model</label>34 <label for="caption_multimodal_model" data-i18n="Model">Model</label>
34 <select id="caption_multimodal_model" class="flex1 text_pole">35 <select id="caption_multimodal_model" class="flex1 text_pole">
36 <option data-type="zerooneai" value="yi-vision">yi-vision</option>
35 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>37 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
36 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>38 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
37 <option data-type="openai" value="gpt-4o">gpt-4o</option>39 <option data-type="openai" value="gpt-4o">gpt-4o</option>
38 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>40 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>
41 <option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
39 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>42 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
40 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>43 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
41 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>44 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
42 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>45 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
43 <option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>46 <option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
44 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>47 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
48 <option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
49 <option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
45 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>50 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
46 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>51 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
47 <option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>52 <option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
public/scripts/extensions/expressions/index.js+8 -5
@@ -1,4 +1,4 @@
1import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';1import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';
2import { dragElement, isMobile } from '../../RossAscends-mods.js';2import { dragElement, isMobile } from '../../RossAscends-mods.js';
3import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';3import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
4import { loadMovingUIState, power_user } from '../../power-user.js';4import { loadMovingUIState, power_user } from '../../power-user.js';
@@ -1156,7 +1156,7 @@ async function getExpressionLabel(text) {
11561156
1157 functionResult = args?.arguments;1157 functionResult = args?.arguments;
1158 });1158 });
1159 const emotionResponse = await generateQuietPrompt(prompt, false, false);1159 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
1160 return parseLlmResponse(functionResult || emotionResponse, expressionsList);1160 return parseLlmResponse(functionResult || emotionResponse, expressionsList);
1161 }1161 }
1162 // Extras1162 // Extras
@@ -1387,7 +1387,8 @@ async function getExpressionsList() {
1387 }1387 }
13881388
1389 // If there was no specific list, or an error, just return the default expressions1389 // 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;
1391 }1392 }
13921393
1393 const result = await resolveExpressionsList();1394 const result = await resolveExpressionsList();
@@ -1618,11 +1619,13 @@ async function onClickExpressionRemoveCustom() {
1618 moduleWorker();1619 moduleWorker();
1619}1620}
16201621
1621function onExperesionApiChanged() {1622function onExpressionApiChanged() {
1622 const tempApi = this.value;1623 const tempApi = this.value;
1623 if (tempApi) {1624 if (tempApi) {
1624 extension_settings.expressions.api = Number(tempApi);1625 extension_settings.expressions.api = Number(tempApi);
1625 $('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm);1626 $('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm);
1627 expressionsList = null;
1628 spriteCache = {};
1626 moduleWorker();1629 moduleWorker();
1627 saveSettingsDebounced();1630 saveSettingsDebounced();
1628 }1631 }
@@ -1972,7 +1975,7 @@ function migrateSettings() {
1972 $('#expression_custom_add').on('click', onClickExpressionAddCustom);1975 $('#expression_custom_add').on('click', onClickExpressionAddCustom);
1973 $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom);1976 $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom);
1974 $('#expression_fallback').on('change', onExpressionFallbackChanged);1977 $('#expression_fallback').on('change', onExpressionFallbackChanged);
1975 $('#expression_api').on('change', onExperesionApiChanged);1978 $('#expression_api').on('change', onExpressionApiChanged);
1976 }1979 }
19771980
1978 // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized.1981 // 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+55 -34
@@ -3,6 +3,7 @@ import {
3 this_chid,3 this_chid,
4 characters,4 characters,
5 getRequestHeaders,5 getRequestHeaders,
6 event_types,
6} from '../../../script.js';7} from '../../../script.js';
7import { groups, selected_group } from '../../group-chats.js';8import { groups, selected_group } from '../../group-chats.js';
8import { loadFileToDocument, delay } from '../../utils.js';9import { loadFileToDocument, delay } from '../../utils.js';
@@ -13,6 +14,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
13import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';14import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
14import { DragAndDropHandler } from '../../dragdrop.js';15import { DragAndDropHandler } from '../../dragdrop.js';
15import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';16import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
17import { translate } from '../../i18n.js';
1618
17const extensionName = 'gallery';19const extensionName = 'gallery';
18const extensionFolderPath = `scripts/extensions/${extensionName}/`;20const extensionFolderPath = `scripts/extensions/${extensionName}/`;
@@ -24,6 +26,27 @@ let paginationVisiblePages = 10;
24let paginationMaxLinesPerPage = 2;26let paginationMaxLinesPerPage = 2;
25let galleryMaxRows = 3;27let 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
34const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved';
35
36const 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
46mutationObserver.observe(document.body, {
47 childList: true,
48 subtree: false,
49});
2750
28/**51/**
29 * Retrieves a list of gallery items based on a given URL. This function calls an API endpoint52 * 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) {
58 * @returns {Promise<void>} - Promise representing the completion of the gallery initialization.81 * @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
59 */82 */
60async function initGallery(items, url) {83async function initGallery(items, url) {
84 const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`;
61 const gallery = $('#dragGallery');85 const gallery = $('#dragGallery');
86 gallery.addClass(nonce);
62 gallery.nanogallery2({87 gallery.nanogallery2({
63 'items': items,88 'items': items,
64 thumbnailWidth: 'auto',89 thumbnailWidth: 'auto',
@@ -81,16 +106,26 @@ async function initGallery(items, url) {
81 fnThumbnailOpen: viewWithDragbox,106 fnThumbnailOpen: viewWithDragbox,
82 });107 });
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
85 eventSource.on('resizeUI', function (elmntName) {114 const resizeHandler = function () {
86 gallery.nanogallery2('resize');115 gallery.nanogallery2('resize');
87 });116 };
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 upload120 eventSource.once(event_types.CHAT_CHANGED, function () {
121 gallery.closest('#gallery').remove();
92 });122 });
93123
124 eventSource.once(CUSTOM_GALLERY_REMOVED_EVENT, function () {
125 gallery.nanogallery2('destroy');
126 dragDropHandler.destroy();
127 eventSource.removeListener('resizeUI', resizeHandler);
128 });
94129
95 // Set dropzone height to be the same as the parent130 // Set dropzone height to be the same as the parent
96 gallery.css('height', gallery.parent().css('height'));131 gallery.css('height', gallery.parent().css('height'));
@@ -139,16 +174,10 @@ async function showCharGallery() {
139174
140 const items = await getGalleryItems(url);175 const items = await getGalleryItems(url);
141 // if there already is a gallery, destroy it and place this one in its place176 // if there already is a gallery, destroy it and place this one in its place
142 if ($('#dragGallery').length) {177 $('#dragGallery').closest('#gallery').remove();
143 $('#dragGallery').nanogallery2('destroy');178 makeMovable();
144 initGallery(items, url);179 await delay(100);
145 } else {180 await initGallery(items, url);
146 makeMovable();
147 setTimeout(async () => {
148 await initGallery(items, url);
149 }, 100);
150 }
151
152 } catch (err) {181 } catch (err) {
153 console.trace();182 console.trace();
154 console.error(err);183 console.error(err);
@@ -201,11 +230,11 @@ async function uploadFile(file, url) {
201 toastr.success('File uploaded successfully. Saved at: ' + result.path);230 toastr.success('File uploaded successfully. Saved at: ' + result.path);
202231
203 // Refresh the gallery232 // Refresh the gallery
204 $('#dragGallery').nanogallery2('destroy'); // Destroy old gallery
205 const newItems = await getGalleryItems(url); // Fetch the latest items233 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
207235 makeMovable();
208236 await delay(100);
237 await initGallery(newItems, url); // Reinitialize the gallery with new items and pass 'url'
209 } catch (error) {238 } catch (error) {
210 console.error('There was an issue uploading the file:', error);239 console.error('There was an issue uploading the file:', error);
211240
@@ -228,7 +257,7 @@ $(document).ready(function () {
228 $('#char-management-dropdown').append(257 $('#char-management-dropdown').append(
229 $('<option>', {258 $('<option>', {
230 id: 'show_char_gallery',259 id: 'show_char_gallery',
231 text: 'Show Gallery',260 text: translate('Show Gallery'),
232 }),261 }),
233 );262 );
234});263});
@@ -272,11 +301,6 @@ function makeMovable(id = 'gallery') {
272 e.preventDefault();301 e.preventDefault();
273 return false;302 return false;
274 });303 });
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 });
280}304}
281305
282/**306/**
@@ -357,11 +381,6 @@ function makeDragImg(id, url) {
357 } else {381 } else {
358 console.error('Failed to append the template content or retrieve the appended content.');382 console.error('Failed to append the template content or retrieve the appended content.');
359 }383 }
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 });
365}384}
366385
367/**386/**
@@ -400,7 +419,8 @@ function viewWithDragbox(items) {
400419
401420
402// Registers a simple command for opening the char gallery.421// Registers a simple command for opening the char gallery.
403SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery',422SlashCommandParser.addCommandObject(SlashCommand.fromProps({
423 name: 'show-gallery',
404 aliases: ['sg'],424 aliases: ['sg'],
405 callback: () => {425 callback: () => {
406 showCharGallery();426 showCharGallery();
@@ -408,7 +428,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery
408 },428 },
409 helpString: 'Shows the gallery.',429 helpString: 'Shows the gallery.',
410}));430}));
411SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery',431SlashCommandParser.addCommandObject(SlashCommand.fromProps({
432 name: 'list-gallery',
412 aliases: ['lg'],433 aliases: ['lg'],
413 callback: listGalleryCommand,434 callback: listGalleryCommand,
414 returns: 'list of images',435 returns: 'list of images',
@@ -431,14 +452,14 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery
431452
432async function listGalleryCommand(args) {453async function listGalleryCommand(args) {
433 try {454 try {
434 let url = args.char ?? (args.group ? groups.find(it=>it.name == args.group)?.id : null) ?? (selected_group || this_chid);455 let url = args.char ?? (args.group ? groups.find(it => it.name == args.group)?.id : null) ?? (selected_group || this_chid);
435 if (!args.char && !args.group && !selected_group && this_chid) {456 if (!args.char && !args.group && !selected_group && this_chid) {
436 const char = characters[this_chid];457 const char = characters[this_chid];
437 url = char.avatar.replace('.png', '');458 url = char.avatar.replace('.png', '');
438 }459 }
439460
440 const items = await getGalleryItems(url);461 const items = await getGalleryItems(url);
441 return JSON.stringify(items.map(it=>it.src));462 return JSON.stringify(items.map(it => it.src));
442463
443 } catch (err) {464 } catch (err) {
444 console.trace();465 console.trace();
public/scripts/extensions/gallery/manifest.json+1 -1
@@ -5,7 +5,7 @@
5 "optional": [5 "optional": [
6 ],6 ],
7 "js": "index.js",7 "js": "index.js",
8 "css": "",8 "css": "style.css",
9 "author": "City-Unit",9 "author": "City-Unit",
10 "version": "1.5.0",10 "version": "1.5.0",
11 "homePage": "https://github.com/SillyTavern/SillyTavern"11 "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+154 -28
@@ -14,6 +14,7 @@ import {
14 substituteParamsExtended,14 substituteParamsExtended,
15 generateRaw,15 generateRaw,
16 getMaxContextSize,16 getMaxContextSize,
17 setExtensionPrompt,
17} from '../../../script.js';18} from '../../../script.js';
18import { is_group_generating, selected_group } from '../../group-chats.js';19import { is_group_generating, selected_group } from '../../group-chats.js';
19import { loadMovingUIState } from '../../power-user.js';20import { loadMovingUIState } from '../../power-user.js';
@@ -24,6 +25,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
24import { SlashCommand } from '../../slash-commands/SlashCommand.js';25import { SlashCommand } from '../../slash-commands/SlashCommand.js';
25import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';26import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
26import { MacrosParser } from '../../macros.js';27import { MacrosParser } from '../../macros.js';
28import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
27export { MODULE_NAME };29export { MODULE_NAME };
2830
29const MODULE_NAME = '1_memory';31const MODULE_NAME = '1_memory';
@@ -35,6 +37,41 @@ let lastMessageHash = null;
35let lastMessageId = null;37let lastMessageId = null;
36let inApiCall = false;38let 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 */
46async 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
60async 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
38const formatMemoryValue = function (value) {75const formatMemoryValue = function (value) {
39 if (!value) {76 if (!value) {
40 return '';77 return '';
@@ -54,6 +91,7 @@ const saveChatDebounced = debounce(() => getContext().saveChat(), debounce_timeo
54const summary_sources = {91const summary_sources = {
55 'extras': 'extras',92 'extras': 'extras',
56 'main': 'main',93 'main': 'main',
94 'webllm': 'webllm',
57};95};
5896
59const prompt_builders = {97const prompt_builders = {
@@ -73,6 +111,7 @@ const defaultSettings = {
73 template: defaultTemplate,111 template: defaultTemplate,
74 position: extension_prompt_types.IN_PROMPT,112 position: extension_prompt_types.IN_PROMPT,
75 role: extension_prompt_roles.SYSTEM,113 role: extension_prompt_roles.SYSTEM,
114 scan: false,
76 depth: 2,115 depth: 2,
77 promptWords: 200,116 promptWords: 200,
78 promptMinWords: 25,117 promptMinWords: 25,
@@ -122,17 +161,18 @@ function loadSettings() {
122 $(`input[name="memory_prompt_builder"][value="${extension_settings.memory.prompt_builder}"]`).prop('checked', true).trigger('input');161 $(`input[name="memory_prompt_builder"][value="${extension_settings.memory.prompt_builder}"]`).prop('checked', true).trigger('input');
123 $('#memory_override_response_length').val(extension_settings.memory.overrideResponseLength).trigger('input');162 $('#memory_override_response_length').val(extension_settings.memory.overrideResponseLength).trigger('input');
124 $('#memory_max_messages_per_request').val(extension_settings.memory.maxMessagesPerRequest).trigger('input');163 $('#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');
125 switchSourceControls(extension_settings.memory.source);165 switchSourceControls(extension_settings.memory.source);
126}166}
127167
128async function onPromptForceWordsAutoClick() {168async function onPromptForceWordsAutoClick() {
129 const context = getContext();169 const context = getContext();
130 const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength);170 const maxPromptLength = await getSourceContextSize();
131 const chat = context.chat;171 const chat = context.chat;
132 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);172 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
133 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;173 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
134 const averageMessageWordCount = messagesWordCount / allMessages.length;174 const averageMessageWordCount = messagesWordCount / allMessages.length;
135 const tokensPerWord = await getTokenCountAsync(allMessages.join('\n')) / messagesWordCount;175 const tokensPerWord = await countSourceTokens(allMessages.join('\n')) / messagesWordCount;
136 const wordsPerToken = 1 / tokensPerWord;176 const wordsPerToken = 1 / tokensPerWord;
137 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);177 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);
138 // How many words should pass so that messages will start be dropped out of context;178 // How many words should pass so that messages will start be dropped out of context;
@@ -165,15 +205,15 @@ async function onPromptForceWordsAutoClick() {
165205
166async function onPromptIntervalAutoClick() {206async function onPromptIntervalAutoClick() {
167 const context = getContext();207 const context = getContext();
168 const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength);208 const maxPromptLength = await getSourceContextSize();
169 const chat = context.chat;209 const chat = context.chat;
170 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);210 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
171 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;211 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
172 const messagesTokenCount = await getTokenCountAsync(allMessages.join('\n'));212 const messagesTokenCount = await countSourceTokens(allMessages.join('\n'));
173 const tokensPerWord = messagesTokenCount / messagesWordCount;213 const tokensPerWord = messagesTokenCount / messagesWordCount;
174 const averageMessageTokenCount = messagesTokenCount / allMessages.length;214 const averageMessageTokenCount = messagesTokenCount / allMessages.length;
175 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);215 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);
176 const promptTokens = await getTokenCountAsync(extension_settings.memory.prompt);216 const promptTokens = await countSourceTokens(extension_settings.memory.prompt);
177 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;217 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;
178 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;218 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
179 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);219 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);
@@ -210,8 +250,8 @@ function onSummarySourceChange(event) {
210250
211function switchSourceControls(value) {251function switchSourceControls(value) {
212 $('#memory_settings [data-summary-source]').each((_, element) => {252 $('#memory_settings [data-summary-source]').each((_, element) => {
213 const source = $(element).data('summary-source');253 const source = element.dataset.summarySource.split(',').map(s => s.trim());
214 $(element).toggle(source === value);254 $(element).toggle(source.includes(value));
215 });255 });
216}256}
217257
@@ -279,6 +319,13 @@ function onMemoryPositionChange(e) {
279 saveSettingsDebounced();319 saveSettingsDebounced();
280}320}
281321
322function onMemoryIncludeWIScanInput() {
323 const value = !!$(this).prop('checked');
324 extension_settings.memory.scan = value;
325 reinsertMemory();
326 saveSettingsDebounced();
327}
328
282function onMemoryPromptWordsForceInput() {329function onMemoryPromptWordsForceInput() {
283 const value = $(this).val();330 const value = $(this).val();
284 extension_settings.memory.promptForceWords = Number(value);331 extension_settings.memory.promptForceWords = Number(value);
@@ -343,10 +390,13 @@ function getIndexOfLatestChatSummary(chat) {
343390
344async function onChatEvent() {391async function onChatEvent() {
345 // Module not enabled392 // Module not enabled
346 if (extension_settings.memory.source === summary_sources.extras) {393 if (extension_settings.memory.source === summary_sources.extras && !modules.includes('summarize')) {
347 if (!modules.includes('summarize')) {394 return;
348 return;395 }
349 }396
397 // WebLLM is not supported
398 if (extension_settings.memory.source === summary_sources.webllm && !isWebLlmSupported()) {
399 return;
350 }400 }
351401
352 const context = getContext();402 const context = getContext();
@@ -421,8 +471,12 @@ async function forceSummarizeChat() {
421 return '';471 return '';
422 }472 }
423473
424 toastr.info('Summarizing chat...', 'Please wait');474 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
427 if (!value) {481 if (!value) {
428 toastr.warning('Failed to summarize chat');482 toastr.warning('Failed to summarize chat');
@@ -454,6 +508,11 @@ async function summarizeCallback(args, text) {
454 return await callExtrasSummarizeAPI(text);508 return await callExtrasSummarizeAPI(text);
455 case summary_sources.main:509 case summary_sources.main:
456 return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);510 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 }
457 default:516 default:
458 toastr.warning('Invalid summarization source specified');517 toastr.warning('Invalid summarization source specified');
459 return '';518 return '';
@@ -474,16 +533,25 @@ async function summarizeChat(context) {
474 case summary_sources.main:533 case summary_sources.main:
475 await summarizeChatMain(context, false, skipWIAN);534 await summarizeChatMain(context, false, skipWIAN);
476 break;535 break;
536 case summary_sources.webllm:
537 await summarizeChatWebLLM(context, false);
538 break;
477 default:539 default:
478 break;540 break;
479 }541 }
480}542}
481543
482async function summarizeChatMain(context, force, skipWIAN) {544/**
483545 * 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 */
551async function getSummaryPromptForNow(context, force) {
484 if (extension_settings.memory.promptInterval === 0 && !force) {552 if (extension_settings.memory.promptInterval === 0 && !force) {
485 console.debug('Prompt interval is set to 0, skipping summarization');553 console.debug('Prompt interval is set to 0, skipping summarization');
486 return;554 return '';
487 }555 }
488556
489 try {557 try {
@@ -495,17 +563,17 @@ async function summarizeChatMain(context, force, skipWIAN) {
495 waitUntilCondition(() => is_send_press === false, 30000, 100);563 waitUntilCondition(() => is_send_press === false, 30000, 100);
496 } catch {564 } catch {
497 console.debug('Timeout waiting for is_send_press');565 console.debug('Timeout waiting for is_send_press');
498 return;566 return '';
499 }567 }
500568
501 if (!context.chat.length) {569 if (!context.chat.length) {
502 console.debug('No messages in chat to summarize');570 console.debug('No messages in chat to summarize');
503 return;571 return '';
504 }572 }
505573
506 if (context.chat.length < extension_settings.memory.promptInterval && !force) {574 if (context.chat.length < extension_settings.memory.promptInterval && !force) {
507 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);575 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);
508 return;576 return '';
509 }577 }
510578
511 let messagesSinceLastSummary = 0;579 let messagesSinceLastSummary = 0;
@@ -529,7 +597,7 @@ async function summarizeChatMain(context, force, skipWIAN) {
529597
530 if (!conditionSatisfied && !force) {598 if (!conditionSatisfied && !force) {
531 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);599 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);
532 return;600 return '';
533 }601 }
534602
535 console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);603 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
538 if (!prompt) {606 if (!prompt) {
539 console.debug('Summarization prompt is empty. Skipping summarization.');607 console.debug('Summarization prompt is empty. Skipping summarization.');
608 return '';
609 }
610
611 return prompt;
612}
613
614async 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
661async function summarizeChatMain(context, force, skipWIAN) {
662 const prompt = await getSummaryPromptForNow(context, force);
663
664 if (!prompt) {
540 return;665 return;
541 }666 }
542667
@@ -624,7 +749,7 @@ async function getRawSummaryPrompt(context, prompt) {
624 chat.pop(); // We always exclude the last message from the buffer749 chat.pop(); // We always exclude the last message from the buffer
625 const chatBuffer = [];750 const chatBuffer = [];
626 const PADDING = 64;751 const PADDING = 64;
627 const PROMPT_SIZE = getMaxContextSize(extension_settings.memory.overrideResponseLength);752 const PROMPT_SIZE = await getSourceContextSize();
628 let latestUsedMessage = null;753 let latestUsedMessage = null;
629754
630 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {755 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {
@@ -641,7 +766,7 @@ async function getRawSummaryPrompt(context, prompt) {
641 const entry = `${message.name}:\n${message.mes}`;766 const entry = `${message.name}:\n${message.mes}`;
642 chatBuffer.push(entry);767 chatBuffer.push(entry);
643768
644 const tokens = await getTokenCountAsync(getMemoryString(true), PADDING);769 const tokens = await countSourceTokens(getMemoryString(true), PADDING);
645770
646 if (tokens > PROMPT_SIZE) {771 if (tokens > PROMPT_SIZE) {
647 chatBuffer.pop();772 chatBuffer.pop();
@@ -670,7 +795,7 @@ async function summarizeChatExtras(context) {
670 const reversedChat = chat.slice().reverse();795 const reversedChat = chat.slice().reverse();
671 reversedChat.shift();796 reversedChat.shift();
672 const memoryBuffer = [];797 const memoryBuffer = [];
673 const CONTEXT_SIZE = 1024 - 64;798 const CONTEXT_SIZE = await getSourceContextSize();
674799
675 for (const message of reversedChat) {800 for (const message of reversedChat) {
676 // we reached the point of latest memory801 // we reached the point of latest memory
@@ -688,14 +813,14 @@ async function summarizeChatExtras(context) {
688 memoryBuffer.push(entry);813 memoryBuffer.push(entry);
689814
690 // check if token limit was reached815 // check if token limit was reached
691 const tokens = getTextTokens(tokenizers.GPT2, getMemoryString()).length;816 const tokens = await countSourceTokens(getMemoryString());
692 if (tokens >= CONTEXT_SIZE) {817 if (tokens >= CONTEXT_SIZE) {
693 break;818 break;
694 }819 }
695 }820 }
696821
697 const resultingString = getMemoryString();822 const resultingString = getMemoryString();
698 const resultingTokens = getTextTokens(tokenizers.GPT2, resultingString).length;823 const resultingTokens = await countSourceTokens(resultingString);
699824
700 if (!resultingString || resultingTokens < CONTEXT_SIZE) {825 if (!resultingString || resultingTokens < CONTEXT_SIZE) {
701 console.debug('Not enough context to summarize');826 console.debug('Not enough context to summarize');
@@ -800,8 +925,7 @@ function reinsertMemory() {
800 * @param {number|null} index Index of the chat message to save the summary to. If null, the pre-last message is used.925 * @param {number|null} index Index of the chat message to save the summary to. If null, the pre-last message is used.
801 */926 */
802function setMemoryContext(value, saveToMessage, index = null) {927function 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);
805 $('#memory_contents').val(value);929 $('#memory_contents').val(value);
806930
807 const summaryLog = value931 const summaryLog = value
@@ -809,6 +933,7 @@ function setMemoryContext(value, saveToMessage, index = null) {
809 : 'Summary has no content';933 : 'Summary has no content';
810 console.debug(summaryLog);934 console.debug(summaryLog);
811935
936 const context = getContext();
812 if (saveToMessage && context.chat.length) {937 if (saveToMessage && context.chat.length) {
813 const idx = index ?? context.chat.length - 2;938 const idx = index ?? context.chat.length - 2;
814 const mes = context.chat[idx < 0 ? 0 : idx];939 const mes = context.chat[idx < 0 ? 0 : idx];
@@ -894,6 +1019,7 @@ function setupListeners() {
894 $('#memory_prompt_words_auto').off('click').on('click', onPromptForceWordsAutoClick);1019 $('#memory_prompt_words_auto').off('click').on('click', onPromptForceWordsAutoClick);
895 $('#memory_override_response_length').off('click').on('input', onOverrideResponseLengthInput);1020 $('#memory_override_response_length').off('click').on('input', onOverrideResponseLengthInput);
896 $('#memory_max_messages_per_request').off('click').on('input', onMaxMessagesPerRequestInput);1021 $('#memory_max_messages_per_request').off('click').on('input', onMaxMessagesPerRequestInput);
1022 $('#memory_include_wi_scan').off('input').on('input', onMemoryIncludeWIScanInput);
897 $('#summarySettingsBlockToggle').off('click').on('click', function () {1023 $('#summarySettingsBlockToggle').off('click').on('click', function () {
898 console.log('saw settings button click');1024 console.log('saw settings button click');
899 $('#summarySettingsBlock').slideToggle(200, 'swing'); //toggleClass("hidden");1025 $('#summarySettingsBlock').slideToggle(200, 'swing'); //toggleClass("hidden");
@@ -922,7 +1048,7 @@ jQuery(async function () {
922 name: 'summarize',1048 name: 'summarize',
923 callback: summarizeCallback,1049 callback: summarizeCallback,
924 namedArgumentList: [1050 namedArgumentList: [
925 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']),1051 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', Object.values(summary_sources)),
926 SlashCommandNamedArgument.fromProps({1052 SlashCommandNamedArgument.fromProps({
927 name: 'prompt',1053 name: 'prompt',
928 description: 'prompt to use for summarization',1054 description: 'prompt to use for summarization',
public/scripts/extensions/memory/settings.html+13 -3
@@ -13,6 +13,7 @@
13 <select id="summary_source">13 <select id="summary_source">
14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
15 <option value="extras">Extras API</option>15 <option value="extras">Extras API</option>
16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
16 </select><br>17 </select><br>
1718
18 <div class="flex-container justifyspacebetween alignitemscenter">19 <div class="flex-container justifyspacebetween alignitemscenter">
@@ -24,7 +25,7 @@
2425
25 <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>26 <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>
26 <div class="memory_contents_controls">27 <div class="memory_contents_controls">
27 <div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="[title]ext_sum_force_tip">28 <div id="memory_force_summarize" data-summary-source="main,webllm" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="[title]ext_sum_force_tip">
28 <i class="fa-solid fa-database"></i>29 <i class="fa-solid fa-database"></i>
29 <span data-i18n="ext_sum_force_text">Summarize now</span>30 <span data-i18n="ext_sum_force_text">Summarize now</span>
30 </div>31 </div>
@@ -58,7 +59,7 @@
58 <span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>59 <span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>
59 </label>60 </label>
60 </div>61 </div>
61 <div data-summary-source="main">62 <div data-summary-source="main,webllm">
62 <label for="memory_prompt" class="title_restorable">63 <label for="memory_prompt" class="title_restorable">
63 <span data-i18n="Summary Prompt">Summary Prompt</span>64 <span data-i18n="Summary Prompt">Summary Prompt</span>
64 <div id="memory_prompt_restore" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">65 <div id="memory_prompt_restore" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">
@@ -74,7 +75,7 @@
74 </label>75 </label>
75 <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />76 <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />
76 <label for="memory_max_messages_per_request">77 <label for="memory_max_messages_per_request">
77 <span data-i18n="ext_sum_raw_max_msg">[Raw] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)78 <span data-i18n="ext_sum_raw_max_msg">[Raw/WebLLM] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)
78 <small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>79 <small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>
79 </label>80 </label>
80 <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />81 <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />
@@ -109,8 +110,17 @@
109 <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>110 <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>
110 </div>111 </div>
111 <label for="memory_position" data-i18n="ext_sum_injection_position">Injection Position</label>112 <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>
112 <div class="radio_group">117 <div class="radio_group">
113 <label>118 <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>
114 <input type="radio" name="memory_position" value="2" />124 <input type="radio" name="memory_position" value="2" />
115 <span data-i18n="Before Main Prompt / Story String">Before Main Prompt / Story String</span>125 <span data-i18n="Before Main Prompt / Story String">Before Main Prompt / Story String</span>
116 </label>126 </label>
public/scripts/extensions/quick-reply/api/QuickReplyApi.js+94 -66
@@ -24,9 +24,17 @@ export class QuickReplyApi {
2424
2525
26 /**26 /**
27 * @param {QuickReply} qr
28 * @returns {QuickReplySet}
29 */
30 getSetByQr(qr) {
31 return QuickReplySet.list.find(it=>it.qrList.includes(qr));
32 }
33
34 /**
27 * Finds and returns an existing Quick Reply Set by its name.35 * Finds and returns an existing Quick Reply Set by its name.
28 *36 *
29 * @param {String} name name of the quick reply set37 * @param {string} name name of the quick reply set
30 * @returns the quick reply set, or undefined if not found38 * @returns the quick reply set, or undefined if not found
31 */39 */
32 getSetByName(name) {40 getSetByName(name) {
@@ -36,13 +44,14 @@ export class QuickReplyApi {
36 /**44 /**
37 * Finds and returns an existing Quick Reply by its set's name and its label.45 * Finds and returns an existing Quick Reply by its set's name and its label.
38 *46 *
39 * @param {String} setName name of the quick reply set47 * @param {string} setName name of the quick reply set
40 * @param {String} label label of the quick reply48 * @param {string|number} label label or numeric ID of the quick reply
41 * @returns the quick reply, or undefined if not found49 * @returns the quick reply, or undefined if not found
42 */50 */
43 getQrByLabel(setName, label) {51 getQrByLabel(setName, label) {
44 const set = this.getSetByName(setName);52 const set = this.getSetByName(setName);
45 if (!set) return;53 if (!set) return;
54 if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label);
46 return set.qrList.find(it=>it.label == label);55 return set.qrList.find(it=>it.label == label);
47 }56 }
4857
@@ -70,24 +79,25 @@ export class QuickReplyApi {
70 /**79 /**
71 * Executes an existing quick reply.80 * Executes an existing quick reply.
72 *81 *
73 * @param {String} setName name of the existing quick reply set82 * @param {string} setName name of the existing quick reply set
74 * @param {String} label label of the existing quick reply (text on the button)83 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
75 * @param {Object} [args] optional arguments84 * @param {object} [args] optional arguments
85 * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
76 */86 */
77 async executeQuickReply(setName, label, args = {}) {87 async executeQuickReply(setName, label, args = {}, options = {}) {
78 const qr = this.getQrByLabel(setName, label);88 const qr = this.getQrByLabel(setName, label);
79 if (!qr) {89 if (!qr) {
80 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);90 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
81 }91 }
82 return await qr.execute(args);92 return await qr.execute(args, false, false, options);
83 }93 }
8494
8595
86 /**96 /**
87 * Adds or removes a quick reply set to the list of globally active quick reply sets.97 * Adds or removes a quick reply set to the list of globally active quick reply sets.
88 *98 *
89 * @param {String} name the name of the set99 * @param {string} name the name of the set
90 * @param {Boolean} isVisible whether to show the set's buttons or not100 * @param {boolean} isVisible whether to show the set's buttons or not
91 */101 */
92 toggleGlobalSet(name, isVisible = true) {102 toggleGlobalSet(name, isVisible = true) {
93 const set = this.getSetByName(name);103 const set = this.getSetByName(name);
@@ -104,8 +114,8 @@ export class QuickReplyApi {
104 /**114 /**
105 * Adds a quick reply set to the list of globally active quick reply sets.115 * Adds a quick reply set to the list of globally active quick reply sets.
106 *116 *
107 * @param {String} name the name of the set117 * @param {string} name the name of the set
108 * @param {Boolean} isVisible whether to show the set's buttons or not118 * @param {boolean} isVisible whether to show the set's buttons or not
109 */119 */
110 addGlobalSet(name, isVisible = true) {120 addGlobalSet(name, isVisible = true) {
111 const set = this.getSetByName(name);121 const set = this.getSetByName(name);
@@ -118,7 +128,7 @@ export class QuickReplyApi {
118 /**128 /**
119 * Removes a quick reply set from the list of globally active quick reply sets.129 * Removes a quick reply set from the list of globally active quick reply sets.
120 *130 *
121 * @param {String} name the name of the set131 * @param {string} name the name of the set
122 */132 */
123 removeGlobalSet(name) {133 removeGlobalSet(name) {
124 const set = this.getSetByName(name);134 const set = this.getSetByName(name);
@@ -132,8 +142,8 @@ export class QuickReplyApi {
132 /**142 /**
133 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.143 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
134 *144 *
135 * @param {String} name the name of the set145 * @param {string} name the name of the set
136 * @param {Boolean} isVisible whether to show the set's buttons or not146 * @param {boolean} isVisible whether to show the set's buttons or not
137 */147 */
138 toggleChatSet(name, isVisible = true) {148 toggleChatSet(name, isVisible = true) {
139 if (!this.settings.chatConfig) return;149 if (!this.settings.chatConfig) return;
@@ -151,8 +161,8 @@ export class QuickReplyApi {
151 /**161 /**
152 * Adds a quick reply set to the list of the current chat's active quick reply sets.162 * Adds a quick reply set to the list of the current chat's active quick reply sets.
153 *163 *
154 * @param {String} name the name of the set164 * @param {string} name the name of the set
155 * @param {Boolean} isVisible whether to show the set's buttons or not165 * @param {boolean} isVisible whether to show the set's buttons or not
156 */166 */
157 addChatSet(name, isVisible = true) {167 addChatSet(name, isVisible = true) {
158 if (!this.settings.chatConfig) return;168 if (!this.settings.chatConfig) return;
@@ -166,7 +176,7 @@ export class QuickReplyApi {
166 /**176 /**
167 * Removes a quick reply set from the list of the current chat's active quick reply sets.177 * Removes a quick reply set from the list of the current chat's active quick reply sets.
168 *178 *
169 * @param {String} name the name of the set179 * @param {string} name the name of the set
170 */180 */
171 removeChatSet(name) {181 removeChatSet(name) {
172 if (!this.settings.chatConfig) return;182 if (!this.settings.chatConfig) return;
@@ -181,21 +191,26 @@ export class QuickReplyApi {
181 /**191 /**
182 * Creates a new quick reply in an existing quick reply set.192 * Creates a new quick reply in an existing quick reply set.
183 *193 *
184 * @param {String} setName name of the quick reply set to insert the new quick reply into194 * @param {string} setName name of the quick reply set to insert the new quick reply into
185 * @param {String} label label for the new quick reply (text on the button)195 * @param {string} label label for the new quick reply (text on the button)
186 * @param {Object} [props]196 * @param {object} [props]
187 * @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply197 * @param {string} [props.icon] the icon to show on the QR button
188 * @param {String} [props.title] the title / tooltip to be shown on the quick reply button198 * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
189 * @param {Boolean} [props.isHidden] whether to hide or show the button199 * @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply
190 * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts200 * @param {string} [props.title] the title / tooltip to be shown on the quick reply button
191 * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message201 * @param {boolean} [props.isHidden] whether to hide or show the button
192 * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message202 * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
193 * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded203 * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
194 * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected204 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
195 * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated205 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
206 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
207 * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
208 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
196 * @returns {QuickReply} the new quick reply209 * @returns {QuickReply} the new quick reply
197 */210 */
198 createQuickReply(setName, label, {211 createQuickReply(setName, label, {
212 icon,
213 showLabel,
199 message,214 message,
200 title,215 title,
201 isHidden,216 isHidden,
@@ -204,6 +219,7 @@ export class QuickReplyApi {
204 executeOnAi,219 executeOnAi,
205 executeOnChatChange,220 executeOnChatChange,
206 executeOnGroupMemberDraft,221 executeOnGroupMemberDraft,
222 executeOnNewChat,
207 automationId,223 automationId,
208 } = {}) {224 } = {}) {
209 const set = this.getSetByName(setName);225 const set = this.getSetByName(setName);
@@ -212,6 +228,8 @@ export class QuickReplyApi {
212 }228 }
213 const qr = set.addQuickReply();229 const qr = set.addQuickReply();
214 qr.label = label ?? '';230 qr.label = label ?? '';
231 qr.icon = icon ?? '';
232 qr.showLabel = showLabel ?? false;
215 qr.message = message ?? '';233 qr.message = message ?? '';
216 qr.title = title ?? '';234 qr.title = title ?? '';
217 qr.isHidden = isHidden ?? false;235 qr.isHidden = isHidden ?? false;
@@ -220,6 +238,7 @@ export class QuickReplyApi {
220 qr.executeOnAi = executeOnAi ?? false;238 qr.executeOnAi = executeOnAi ?? false;
221 qr.executeOnChatChange = executeOnChatChange ?? false;239 qr.executeOnChatChange = executeOnChatChange ?? false;
222 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;240 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
241 qr.executeOnNewChat = executeOnNewChat ?? false;
223 qr.automationId = automationId ?? '';242 qr.automationId = automationId ?? '';
224 qr.onUpdate();243 qr.onUpdate();
225 return qr;244 return qr;
@@ -228,22 +247,27 @@ export class QuickReplyApi {
228 /**247 /**
229 * Updates an existing quick reply.248 * Updates an existing quick reply.
230 *249 *
231 * @param {String} setName name of the existing quick reply set250 * @param {string} setName name of the existing quick reply set
232 * @param {String} label label of the existing quick reply (text on the button)251 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
233 * @param {Object} [props]252 * @param {object} [props]
234 * @param {String} [props.newLabel] new label for quick reply (text on the button)253 * @param {string} [props.icon] the icon to show on the QR button
235 * @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply254 * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
236 * @param {String} [props.title] the title / tooltip to be shown on the quick reply button255 * @param {string} [props.newLabel] new label for quick reply (text on the button)
237 * @param {Boolean} [props.isHidden] whether to hide or show the button256 * @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply
238 * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts257 * @param {string} [props.title] the title / tooltip to be shown on the quick reply button
239 * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message258 * @param {boolean} [props.isHidden] whether to hide or show the button
240 * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message259 * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
241 * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded260 * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
242 * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected261 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
243 * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated262 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
263 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
264 * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
265 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
244 * @returns {QuickReply} the altered quick reply266 * @returns {QuickReply} the altered quick reply
245 */267 */
246 updateQuickReply(setName, label, {268 updateQuickReply(setName, label, {
269 icon,
270 showLabel,
247 newLabel,271 newLabel,
248 message,272 message,
249 title,273 title,
@@ -253,12 +277,15 @@ export class QuickReplyApi {
253 executeOnAi,277 executeOnAi,
254 executeOnChatChange,278 executeOnChatChange,
255 executeOnGroupMemberDraft,279 executeOnGroupMemberDraft,
280 executeOnNewChat,
256 automationId,281 automationId,
257 } = {}) {282 } = {}) {
258 const qr = this.getQrByLabel(setName, label);283 const qr = this.getQrByLabel(setName, label);
259 if (!qr) {284 if (!qr) {
260 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);285 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
261 }286 }
287 qr.updateIcon(icon ?? qr.icon);
288 qr.updateShowLabel(showLabel ?? qr.showLabel);
262 qr.updateLabel(newLabel ?? qr.label);289 qr.updateLabel(newLabel ?? qr.label);
263 qr.updateMessage(message ?? qr.message);290 qr.updateMessage(message ?? qr.message);
264 qr.updateTitle(title ?? qr.title);291 qr.updateTitle(title ?? qr.title);
@@ -268,6 +295,7 @@ export class QuickReplyApi {
268 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;295 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
269 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;296 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
270 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;297 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
298 qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
271 qr.automationId = automationId ?? qr.automationId;299 qr.automationId = automationId ?? qr.automationId;
272 qr.onUpdate();300 qr.onUpdate();
273 return qr;301 return qr;
@@ -276,8 +304,8 @@ export class QuickReplyApi {
276 /**304 /**
277 * Deletes an existing quick reply.305 * Deletes an existing quick reply.
278 *306 *
279 * @param {String} setName name of the existing quick reply set307 * @param {string} setName name of the existing quick reply set
280 * @param {String} label label of the existing quick reply (text on the button)308 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
281 */309 */
282 deleteQuickReply(setName, label) {310 deleteQuickReply(setName, label) {
283 const qr = this.getQrByLabel(setName, label);311 const qr = this.getQrByLabel(setName, label);
@@ -291,10 +319,10 @@ export class QuickReplyApi {
291 /**319 /**
292 * Adds an existing quick reply set as a context menu to an existing quick reply.320 * Adds an existing quick reply set as a context menu to an existing quick reply.
293 *321 *
294 * @param {String} setName name of the existing quick reply set containing the quick reply322 * @param {string} setName name of the existing quick reply set containing the quick reply
295 * @param {String} label label of the existing quick reply323 * @param {string|number} label label of the existing quick reply or its numeric ID
296 * @param {String} contextSetName name of the existing quick reply set to be used as a context menu324 * @param {string} contextSetName name of the existing quick reply set to be used as a context menu
297 * @param {Boolean} isChained whether or not to chain the context menu quick replies325 * @param {boolean} isChained whether or not to chain the context menu quick replies
298 */326 */
299 createContextItem(setName, label, contextSetName, isChained = false) {327 createContextItem(setName, label, contextSetName, isChained = false) {
300 const qr = this.getQrByLabel(setName, label);328 const qr = this.getQrByLabel(setName, label);
@@ -314,9 +342,9 @@ export class QuickReplyApi {
314 /**342 /**
315 * Removes a quick reply set from a quick reply's context menu.343 * Removes a quick reply set from a quick reply's context menu.
316 *344 *
317 * @param {String} setName name of the existing quick reply set containing the quick reply345 * @param {string} setName name of the existing quick reply set containing the quick reply
318 * @param {String} label label of the existing quick reply346 * @param {string|number} label label of the existing quick reply or its numeric ID
319 * @param {String} contextSetName name of the existing quick reply set to be used as a context menu347 * @param {string} contextSetName name of the existing quick reply set to be used as a context menu
320 */348 */
321 deleteContextItem(setName, label, contextSetName) {349 deleteContextItem(setName, label, contextSetName) {
322 const qr = this.getQrByLabel(setName, label);350 const qr = this.getQrByLabel(setName, label);
@@ -333,8 +361,8 @@ export class QuickReplyApi {
333 /**361 /**
334 * Removes all entries from a quick reply's context menu.362 * Removes all entries from a quick reply's context menu.
335 *363 *
336 * @param {String} setName name of the existing quick reply set containing the quick reply364 * @param {string} setName name of the existing quick reply set containing the quick reply
337 * @param {String} label label of the existing quick reply365 * @param {string|number} label label of the existing quick reply or its numeric ID
338 */366 */
339 clearContextMenu(setName, label) {367 clearContextMenu(setName, label) {
340 const qr = this.getQrByLabel(setName, label);368 const qr = this.getQrByLabel(setName, label);
@@ -348,11 +376,11 @@ export class QuickReplyApi {
348 /**376 /**
349 * Create a new quick reply set.377 * Create a new quick reply set.
350 *378 *
351 * @param {String} name name of the new quick reply set379 * @param {string} name name of the new quick reply set
352 * @param {Object} [props]380 * @param {object} [props]
353 * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box381 * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
354 * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input382 * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
355 * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply383 * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
356 * @returns {Promise<QuickReplySet>} the new quick reply set384 * @returns {Promise<QuickReplySet>} the new quick reply set
357 */385 */
358 async createSet(name, {386 async createSet(name, {
@@ -384,11 +412,11 @@ export class QuickReplyApi {
384 /**412 /**
385 * Update an existing quick reply set.413 * Update an existing quick reply set.
386 *414 *
387 * @param {String} name name of the existing quick reply set415 * @param {string} name name of the existing quick reply set
388 * @param {Object} [props]416 * @param {object} [props]
389 * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box417 * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
390 * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input418 * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
391 * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply419 * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
392 * @returns {Promise<QuickReplySet>} the altered quick reply set420 * @returns {Promise<QuickReplySet>} the altered quick reply set
393 */421 */
394 async updateSet(name, {422 async updateSet(name, {
@@ -411,7 +439,7 @@ export class QuickReplyApi {
411 /**439 /**
412 * Delete an existing quick reply set.440 * Delete an existing quick reply set.
413 *441 *
414 * @param {String} name name of the existing quick reply set442 * @param {string} name name of the existing quick reply set
415 */443 */
416 async deleteSet(name) {444 async deleteSet(name) {
417 const set = this.getSetByName(name);445 const set = this.getSetByName(name);
@@ -451,7 +479,7 @@ export class QuickReplyApi {
451 /**479 /**
452 * Gets a list of all quick replies in the quick reply set.480 * Gets a list of all quick replies in the quick reply set.
453 *481 *
454 * @param {String} setName name of the existing quick reply set482 * @param {string} setName name of the existing quick reply set
455 * @returns array with the labels of this set's quick replies483 * @returns array with the labels of this set's quick replies
456 */484 */
457 listQuickReplies(setName) {485 listQuickReplies(setName) {
public/scripts/extensions/quick-reply/html/qrEditor.html+38 -8
@@ -2,10 +2,23 @@
2 <div id="qr--main">2 <div id="qr--main">
3 <h3 data-i18n="Labels and Message">Labels and Message</h3>3 <h3 data-i18n="Labels and Message">Labels and Message</h3>
4 <div class="qr--labels">4 <div class="qr--labels">
5 <label>5 <label class="qr--fit">
6 <span class="qr--labelText" data-i18n="Label">Label</span>6 <span class="qr--labelText" data-i18n="Label">Icon</span>
7 <input type="text" class="text_pole" id="qr--modal-label">7 <small class="qr--labelHint">&nbsp;</small>
8 <div class="menu_button fa-fw" id="qr--modal-icon" title="Click to change icon"></div>
8 </label>9 </label>
10 <div class="label">
11 <span class="qr--labelText" data-i18n="Label">Label</span>
12 <small class="qr--labelHint" data-i18n="(label of the button, if no icon is chosen) ">(label of the button, if no icon is chosen)</small>
13 <div class="qr--inputGroup">
14 <label class="checkbox_label" title="Show label even if an icon is assigned">
15 <input type="checkbox" id="qr--modal-showLabel">
16 Show
17 </label>
18 <input type="text" class="text_pole" id="qr--modal-label">
19 <div class="menu_button fa-fw fa-solid fa-chevron-down" id="qr--modal-switcher" title="Switch to another QR"></div>
20 </div>
21 </div>
9 <label>22 <label>
10 <span class="qr--labelText" data-i18n="Title">Title</span>23 <span class="qr--labelText" data-i18n="Title">Title</span>
11 <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>24 <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>
@@ -33,6 +46,8 @@
33 <input type="checkbox" id="qr--modal-syntax">46 <input type="checkbox" id="qr--modal-syntax">
34 <span>Syntax highlight</span>47 <span>Syntax highlight</span>
35 </label>48 </label>
49 <small>Ctrl+Alt+Click (or F9) to set / remove breakpoints</small>
50 <small>Ctrl+<span id="qr--modal-commentKey"></span> to toggle block comments</small>
36 </div>51 </div>
37 <div id="qr--modal-messageHolder">52 <div id="qr--modal-messageHolder">
38 <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>53 <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
@@ -43,6 +58,10 @@
4358
4459
4560
61 <div id="qr--resizeHandle"></div>
62
63
64
46 <div id="qr--qrOptions">65 <div id="qr--qrOptions">
47 <h3 data-i18n="Context Menu">Context Menu</h3>66 <h3 data-i18n="Context Menu">Context Menu</h3>
48 <div id="qr--ctxEditor">67 <div id="qr--ctxEditor">
@@ -64,7 +83,7 @@
6483
6584
66 <h3 data-i18n="Auto-Execute">Auto-Execute</h3>85 <h3 data-i18n="Auto-Execute">Auto-Execute</h3>
67 <div class="flex-container flexFlowColumn">86 <div id="qr--autoExec" class="flex-container flexFlowColumn">
68 <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">87 <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
69 <input type="checkbox" id="qr--preventAutoExecute" >88 <input type="checkbox" id="qr--preventAutoExecute" >
70 <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>89 <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>
@@ -90,6 +109,10 @@
90 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>109 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>
91 </label>110 </label>
92 <label class="checkbox_label">111 <label class="checkbox_label">
112 <input type="checkbox" id="qr--executeOnNewChat">
113 <span><i class="fa-solid fa-fw fa-comments"></i><span data-i18n="Execute on new chat">Execute on new chat</span></span>
114 </label>
115 <label class="checkbox_label">
93 <input type="checkbox" id="qr--executeOnGroupMemberDraft">116 <input type="checkbox" id="qr--executeOnGroupMemberDraft">
94 <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>117 <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>
95 </label>118 </label>
@@ -117,11 +140,18 @@
117 </div>140 </div>
118 </div>141 </div>
119 <div id="qr--modal-executeProgress"></div>142 <div id="qr--modal-executeProgress"></div>
120 <label class="checkbox_label">
121 <input type="checkbox" id="qr--modal-executeHide">
122 <span title="Hide editor while executing"> Hide editor while executing</span>
123 </label>
124 <div id="qr--modal-executeErrors"></div>143 <div id="qr--modal-executeErrors"></div>
125 <div id="qr--modal-executeResult"></div>144 <div id="qr--modal-executeResult"></div>
145
146 <div id="qr--modal-debugButtons">
147 <div title="Resume" id="qr--modal-resume" class="qr--modal-debugButton menu_button"></div>
148 <div title="Step Over" id="qr--modal-step" class="qr--modal-debugButton menu_button"></div>
149 <div title="Step Into" id="qr--modal-stepInto" class="qr--modal-debugButton menu_button"></div>
150 <div title="Step Out" id="qr--modal-stepOut" class="qr--modal-debugButton menu_button"></div>
151 <div title="Minimize" id="qr--modal-minimize" class="qr--modal-debugButton menu_button fa-solid fa-minimize"></div>
152 <div title="Maximize" id="qr--modal-maximize" class="qr--modal-debugButton menu_button fa-solid fa-maximize"></div>
153 </div>
154 <textarea rows="1" id="qr--modal-send_textarea" placeholder="Chat input for use with {{input}}" title="Chat input for use with {{input}}"></textarea>
155 <div id="qr--modal-debugState"></div>
126 </div>156 </div>
127</div>157</div>
public/scripts/extensions/quick-reply/html/settings.html+10 -0
@@ -60,10 +60,20 @@
60 <label class="flex-container" id="qr--injectInputContainer">60 <label class="flex-container" id="qr--injectInputContainer">
61 <input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>61 <input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>
62 </label>62 </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>
63 </div>71 </div>
64 <div id="qr--set-qrList" class="qr--qrList"></div>72 <div id="qr--set-qrList" class="qr--qrList"></div>
65 <div class="qr--set-qrListActions">73 <div class="qr--set-qrListActions">
66 <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>74 <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>
67 </div>77 </div>
68 </div>78 </div>
69 </div>79 </div>
public/scripts/extensions/quick-reply/index.js+8 -2
@@ -105,6 +105,7 @@ const loadSets = async () => {
105 qr.executeOnAi = slot.autoExecute_botMessage ?? false;105 qr.executeOnAi = slot.autoExecute_botMessage ?? false;
106 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;106 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
107 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;107 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
108 qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
108 qr.automationId = slot.automationId ?? '';109 qr.automationId = slot.automationId ?? '';
109 qr.contextList = (slot.contextMenu ?? []).map(it=>({110 qr.contextList = (slot.contextMenu ?? []).map(it=>({
110 set: it.preset,111 set: it.preset,
@@ -176,7 +177,7 @@ const init = async () => {
176 buttons.show();177 buttons.show();
177 settings.onSave = ()=>buttons.refresh();178 settings.onSave = ()=>buttons.refresh();
178179
179 window['executeQuickReplyByName'] = async(name, args = {}) => {180 window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
180 let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]181 let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
181 .map(it=>it.set.qrList)182 .map(it=>it.set.qrList)
182 .flat()183 .flat()
@@ -191,7 +192,7 @@ const init = async () => {
191 }192 }
192 }193 }
193 if (qr && qr.onExecute) {194 if (qr && qr.onExecute) {
194 return await qr.execute(args, false, true);195 return await qr.execute(args, false, true, options);
195 } else {196 } else {
196 throw new Error(`No Quick Reply found for "${name}".`);197 throw new Error(`No Quick Reply found for "${name}".`);
197 }198 }
@@ -260,3 +261,8 @@ const onWIActivation = async (entries) => {
260 await autoExec.handleWIActivation(entries);261 await autoExec.handleWIActivation(entries);
261};262};
262eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));263eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));
264
265const onNewChat = async () => {
266 await autoExec.handleNewChat();
267};
268eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
public/scripts/extensions/quick-reply/lib/morphdom-esm.js+769 -0
@@ -0,0 +1,769 @@
1var DOCUMENT_FRAGMENT_NODE = 11;
2
3function morphAttrs(fromNode, toNode) {
4 var toNodeAttrs = toNode.attributes;
5 var attr;
6 var attrName;
7 var attrNamespaceURI;
8 var attrValue;
9 var fromValue;
10
11 // document-fragments dont have attributes so lets not do anything
12 if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
13 return;
14 }
15
16 // update attributes on original DOM element
17 for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
18 attr = toNodeAttrs[i];
19 attrName = attr.name;
20 attrNamespaceURI = attr.namespaceURI;
21 attrValue = attr.value;
22
23 if (attrNamespaceURI) {
24 attrName = attr.localName || attrName;
25 fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
26
27 if (fromValue !== attrValue) {
28 if (attr.prefix === 'xmlns'){
29 attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
30 }
31 fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
32 }
33 } else {
34 fromValue = fromNode.getAttribute(attrName);
35
36 if (fromValue !== attrValue) {
37 fromNode.setAttribute(attrName, attrValue);
38 }
39 }
40 }
41
42 // Remove any extra attributes found on the original DOM element that
43 // weren't found on the target element.
44 var fromNodeAttrs = fromNode.attributes;
45
46 for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
47 attr = fromNodeAttrs[d];
48 attrName = attr.name;
49 attrNamespaceURI = attr.namespaceURI;
50
51 if (attrNamespaceURI) {
52 attrName = attr.localName || attrName;
53
54 if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
55 fromNode.removeAttributeNS(attrNamespaceURI, attrName);
56 }
57 } else {
58 if (!toNode.hasAttribute(attrName)) {
59 fromNode.removeAttribute(attrName);
60 }
61 }
62 }
63}
64
65var range; // Create a range object for efficently rendering strings to elements.
66var NS_XHTML = 'http://www.w3.org/1999/xhtml';
67
68var doc = typeof document === 'undefined' ? undefined : document;
69var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
70var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
71
72function createFragmentFromTemplate(str) {
73 var template = doc.createElement('template');
74 template.innerHTML = str;
75 return template.content.childNodes[0];
76}
77
78function createFragmentFromRange(str) {
79 if (!range) {
80 range = doc.createRange();
81 range.selectNode(doc.body);
82 }
83
84 var fragment = range.createContextualFragment(str);
85 return fragment.childNodes[0];
86}
87
88function createFragmentFromWrap(str) {
89 var fragment = doc.createElement('body');
90 fragment.innerHTML = str;
91 return fragment.childNodes[0];
92}
93
94/**
95 * This is about the same
96 * var html = new DOMParser().parseFromString(str, 'text/html');
97 * return html.body.firstChild;
98 *
99 * @method toElement
100 * @param {String} str
101 */
102function toElement(str) {
103 str = str.trim();
104 if (HAS_TEMPLATE_SUPPORT) {
105 // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
106 // createContextualFragment doesn't support
107 // <template> support not available in IE
108 return createFragmentFromTemplate(str);
109 } else if (HAS_RANGE_SUPPORT) {
110 return createFragmentFromRange(str);
111 }
112
113 return createFragmentFromWrap(str);
114}
115
116/**
117 * Returns true if two node's names are the same.
118 *
119 * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
120 * nodeName and different namespace URIs.
121 *
122 * @param {Element} a
123 * @param {Element} b The target element
124 * @return {boolean}
125 */
126function compareNodeNames(fromEl, toEl) {
127 var fromNodeName = fromEl.nodeName;
128 var toNodeName = toEl.nodeName;
129 var fromCodeStart, toCodeStart;
130
131 if (fromNodeName === toNodeName) {
132 return true;
133 }
134
135 fromCodeStart = fromNodeName.charCodeAt(0);
136 toCodeStart = toNodeName.charCodeAt(0);
137
138 // If the target element is a virtual DOM node or SVG node then we may
139 // need to normalize the tag name before comparing. Normal HTML elements that are
140 // in the "http://www.w3.org/1999/xhtml"
141 // are converted to upper case
142 if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
143 return fromNodeName === toNodeName.toUpperCase();
144 } else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
145 return toNodeName === fromNodeName.toUpperCase();
146 } else {
147 return false;
148 }
149}
150
151/**
152 * Create an element, optionally with a known namespace URI.
153 *
154 * @param {string} name the element name, e.g. 'div' or 'svg'
155 * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
156 * its `xmlns` attribute or its inferred namespace.
157 *
158 * @return {Element}
159 */
160function createElementNS(name, namespaceURI) {
161 return !namespaceURI || namespaceURI === NS_XHTML ?
162 doc.createElement(name) :
163 doc.createElementNS(namespaceURI, name);
164}
165
166/**
167 * Copies the children of one DOM element to another DOM element
168 */
169function moveChildren(fromEl, toEl) {
170 var curChild = fromEl.firstChild;
171 while (curChild) {
172 var nextChild = curChild.nextSibling;
173 toEl.appendChild(curChild);
174 curChild = nextChild;
175 }
176 return toEl;
177}
178
179function syncBooleanAttrProp(fromEl, toEl, name) {
180 if (fromEl[name] !== toEl[name]) {
181 fromEl[name] = toEl[name];
182 if (fromEl[name]) {
183 fromEl.setAttribute(name, '');
184 } else {
185 fromEl.removeAttribute(name);
186 }
187 }
188}
189
190var specialElHandlers = {
191 OPTION: function(fromEl, toEl) {
192 var parentNode = fromEl.parentNode;
193 if (parentNode) {
194 var parentName = parentNode.nodeName.toUpperCase();
195 if (parentName === 'OPTGROUP') {
196 parentNode = parentNode.parentNode;
197 parentName = parentNode && parentNode.nodeName.toUpperCase();
198 }
199 if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
200 if (fromEl.hasAttribute('selected') && !toEl.selected) {
201 // Workaround for MS Edge bug where the 'selected' attribute can only be
202 // removed if set to a non-empty value:
203 // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
204 fromEl.setAttribute('selected', 'selected');
205 fromEl.removeAttribute('selected');
206 }
207 // We have to reset select element's selectedIndex to -1, otherwise setting
208 // fromEl.selected using the syncBooleanAttrProp below has no effect.
209 // The correct selectedIndex will be set in the SELECT special handler below.
210 parentNode.selectedIndex = -1;
211 }
212 }
213 syncBooleanAttrProp(fromEl, toEl, 'selected');
214 },
215 /**
216 * The "value" attribute is special for the <input> element since it sets
217 * the initial value. Changing the "value" attribute without changing the
218 * "value" property will have no effect since it is only used to the set the
219 * initial value. Similar for the "checked" attribute, and "disabled".
220 */
221 INPUT: function(fromEl, toEl) {
222 syncBooleanAttrProp(fromEl, toEl, 'checked');
223 syncBooleanAttrProp(fromEl, toEl, 'disabled');
224
225 if (fromEl.value !== toEl.value) {
226 fromEl.value = toEl.value;
227 }
228
229 if (!toEl.hasAttribute('value')) {
230 fromEl.removeAttribute('value');
231 }
232 },
233
234 TEXTAREA: function(fromEl, toEl) {
235 var newValue = toEl.value;
236 if (fromEl.value !== newValue) {
237 fromEl.value = newValue;
238 }
239
240 var firstChild = fromEl.firstChild;
241 if (firstChild) {
242 // Needed for IE. Apparently IE sets the placeholder as the
243 // node value and vise versa. This ignores an empty update.
244 var oldValue = firstChild.nodeValue;
245
246 if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
247 return;
248 }
249
250 firstChild.nodeValue = newValue;
251 }
252 },
253 SELECT: function(fromEl, toEl) {
254 if (!toEl.hasAttribute('multiple')) {
255 var selectedIndex = -1;
256 var i = 0;
257 // We have to loop through children of fromEl, not toEl since nodes can be moved
258 // from toEl to fromEl directly when morphing.
259 // At the time this special handler is invoked, all children have already been morphed
260 // and appended to / removed from fromEl, so using fromEl here is safe and correct.
261 var curChild = fromEl.firstChild;
262 var optgroup;
263 var nodeName;
264 while(curChild) {
265 nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
266 if (nodeName === 'OPTGROUP') {
267 optgroup = curChild;
268 curChild = optgroup.firstChild;
269 } else {
270 if (nodeName === 'OPTION') {
271 if (curChild.hasAttribute('selected')) {
272 selectedIndex = i;
273 break;
274 }
275 i++;
276 }
277 curChild = curChild.nextSibling;
278 if (!curChild && optgroup) {
279 curChild = optgroup.nextSibling;
280 optgroup = null;
281 }
282 }
283 }
284
285 fromEl.selectedIndex = selectedIndex;
286 }
287 }
288};
289
290var ELEMENT_NODE = 1;
291var DOCUMENT_FRAGMENT_NODE$1 = 11;
292var TEXT_NODE = 3;
293var COMMENT_NODE = 8;
294
295function noop() {}
296
297function defaultGetNodeKey(node) {
298 if (node) {
299 return (node.getAttribute && node.getAttribute('id')) || node.id;
300 }
301}
302
303function morphdomFactory(morphAttrs) {
304
305 return function morphdom(fromNode, toNode, options) {
306 if (!options) {
307 options = {};
308 }
309
310 if (typeof toNode === 'string') {
311 if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') {
312 var toNodeHtml = toNode;
313 toNode = doc.createElement('html');
314 toNode.innerHTML = toNodeHtml;
315 } else {
316 toNode = toElement(toNode);
317 }
318 } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
319 toNode = toNode.firstElementChild;
320 }
321
322 var getNodeKey = options.getNodeKey || defaultGetNodeKey;
323 var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
324 var onNodeAdded = options.onNodeAdded || noop;
325 var onBeforeElUpdated = options.onBeforeElUpdated || noop;
326 var onElUpdated = options.onElUpdated || noop;
327 var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
328 var onNodeDiscarded = options.onNodeDiscarded || noop;
329 var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
330 var skipFromChildren = options.skipFromChildren || noop;
331 var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); };
332 var childrenOnly = options.childrenOnly === true;
333
334 // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
335 var fromNodesLookup = Object.create(null);
336 var keyedRemovalList = [];
337
338 function addKeyedRemoval(key) {
339 keyedRemovalList.push(key);
340 }
341
342 function walkDiscardedChildNodes(node, skipKeyedNodes) {
343 if (node.nodeType === ELEMENT_NODE) {
344 var curChild = node.firstChild;
345 while (curChild) {
346
347 var key = undefined;
348
349 if (skipKeyedNodes && (key = getNodeKey(curChild))) {
350 // If we are skipping keyed nodes then we add the key
351 // to a list so that it can be handled at the very end.
352 addKeyedRemoval(key);
353 } else {
354 // Only report the node as discarded if it is not keyed. We do this because
355 // at the end we loop through all keyed elements that were unmatched
356 // and then discard them in one final pass.
357 onNodeDiscarded(curChild);
358 if (curChild.firstChild) {
359 walkDiscardedChildNodes(curChild, skipKeyedNodes);
360 }
361 }
362
363 curChild = curChild.nextSibling;
364 }
365 }
366 }
367
368 /**
369 * Removes a DOM node out of the original DOM
370 *
371 * @param {Node} node The node to remove
372 * @param {Node} parentNode The nodes parent
373 * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
374 * @return {undefined}
375 */
376 function removeNode(node, parentNode, skipKeyedNodes) {
377 if (onBeforeNodeDiscarded(node) === false) {
378 return;
379 }
380
381 if (parentNode) {
382 parentNode.removeChild(node);
383 }
384
385 onNodeDiscarded(node);
386 walkDiscardedChildNodes(node, skipKeyedNodes);
387 }
388
389 // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
390 // function indexTree(root) {
391 // var treeWalker = document.createTreeWalker(
392 // root,
393 // NodeFilter.SHOW_ELEMENT);
394 //
395 // var el;
396 // while((el = treeWalker.nextNode())) {
397 // var key = getNodeKey(el);
398 // if (key) {
399 // fromNodesLookup[key] = el;
400 // }
401 // }
402 // }
403
404 // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
405 //
406 // function indexTree(node) {
407 // var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
408 // var el;
409 // while((el = nodeIterator.nextNode())) {
410 // var key = getNodeKey(el);
411 // if (key) {
412 // fromNodesLookup[key] = el;
413 // }
414 // }
415 // }
416
417 function indexTree(node) {
418 if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
419 var curChild = node.firstChild;
420 while (curChild) {
421 var key = getNodeKey(curChild);
422 if (key) {
423 fromNodesLookup[key] = curChild;
424 }
425
426 // Walk recursively
427 indexTree(curChild);
428
429 curChild = curChild.nextSibling;
430 }
431 }
432 }
433
434 indexTree(fromNode);
435
436 function handleNodeAdded(el) {
437 onNodeAdded(el);
438
439 var curChild = el.firstChild;
440 while (curChild) {
441 var nextSibling = curChild.nextSibling;
442
443 var key = getNodeKey(curChild);
444 if (key) {
445 var unmatchedFromEl = fromNodesLookup[key];
446 // if we find a duplicate #id node in cache, replace `el` with cache value
447 // and morph it to the child node.
448 if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
449 curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
450 morphEl(unmatchedFromEl, curChild);
451 } else {
452 handleNodeAdded(curChild);
453 }
454 } else {
455 // recursively call for curChild and it's children to see if we find something in
456 // fromNodesLookup
457 handleNodeAdded(curChild);
458 }
459
460 curChild = nextSibling;
461 }
462 }
463
464 function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
465 // We have processed all of the "to nodes". If curFromNodeChild is
466 // non-null then we still have some from nodes left over that need
467 // to be removed
468 while (curFromNodeChild) {
469 var fromNextSibling = curFromNodeChild.nextSibling;
470 if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
471 // Since the node is keyed it might be matched up later so we defer
472 // the actual removal to later
473 addKeyedRemoval(curFromNodeKey);
474 } else {
475 // NOTE: we skip nested keyed nodes from being removed since there is
476 // still a chance they will be matched up later
477 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
478 }
479 curFromNodeChild = fromNextSibling;
480 }
481 }
482
483 function morphEl(fromEl, toEl, childrenOnly) {
484 var toElKey = getNodeKey(toEl);
485
486 if (toElKey) {
487 // If an element with an ID is being morphed then it will be in the final
488 // DOM so clear it out of the saved elements collection
489 delete fromNodesLookup[toElKey];
490 }
491
492 if (!childrenOnly) {
493 // optional
494 var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);
495 if (beforeUpdateResult === false) {
496 return;
497 } else if (beforeUpdateResult instanceof HTMLElement) {
498 fromEl = beforeUpdateResult;
499 // reindex the new fromEl in case it's not in the same
500 // tree as the original fromEl
501 // (Phoenix LiveView sometimes returns a cloned tree,
502 // but keyed lookups would still point to the original tree)
503 indexTree(fromEl);
504 }
505
506 // update attributes on original DOM element first
507 morphAttrs(fromEl, toEl);
508 // optional
509 onElUpdated(fromEl);
510
511 if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
512 return;
513 }
514 }
515
516 if (fromEl.nodeName !== 'TEXTAREA') {
517 morphChildren(fromEl, toEl);
518 } else {
519 specialElHandlers.TEXTAREA(fromEl, toEl);
520 }
521 }
522
523 function morphChildren(fromEl, toEl) {
524 var skipFrom = skipFromChildren(fromEl, toEl);
525 var curToNodeChild = toEl.firstChild;
526 var curFromNodeChild = fromEl.firstChild;
527 var curToNodeKey;
528 var curFromNodeKey;
529
530 var fromNextSibling;
531 var toNextSibling;
532 var matchingFromEl;
533
534 // walk the children
535 outer: while (curToNodeChild) {
536 toNextSibling = curToNodeChild.nextSibling;
537 curToNodeKey = getNodeKey(curToNodeChild);
538
539 // walk the fromNode children all the way through
540 while (!skipFrom && curFromNodeChild) {
541 fromNextSibling = curFromNodeChild.nextSibling;
542
543 if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
544 curToNodeChild = toNextSibling;
545 curFromNodeChild = fromNextSibling;
546 continue outer;
547 }
548
549 curFromNodeKey = getNodeKey(curFromNodeChild);
550
551 var curFromNodeType = curFromNodeChild.nodeType;
552
553 // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
554 var isCompatible = undefined;
555
556 if (curFromNodeType === curToNodeChild.nodeType) {
557 if (curFromNodeType === ELEMENT_NODE) {
558 // Both nodes being compared are Element nodes
559
560 if (curToNodeKey) {
561 // The target node has a key so we want to match it up with the correct element
562 // in the original DOM tree
563 if (curToNodeKey !== curFromNodeKey) {
564 // The current element in the original DOM tree does not have a matching key so
565 // let's check our lookup to see if there is a matching element in the original
566 // DOM tree
567 if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
568 if (fromNextSibling === matchingFromEl) {
569 // Special case for single element removals. To avoid removing the original
570 // DOM node out of the tree (since that can break CSS transitions, etc.),
571 // we will instead discard the current node and wait until the next
572 // iteration to properly match up the keyed target element with its matching
573 // element in the original tree
574 isCompatible = false;
575 } else {
576 // We found a matching keyed element somewhere in the original DOM tree.
577 // Let's move the original DOM node into the current position and morph
578 // it.
579
580 // NOTE: We use insertBefore instead of replaceChild because we want to go through
581 // the `removeNode()` function for the node that is being discarded so that
582 // all lifecycle hooks are correctly invoked
583 fromEl.insertBefore(matchingFromEl, curFromNodeChild);
584
585 // fromNextSibling = curFromNodeChild.nextSibling;
586
587 if (curFromNodeKey) {
588 // Since the node is keyed it might be matched up later so we defer
589 // the actual removal to later
590 addKeyedRemoval(curFromNodeKey);
591 } else {
592 // NOTE: we skip nested keyed nodes from being removed since there is
593 // still a chance they will be matched up later
594 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
595 }
596
597 curFromNodeChild = matchingFromEl;
598 curFromNodeKey = getNodeKey(curFromNodeChild);
599 }
600 } else {
601 // The nodes are not compatible since the "to" node has a key and there
602 // is no matching keyed node in the source tree
603 isCompatible = false;
604 }
605 }
606 } else if (curFromNodeKey) {
607 // The original has a key
608 isCompatible = false;
609 }
610
611 isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
612 if (isCompatible) {
613 // We found compatible DOM elements so transform
614 // the current "from" node to match the current
615 // target DOM node.
616 // MORPH
617 morphEl(curFromNodeChild, curToNodeChild);
618 }
619
620 } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
621 // Both nodes being compared are Text or Comment nodes
622 isCompatible = true;
623 // Simply update nodeValue on the original node to
624 // change the text value
625 if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
626 curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
627 }
628
629 }
630 }
631
632 if (isCompatible) {
633 // Advance both the "to" child and the "from" child since we found a match
634 // Nothing else to do as we already recursively called morphChildren above
635 curToNodeChild = toNextSibling;
636 curFromNodeChild = fromNextSibling;
637 continue outer;
638 }
639
640 // No compatible match so remove the old node from the DOM and continue trying to find a
641 // match in the original DOM. However, we only do this if the from node is not keyed
642 // since it is possible that a keyed node might match up with a node somewhere else in the
643 // target tree and we don't want to discard it just yet since it still might find a
644 // home in the final DOM tree. After everything is done we will remove any keyed nodes
645 // that didn't find a home
646 if (curFromNodeKey) {
647 // Since the node is keyed it might be matched up later so we defer
648 // the actual removal to later
649 addKeyedRemoval(curFromNodeKey);
650 } else {
651 // NOTE: we skip nested keyed nodes from being removed since there is
652 // still a chance they will be matched up later
653 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
654 }
655
656 curFromNodeChild = fromNextSibling;
657 } // END: while(curFromNodeChild) {}
658
659 // If we got this far then we did not find a candidate match for
660 // our "to node" and we exhausted all of the children "from"
661 // nodes. Therefore, we will just append the current "to" node
662 // to the end
663 if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
664 // MORPH
665 if(!skipFrom){ addChild(fromEl, matchingFromEl); }
666 morphEl(matchingFromEl, curToNodeChild);
667 } else {
668 var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
669 if (onBeforeNodeAddedResult !== false) {
670 if (onBeforeNodeAddedResult) {
671 curToNodeChild = onBeforeNodeAddedResult;
672 }
673
674 if (curToNodeChild.actualize) {
675 curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
676 }
677 addChild(fromEl, curToNodeChild);
678 handleNodeAdded(curToNodeChild);
679 }
680 }
681
682 curToNodeChild = toNextSibling;
683 curFromNodeChild = fromNextSibling;
684 }
685
686 cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
687
688 var specialElHandler = specialElHandlers[fromEl.nodeName];
689 if (specialElHandler) {
690 specialElHandler(fromEl, toEl);
691 }
692 } // END: morphChildren(...)
693
694 var morphedNode = fromNode;
695 var morphedNodeType = morphedNode.nodeType;
696 var toNodeType = toNode.nodeType;
697
698 if (!childrenOnly) {
699 // Handle the case where we are given two DOM nodes that are not
700 // compatible (e.g. <div> --> <span> or <div> --> TEXT)
701 if (morphedNodeType === ELEMENT_NODE) {
702 if (toNodeType === ELEMENT_NODE) {
703 if (!compareNodeNames(fromNode, toNode)) {
704 onNodeDiscarded(fromNode);
705 morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
706 }
707 } else {
708 // Going from an element node to a text node
709 morphedNode = toNode;
710 }
711 } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
712 if (toNodeType === morphedNodeType) {
713 if (morphedNode.nodeValue !== toNode.nodeValue) {
714 morphedNode.nodeValue = toNode.nodeValue;
715 }
716
717 return morphedNode;
718 } else {
719 // Text node to something else
720 morphedNode = toNode;
721 }
722 }
723 }
724
725 if (morphedNode === toNode) {
726 // The "to node" was not compatible with the "from node" so we had to
727 // toss out the "from node" and use the "to node"
728 onNodeDiscarded(fromNode);
729 } else {
730 if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
731 return;
732 }
733
734 morphEl(morphedNode, toNode, childrenOnly);
735
736 // We now need to loop over any keyed nodes that might need to be
737 // removed. We only do the removal if we know that the keyed node
738 // never found a match. When a keyed node is matched up we remove
739 // it out of fromNodesLookup and we use fromNodesLookup to determine
740 // if a keyed node has been matched up or not
741 if (keyedRemovalList) {
742 for (var i=0, len=keyedRemovalList.length; i<len; i++) {
743 var elToRemove = fromNodesLookup[keyedRemovalList[i]];
744 if (elToRemove) {
745 removeNode(elToRemove, elToRemove.parentNode, false);
746 }
747 }
748 }
749 }
750
751 if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
752 if (morphedNode.actualize) {
753 morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
754 }
755 // If we had to swap out the from node with a new node because the old
756 // node was not compatible with the target node then we need to
757 // replace the old DOM node in the original DOM tree. This is only
758 // possible if the original DOM node was part of a DOM tree which
759 // we know is the case if it has a parent node.
760 fromNode.parentNode.replaceChild(morphedNode, fromNode);
761 }
762
763 return morphedNode;
764 };
765}
766
767var morphdom = morphdomFactory(morphAttrs);
768
769export default morphdom;
public/scripts/extensions/quick-reply/lib/morphdom.LICENSE.txt+21 -0
@@ -0,0 +1,21 @@
1The MIT License (MIT)
2
3Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com)
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in
13all copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21THE SOFTWARE.
\ No newline at end of file21 \ No newline at end of file
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+9 -0
@@ -83,6 +83,15 @@ export class AutoExecuteHandler {
83 await this.performAutoExecute(qrList);83 await this.performAutoExecute(qrList);
84 }84 }
8585
86 async handleNewChat() {
87 if (!this.checkExecute()) return;
88 const qrList = [
89 ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat)).flat(),
90 ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat))?.flat() ?? []),
91 ];
92 await this.performAutoExecute(qrList);
93 }
94
86 /**95 /**
87 * @param {any[]} entries Set of activated entries96 * @param {any[]} entries Set of activated entries
88 */97 */
public/scripts/extensions/quick-reply/src/QuickReply.js+1285 -87
@@ -1,10 +1,17 @@
1import { POPUP_TYPE, Popup } from '../../../popup.js';1import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js';
2import { setSlashCommandAutoComplete } from '../../../slash-commands.js';2import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
3import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';3import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
4import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js';
5import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
6import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js';
7import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
8import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js';
9import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
4import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';10import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';11import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { debounce, getSortableDelay } from '../../../utils.js';12import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
7import { log, warn } from '../index.js';13import { log, quickReplyApi, warn } from '../index.js';
14import morphdom from '../lib/morphdom-esm.js';
8import { QuickReplyContextLink } from './QuickReplyContextLink.js';15import { QuickReplyContextLink } from './QuickReplyContextLink.js';
9import { QuickReplySet } from './QuickReplySet.js';16import { QuickReplySet } from './QuickReplySet.js';
10import { ContextMenu } from './ui/ctx/ContextMenu.js';17import { ContextMenu } from './ui/ctx/ContextMenu.js';
@@ -21,48 +28,62 @@ export class QuickReply {
2128
2229
2330
24 /**@type {Number}*/ id;31 /**@type {number}*/ id;
25 /**@type {String}*/ label = '';32 /**@type {string}*/ icon;
26 /**@type {String}*/ title = '';33 /**@type {string}*/ label = '';
27 /**@type {String}*/ message = '';34 /**@type {boolean}*/ showLabel = false;
35 /**@type {string}*/ title = '';
36 /**@type {string}*/ message = '';
2837
29 /**@type {QuickReplyContextLink[]}*/ contextList;38 /**@type {QuickReplyContextLink[]}*/ contextList;
3039
31 /**@type {Boolean}*/ preventAutoExecute = true;40 /**@type {boolean}*/ preventAutoExecute = true;
32 /**@type {Boolean}*/ isHidden = false;41 /**@type {boolean}*/ isHidden = false;
33 /**@type {Boolean}*/ executeOnStartup = false;42 /**@type {boolean}*/ executeOnStartup = false;
34 /**@type {Boolean}*/ executeOnUser = false;43 /**@type {boolean}*/ executeOnUser = false;
35 /**@type {Boolean}*/ executeOnAi = false;44 /**@type {boolean}*/ executeOnAi = false;
36 /**@type {Boolean}*/ executeOnChatChange = false;45 /**@type {boolean}*/ executeOnChatChange = false;
37 /**@type {Boolean}*/ executeOnGroupMemberDraft = false;46 /**@type {boolean}*/ executeOnGroupMemberDraft = false;
38 /**@type {String}*/ automationId = '';47 /**@type {boolean}*/ executeOnNewChat = false;
48 /**@type {string}*/ automationId = '';
3949
40 /**@type {Function}*/ onExecute;50 /**@type {function}*/ onExecute;
41 /**@type {Function}*/ onDelete;51 /**@type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>}*/ onDebug;
42 /**@type {Function}*/ onUpdate;52 /**@type {function}*/ onDelete;
53 /**@type {function}*/ onUpdate;
54 /**@type {function}*/ onInsertBefore;
55 /**@type {function}*/ onTransfer;
4356
4457
45 /**@type {HTMLElement}*/ dom;58 /**@type {HTMLElement}*/ dom;
59 /**@type {HTMLElement}*/ domIcon;
46 /**@type {HTMLElement}*/ domLabel;60 /**@type {HTMLElement}*/ domLabel;
47 /**@type {HTMLElement}*/ settingsDom;61 /**@type {HTMLElement}*/ settingsDom;
62 /**@type {HTMLElement}*/ settingsDomIcon;
48 /**@type {HTMLInputElement}*/ settingsDomLabel;63 /**@type {HTMLInputElement}*/ settingsDomLabel;
49 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;64 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;
5065
51 /**@type {Popup}*/ editorPopup;66 /**@type {Popup}*/ editorPopup;
67 /**@type {HTMLElement}*/ editorDom;
5268
69 /**@type {HTMLTextAreaElement}*/ editorMessage;
70 /**@type {HTMLTextAreaElement}*/ editorMessageLabel;
71 /**@type {HTMLElement}*/ editorSyntax;
53 /**@type {HTMLElement}*/ editorExecuteBtn;72 /**@type {HTMLElement}*/ editorExecuteBtn;
54 /**@type {HTMLElement}*/ editorExecuteBtnPause;73 /**@type {HTMLElement}*/ editorExecuteBtnPause;
55 /**@type {HTMLElement}*/ editorExecuteBtnStop;74 /**@type {HTMLElement}*/ editorExecuteBtnStop;
56 /**@type {HTMLElement}*/ editorExecuteProgress;75 /**@type {HTMLElement}*/ editorExecuteProgress;
57 /**@type {HTMLElement}*/ editorExecuteErrors;76 /**@type {HTMLElement}*/ editorExecuteErrors;
58 /**@type {HTMLElement}*/ editorExecuteResult;77 /**@type {HTMLElement}*/ editorExecuteResult;
59 /**@type {HTMLInputElement}*/ editorExecuteHide;78 /**@type {HTMLElement}*/ editorDebugState;
60 /**@type {Promise}*/ editorExecutePromise;79 /**@type {Promise}*/ editorExecutePromise;
80 /**@type {boolean}*/ isExecuting;
61 /**@type {SlashCommandAbortController}*/ abortController;81 /**@type {SlashCommandAbortController}*/ abortController;
82 /**@type {SlashCommandDebugController}*/ debugController;
6283
6384
64 get hasContext() {85 get hasContext() {
65 return this.contextList && this.contextList.length > 0;86 return this.contextList && this.contextList.filter(it => it.set).length > 0;
66 }87 }
6788
6889
@@ -75,6 +96,14 @@ export class QuickReply {
75 updateRender() {96 updateRender() {
76 if (!this.dom) return;97 if (!this.dom) return;
77 this.dom.title = this.title || this.message;98 this.dom.title = this.title || this.message;
99 if (this.icon) {
100 this.domIcon.classList.remove('qr--hidden');
101 if (this.showLabel) this.domLabel.classList.remove('qr--hidden');
102 else this.domLabel.classList.add('qr--hidden');
103 } else {
104 this.domIcon.classList.add('qr--hidden');
105 this.domLabel.classList.remove('qr--hidden');
106 }
78 this.domLabel.textContent = this.label;107 this.domLabel.textContent = this.label;
79 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');108 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');
80 }109 }
@@ -105,9 +134,18 @@ export class QuickReply {
105 }134 }
106 this.execute();135 this.execute();
107 });136 });
137 const icon = document.createElement('div'); {
138 this.domIcon = icon;
139 icon.classList.add('qr--button-icon');
140 icon.classList.add('fa-solid');
141 if (!this.icon) icon.classList.add('qr--hidden');
142 else icon.classList.add(this.icon);
143 root.append(icon);
144 }
108 const lbl = document.createElement('div'); {145 const lbl = document.createElement('div'); {
109 this.domLabel = lbl;146 this.domLabel = lbl;
110 lbl.classList.add('qr--button-label');147 lbl.classList.add('qr--button-label');
148 if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
111 lbl.textContent = this.label;149 lbl.textContent = this.label;
112 root.append(lbl);150 root.append(lbl);
113 }151 }
@@ -138,36 +176,115 @@ export class QuickReply {
138 item.classList.add('qr--set-item');176 item.classList.add('qr--set-item');
139 item.setAttribute('data-order', String(idx));177 item.setAttribute('data-order', String(idx));
140 item.setAttribute('data-id', String(this.id));178 item.setAttribute('data-id', String(this.id));
141 const drag = document.createElement('div'); {179 const adder = document.createElement('div'); {
142 drag.classList.add('drag-handle');180 adder.classList.add('qr--set-itemAdder');
143 drag.classList.add('ui-sortable-handle');181 const actions = document.createElement('div'); {
144 drag.textContent = '☰';182 actions.classList.add('qr--actions');
145 item.append(drag);183 const addNew = document.createElement('div'); {
146 }184 addNew.classList.add('qr--action');
147 const lblContainer = document.createElement('div'); {185 addNew.classList.add('qr--add');
148 lblContainer.classList.add('qr--set-itemLabelContainer');186 addNew.classList.add('menu_button');
149 const lbl = document.createElement('input'); {187 addNew.classList.add('menu_button_icon');
150 this.settingsDomLabel = lbl;188 addNew.classList.add('fa-solid');
151 lbl.classList.add('qr--set-itemLabel');189 addNew.classList.add('fa-plus');
152 lbl.classList.add('text_pole');190 addNew.title = 'Add quick reply';
153 lbl.value = this.label;191 addNew.addEventListener('click', ()=>this.onInsertBefore());
154 lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));192 actions.append(addNew);
155 lblContainer.append(lbl);193 }
194 const paste = document.createElement('div'); {
195 paste.classList.add('qr--action');
196 paste.classList.add('qr--paste');
197 paste.classList.add('menu_button');
198 paste.classList.add('menu_button_icon');
199 paste.classList.add('fa-solid');
200 paste.classList.add('fa-paste');
201 paste.title = 'Add quick reply from clipboard';
202 paste.addEventListener('click', async()=>{
203 const text = await navigator.clipboard.readText();
204 this.onInsertBefore(text);
205 });
206 actions.append(paste);
207 }
208 const importFile = document.createElement('div'); {
209 importFile.classList.add('qr--action');
210 importFile.classList.add('qr--importFile');
211 importFile.classList.add('menu_button');
212 importFile.classList.add('menu_button_icon');
213 importFile.classList.add('fa-solid');
214 importFile.classList.add('fa-file-import');
215 importFile.title = 'Add quick reply from JSON file';
216 importFile.addEventListener('click', async()=>{
217 const inp = document.createElement('input'); {
218 inp.type = 'file';
219 inp.accept = '.json';
220 inp.addEventListener('change', async()=>{
221 if (inp.files.length > 0) {
222 for (const file of inp.files) {
223 const text = await file.text();
224 this.onInsertBefore(text);
225 }
226 }
227 });
228 inp.click();
229 }
230 });
231 actions.append(importFile);
232 }
233 adder.append(actions);
234 }
235 item.append(adder);
236 }
237 const itemContent = document.createElement('div'); {
238 itemContent.classList.add('qr--content');
239 const drag = document.createElement('div'); {
240 drag.classList.add('drag-handle');
241 drag.classList.add('ui-sortable-handle');
242 drag.textContent = '☰';
243 itemContent.append(drag);
244 }
245 const lblContainer = document.createElement('div'); {
246 lblContainer.classList.add('qr--set-itemLabelContainer');
247 const icon = document.createElement('div'); {
248 this.settingsDomIcon = icon;
249 icon.title = 'Click to change icon';
250 icon.classList.add('qr--set-itemIcon');
251 icon.classList.add('menu_button');
252 icon.classList.add('fa-fw');
253 if (this.icon) {
254 icon.classList.add('fa-solid');
255 icon.classList.add(this.icon);
256 }
257 icon.addEventListener('click', async()=>{
258 let value = await showFontAwesomePicker();
259 this.updateIcon(value);
260 });
261 lblContainer.append(icon);
262 }
263 const lbl = document.createElement('input'); {
264 this.settingsDomLabel = lbl;
265 lbl.classList.add('qr--set-itemLabel');
266 lbl.classList.add('text_pole');
267 lbl.value = this.label;
268 lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
269 lblContainer.append(lbl);
270 }
271 itemContent.append(lblContainer);
156 }272 }
157 item.append(lblContainer);273 item.append(itemContent);
158 }274 }
159 const optContainer = document.createElement('div'); {275 const optContainer = document.createElement('div'); {
160 optContainer.classList.add('qr--set-optionsContainer');276 optContainer.classList.add('qr--set-optionsContainer');
161 const opt = document.createElement('div'); {277 const opt = document.createElement('div'); {
162 opt.classList.add('qr--action');278 opt.classList.add('qr--action');
163 opt.classList.add('menu_button');279 opt.classList.add('menu_button');
280 opt.classList.add('fa-fw');
164 opt.classList.add('fa-solid');281 opt.classList.add('fa-solid');
165 opt.textContent = '⁝';282 opt.textContent = '⁝';
166 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';283 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';
167 opt.addEventListener('click', ()=>this.showEditor());284 opt.addEventListener('click', ()=>this.showEditor());
168 optContainer.append(opt);285 optContainer.append(opt);
169 }286 }
170 item.append(optContainer);287 itemContent.append(optContainer);
171 }288 }
172 const mes = document.createElement('textarea'); {289 const mes = document.createElement('textarea'); {
173 this.settingsDomMessage = mes;290 this.settingsDomMessage = mes;
@@ -176,22 +293,89 @@ export class QuickReply {
176 mes.value = this.message;293 mes.value = this.message;
177 //HACK need to use jQuery to catch the triggered event from the expanded editor294 //HACK need to use jQuery to catch the triggered event from the expanded editor
178 $(mes).on('input', ()=>this.updateMessage(mes.value));295 $(mes).on('input', ()=>this.updateMessage(mes.value));
179 item.append(mes);296 itemContent.append(mes);
180 }297 }
181 const actions = document.createElement('div'); {298 const actions = document.createElement('div'); {
182 actions.classList.add('qr--actions');299 actions.classList.add('qr--actions');
300 const move = document.createElement('div'); {
301 move.classList.add('qr--action');
302 move.classList.add('menu_button');
303 move.classList.add('fa-fw');
304 move.classList.add('fa-solid');
305 move.classList.add('fa-truck-arrow-right');
306 move.title = 'Move quick reply to other set';
307 move.addEventListener('click', ()=>this.onTransfer(this));
308 actions.append(move);
309 }
310 const copy = document.createElement('div'); {
311 copy.classList.add('qr--action');
312 copy.classList.add('menu_button');
313 copy.classList.add('fa-fw');
314 copy.classList.add('fa-solid');
315 copy.classList.add('fa-copy');
316 copy.title = 'Copy quick reply to clipboard';
317 copy.addEventListener('click', async()=>{
318 await navigator.clipboard.writeText(JSON.stringify(this));
319 copy.classList.add('qr--success');
320 await delay(3010);
321 copy.classList.remove('qr--success');
322 });
323 actions.append(copy);
324 }
325 const cut = document.createElement('div'); {
326 cut.classList.add('qr--action');
327 cut.classList.add('menu_button');
328 cut.classList.add('fa-fw');
329 cut.classList.add('fa-solid');
330 cut.classList.add('fa-cut');
331 cut.title = 'Cut quick reply to clipboard (copy and remove)';
332 cut.addEventListener('click', async()=>{
333 await navigator.clipboard.writeText(JSON.stringify(this));
334 this.delete();
335 });
336 actions.append(cut);
337 }
338 const exp = document.createElement('div'); {
339 exp.classList.add('qr--action');
340 exp.classList.add('menu_button');
341 exp.classList.add('fa-fw');
342 exp.classList.add('fa-solid');
343 exp.classList.add('fa-file-export');
344 exp.title = 'Export quick reply as file';
345 exp.addEventListener('click', ()=>{
346 const blob = new Blob([JSON.stringify(this)], { type:'text' });
347 const url = URL.createObjectURL(blob);
348 const a = document.createElement('a'); {
349 a.href = url;
350 a.download = `${this.label}.qr.json`;
351 a.click();
352 }
353 });
354 actions.append(exp);
355 }
183 const del = document.createElement('div'); {356 const del = document.createElement('div'); {
184 del.classList.add('qr--action');357 del.classList.add('qr--action');
185 del.classList.add('menu_button');358 del.classList.add('menu_button');
186 del.classList.add('menu_button_icon');359 del.classList.add('fa-fw');
187 del.classList.add('fa-solid');360 del.classList.add('fa-solid');
188 del.classList.add('fa-trash-can');361 del.classList.add('fa-trash-can');
189 del.classList.add('redWarningBG');362 del.classList.add('redWarningBG');
190 del.title = 'Remove quick reply';363 del.title = 'Remove Quick Reply\n---\nShit+Click to skip confirmation';
191 del.addEventListener('click', ()=>this.delete());364 del.addEventListener('click', async(evt)=>{
365 if (!evt.shiftKey) {
366 const result = await Popup.show.confirm(
367 'Remove Quick Reply',
368 'Are you sure you want to remove this Quick Reply?',
369 );
370 if (result != POPUP_RESULT.AFFIRMATIVE) {
371 return;
372 }
373 }
374 this.delete();
375 });
192 actions.append(del);376 actions.append(del);
193 }377 }
194 item.append(actions);378 itemContent.append(actions);
195 }379 }
196 }380 }
197 }381 }
@@ -208,22 +392,156 @@ export class QuickReply {
208 /**@type {HTMLElement} */392 /**@type {HTMLElement} */
209 // @ts-ignore393 // @ts-ignore
210 const dom = this.template.cloneNode(true);394 const dom = this.template.cloneNode(true);
395 this.editorDom = dom;
211 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });396 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
212 const popupResult = this.editorPopup.show();397 const popupResult = this.editorPopup.show();
213398
214 // basics399 // basics
400 /**@type {HTMLElement}*/
401 const icon = dom.querySelector('#qr--modal-icon');
402 if (this.icon) {
403 icon.classList.add('fa-solid');
404 icon.classList.add(this.icon);
405 }
406 else {
407 icon.textContent = '…';
408 }
409 icon.addEventListener('click', async()=>{
410 let value = await showFontAwesomePicker();
411 if (value === null) return;
412 if (this.icon) icon.classList.remove(this.icon);
413 if (value == '') {
414 icon.classList.remove('fa-solid');
415 icon.textContent = '…';
416 } else {
417 icon.textContent = '';
418 icon.classList.add('fa-solid');
419 icon.classList.add(value);
420 }
421 this.updateIcon(value);
422 });
423 /**@type {HTMLInputElement}*/
424 const showLabel = dom.querySelector('#qr--modal-showLabel');
425 showLabel.checked = this.showLabel;
426 showLabel.addEventListener('click', ()=>{
427 this.updateShowLabel(showLabel.checked);
428 });
215 /**@type {HTMLInputElement}*/429 /**@type {HTMLInputElement}*/
216 const label = dom.querySelector('#qr--modal-label');430 const label = dom.querySelector('#qr--modal-label');
217 label.value = this.label;431 label.value = this.label;
218 label.addEventListener('input', ()=>{432 label.addEventListener('input', ()=>{
219 this.updateLabel(label.value);433 this.updateLabel(label.value);
220 });434 });
435 let switcherList;
436 dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{
437 if (switcherList) {
438 switcherList.remove();
439 switcherList = null;
440 return;
441 }
442 const list = document.createElement('ul'); {
443 switcherList = list;
444 list.classList.add('qr--modal-switcherList');
445 const makeList = (qrs)=>{
446 const setItem = document.createElement('li'); {
447 setItem.classList.add('qr--modal-switcherItem');
448 setItem.addEventListener('click', ()=>{
449 list.innerHTML = '';
450 for (const qrs of quickReplyApi.listSets()) {
451 const item = document.createElement('li'); {
452 item.classList.add('qr--modal-switcherItem');
453 item.addEventListener('click', ()=>{
454 list.innerHTML = '';
455 makeList(quickReplyApi.getSetByName(qrs));
456 });
457 const lbl = document.createElement('div'); {
458 lbl.classList.add('qr--label');
459 lbl.textContent = qrs;
460 item.append(lbl);
461 }
462 list.append(item);
463 }
464 }
465 });
466 const lbl = document.createElement('div'); {
467 lbl.classList.add('qr--label');
468 const icon = document.createElement('i'); {
469 icon.classList.add('fa-solid');
470 icon.classList.add('fa-arrow-alt-circle-right');
471 icon.classList.add('menu_button');
472 lbl.append(icon);
473 }
474 const text = document.createElement('span'); {
475 text.textContent = 'Switch QR Sets...';
476 lbl.append(text);
477 }
478 setItem.append(lbl);
479 }
480 list.append(setItem);
481 }
482 const addItem = document.createElement('li'); {
483 addItem.classList.add('qr--modal-switcherItem');
484 addItem.addEventListener('click', ()=>{
485 const qr = quickReplyApi.getSetByQr(this).addQuickReply();
486 this.editorPopup.completeAffirmative();
487 qr.showEditor();
488 });
489 const lbl = document.createElement('div'); {
490 lbl.classList.add('qr--label');
491 const icon = document.createElement('i'); {
492 icon.classList.add('fa-solid');
493 icon.classList.add('fa-plus');
494 icon.classList.add('menu_button');
495 lbl.append(icon);
496 }
497 const text = document.createElement('span'); {
498 text.textContent = 'Add QR';
499 lbl.append(text);
500 }
501 addItem.append(lbl);
502 }
503 list.append(addItem);
504 }
505 for (const qr of qrs.qrList.toSorted((a,b)=>a.label.toLowerCase().localeCompare(b.label.toLowerCase()))) {
506 const item = document.createElement('li'); {
507 item.classList.add('qr--modal-switcherItem');
508 if (qr == this) item.classList.add('qr--current');
509 else item.addEventListener('click', ()=>{
510 this.editorPopup.completeAffirmative();
511 qr.showEditor();
512 });
513 const lbl = document.createElement('div'); {
514 lbl.classList.add('qr--label');
515 lbl.textContent = qr.label;
516 item.append(lbl);
517 }
518 const id = document.createElement('div'); {
519 id.classList.add('qr--id');
520 id.textContent = qr.id.toString();
521 item.append(id);
522 }
523 const mes = document.createElement('div'); {
524 mes.classList.add('qr--message');
525 mes.textContent = qr.message;
526 item.append(mes);
527 }
528 list.append(item);
529 }
530 }
531 };
532 makeList(quickReplyApi.getSetByQr(this));
533 }
534 label.parentElement.append(list);
535 });
221 /**@type {HTMLInputElement}*/536 /**@type {HTMLInputElement}*/
222 const title = dom.querySelector('#qr--modal-title');537 const title = dom.querySelector('#qr--modal-title');
223 title.value = this.title;538 title.value = this.title;
224 title.addEventListener('input', () => {539 title.addEventListener('input', () => {
225 this.updateTitle(title.value);540 this.updateTitle(title.value);
226 });541 });
542 /**@type {HTMLElement}*/
543 const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
544 this.editorSyntax = messageSyntaxInner;
227 /**@type {HTMLInputElement}*/545 /**@type {HTMLInputElement}*/
228 const wrap = dom.querySelector('#qr--modal-wrap');546 const wrap = dom.querySelector('#qr--modal-wrap');
229 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');547 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');
@@ -235,9 +553,15 @@ export class QuickReply {
235 if (wrap.checked) {553 if (wrap.checked) {
236 message.style.whiteSpace = 'pre-wrap';554 message.style.whiteSpace = 'pre-wrap';
237 messageSyntaxInner.style.whiteSpace = 'pre-wrap';555 messageSyntaxInner.style.whiteSpace = 'pre-wrap';
556 if (this.clone) {
557 this.clone.style.whiteSpace = 'pre-wrap';
558 }
238 } else {559 } else {
239 message.style.whiteSpace = 'pre';560 message.style.whiteSpace = 'pre';
240 messageSyntaxInner.style.whiteSpace = 'pre';561 messageSyntaxInner.style.whiteSpace = 'pre';
562 if (this.clone) {
563 this.clone.style.whiteSpace = 'pre';
564 }
241 }565 }
242 updateScrollDebounced();566 updateScrollDebounced();
243 };567 };
@@ -261,11 +585,8 @@ export class QuickReply {
261 });585 });
262 };586 };
263 const updateScrollDebounced = updateScroll;587 const updateScrollDebounced = updateScroll;
264 const updateSyntax = ()=>{
265 messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
266 };
267 const updateSyntaxEnabled = ()=>{588 const updateSyntaxEnabled = ()=>{
268 if (JSON.parse(localStorage.getItem('qr--syntax'))) {589 if (syntax.checked) {
269 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');590 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
270 } else {591 } else {
271 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');592 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
@@ -296,48 +617,108 @@ export class QuickReply {
296 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));617 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
297 updateSyntaxEnabled();618 updateSyntaxEnabled();
298 });619 });
620 if (navigator.keyboard) {
621 navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash'));
622 } else {
623 dom.querySelector('#qr--modal-commentKey').closest('small').remove();
624 }
625 this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]');
299 /**@type {HTMLTextAreaElement}*/626 /**@type {HTMLTextAreaElement}*/
300 const message = dom.querySelector('#qr--modal-message');627 const message = dom.querySelector('#qr--modal-message');
628 this.editorMessage = message;
301 message.value = this.message;629 message.value = this.message;
630 const updateMessageDebounced = debounce((value)=>this.updateMessage(value), 10);
302 message.addEventListener('input', () => {631 message.addEventListener('input', () => {
303 updateSyntax();632 updateMessageDebounced(message.value);
304 this.updateMessage(message.value);
305 updateScrollDebounced();633 updateScrollDebounced();
306 });634 }, { passive:true });
307 setSlashCommandAutoComplete(message, true);635 const getLineStart = ()=>{
308 //TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize636 const start = message.selectionStart;
637 const end = message.selectionEnd;
638 let lineStart;
639 if (start == 0 || message.value[start - 1] == '\n') {
640 // cursor is already at beginning of line
641 // -> keep start
642 lineStart = start;
643 } else {
644 // cursor is at end of line or somewhere in the line
645 // -> find last newline before cursor and start after that
646 lineStart = message.value.lastIndexOf('\n', start - 1) + 1;
647 }
648 return lineStart;
649 };
309 message.addEventListener('keydown', async(evt) => {650 message.addEventListener('keydown', async(evt) => {
651 if (this.isExecuting) return;
310 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {652 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
653 // increase indent
311 evt.preventDefault();654 evt.preventDefault();
312 const start = message.selectionStart;655 const start = message.selectionStart;
313 const end = message.selectionEnd;656 const end = message.selectionEnd;
314 if (end - start > 0 && message.value.substring(start, end).includes('\n')) {657 if (end - start > 0 && message.value.substring(start, end).includes('\n')) {
315 const lineStart = message.value.lastIndexOf('\n', start);658 evt.stopImmediatePropagation();
316 const count = message.value.substring(lineStart, end).split('\n').length - 1;659 evt.stopPropagation();
317 message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;660 const lineStart = getLineStart();
318 message.selectionStart = start + 1;661 message.selectionStart = lineStart;
319 message.selectionEnd = end + count;662 const affectedLines = message.value.substring(lineStart, end).split('\n');
320 updateSyntax();663 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
321 } else {664 document.execCommand('insertText', false, `\t${affectedLines.join('\n\t')}`);
322 message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
323 message.selectionStart = start + 1;665 message.selectionStart = start + 1;
324 message.selectionEnd = end + 1;666 message.selectionEnd = end + affectedLines.length;
325 updateSyntax();667 message.dispatchEvent(new Event('input', { bubbles:true }));
668 } else if (!(ac.isReplaceable && ac.isActive)) {
669 evt.stopImmediatePropagation();
670 evt.stopPropagation();
671 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
672 document.execCommand('insertText', false, '\t');
673 message.dispatchEvent(new Event('input', { bubbles:true }));
326 }674 }
327 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {675 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
676 // decrease indent
328 evt.preventDefault();677 evt.preventDefault();
678 evt.stopImmediatePropagation();
679 evt.stopPropagation();
329 const start = message.selectionStart;680 const start = message.selectionStart;
330 const end = message.selectionEnd;681 const end = message.selectionEnd;
331 const lineStart = message.value.lastIndexOf('\n', start);682 const lineStart = getLineStart();
332 const count = message.value.substring(lineStart, end).split('\n\t').length - 1;683 message.selectionStart = lineStart;
333 message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;684 const affectedLines = message.value.substring(lineStart, end).split('\n');
334 message.selectionStart = start - 1;685 const newText = affectedLines.map(it=>it.replace(/^\t/, '')).join('\n');
335 message.selectionEnd = end - count;686 const delta = affectedLines.join('\n').length - newText.length;
336 updateSyntax();687 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
688 if (delta > 0) {
689 if (newText == '') {
690 document.execCommand('delete', false);
691 } else {
692 document.execCommand('insertText', false, newText);
693 }
694 message.selectionStart = start - (affectedLines[0].startsWith('\t') ? 1 : 0);
695 message.selectionEnd = end - delta;
696 message.dispatchEvent(new Event('input', { bubbles:true }));
697 } else {
698 message.selectionStart = start;
699 }
700 } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) {
701 // new line, keep indent
702 const start = message.selectionStart;
703 const end = message.selectionEnd;
704 let lineStart = getLineStart();
705 const indent = /^([^\S\n]*)/.exec(message.value.slice(lineStart))[1] ?? '';
706 if (indent.length) {
707 evt.stopImmediatePropagation();
708 evt.stopPropagation();
709 evt.preventDefault();
710 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
711 document.execCommand('insertText', false, `\n${indent}`);
712 message.selectionStart = start + 1 + indent.length;
713 message.selectionEnd = message.selectionStart;
714 message.dispatchEvent(new Event('input', { bubbles:true }));
715 }
337 } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {716 } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
338 evt.stopPropagation();
339 evt.preventDefault();
340 if (executeShortcut.checked) {717 if (executeShortcut.checked) {
718 // execute QR
719 evt.stopImmediatePropagation();
720 evt.stopPropagation();
721 evt.preventDefault();
341 const selectionStart = message.selectionStart;722 const selectionStart = message.selectionStart;
342 const selectionEnd = message.selectionEnd;723 const selectionEnd = message.selectionEnd;
343 message.blur();724 message.blur();
@@ -348,17 +729,175 @@ export class QuickReply {
348 message.selectionEnd = selectionEnd;729 message.selectionEnd = selectionEnd;
349 }730 }
350 }731 }
732 } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
733 // toggle breakpoint
734 evt.stopImmediatePropagation();
735 evt.stopPropagation();
736 evt.preventDefault();
737 preBreakPointStart = message.selectionStart;
738 preBreakPointEnd = message.selectionEnd;
739 toggleBreakpoint();
740 } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
741 // toggle block comment
742 // (evt.code will use the same physical key on the keyboard across different keyboard layouts)
743 evt.stopImmediatePropagation();
744 evt.stopPropagation();
745 evt.preventDefault();
746 // check if we are inside a comment -> uncomment
747 const parser = new SlashCommandParser();
748 parser.parse(message.value, false);
749 const start = message.selectionStart;
750 const end = message.selectionEnd;
751 const comment = parser.commandIndex.findLast(it=>it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end));
752 if (comment) {
753 // uncomment
754 let content = message.value.slice(comment.start + 1, comment.end - 1);
755 let len = content.length;
756 content = content.replace(/^ /, '');
757 const offsetStart = len - content.length;
758 len = content.length;
759 content = content.replace(/ $/, '');
760 const offsetEnd = len - content.length;
761 message.selectionStart = comment.start - 1;
762 message.selectionEnd = comment.end + 1;
763 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
764 document.execCommand('insertText', false, content);
765 message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0);
766 message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0);
767 } else {
768 // comment
769 const lineStart = getLineStart();
770 const lineEnd = message.value.indexOf('\n', end);
771 message.selectionStart = lineStart;
772 message.selectionEnd = lineEnd;
773 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
774 document.execCommand('insertText', false, `/* ${message.value.slice(lineStart, lineEnd)} *|`);
775 message.selectionStart = start + 3;
776 message.selectionEnd = end + 3;
777 }
778 message.dispatchEvent(new Event('input', { bubbles:true }));
351 }779 }
352 });780 });
781 const ac = await setSlashCommandAutoComplete(message, true);
353 message.addEventListener('wheel', (evt)=>{782 message.addEventListener('wheel', (evt)=>{
354 updateScrollDebounced(evt);783 updateScrollDebounced(evt);
355 });784 });
356 message.addEventListener('scroll', (evt)=>{785 message.addEventListener('scroll', (evt)=>{
357 updateScrollDebounced();786 updateScrollDebounced();
358 });787 });
788 let preBreakPointStart;
789 let preBreakPointEnd;
790 /**
791 * @param {SlashCommandBreakPoint} bp
792 */
793 const removeBreakpoint = (bp)=>{
794 // start at -1 because "/" is not included in start-end
795 let start = bp.start - 1;
796 // step left until forward slash "/"
797 while (message.value[start] != '/') start--;
798 // step left while whitespace (except newline) before start
799 while (/[^\S\n]/.test(message.value[start - 1])) start--;
800 // if newline before indent, include the newline for removal
801 if (message.value[start - 1] == '\n') start--;
802 let end = bp.end;
803 // step right while whitespace
804 while (/\s/.test(message.value[end])) end++;
805 // if pipe after whitepace, include pipe for removal
806 if (message.value[end] == '|') end++;
807 message.selectionStart = start;
808 message.selectionEnd = end;
809 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
810 document.execCommand('insertText', false, '');
811 message.dispatchEvent(new Event('input', { bubbles:true }));
812 let postStart = preBreakPointStart;
813 let postEnd = preBreakPointEnd;
814 // set caret back to where it was
815 if (preBreakPointStart <= start) {
816 // selection start was before breakpoint: do nothing
817 } else if (preBreakPointStart > start && preBreakPointEnd < end) {
818 // selection start was inside breakpoint: move to index before breakpoint
819 postStart = start;
820 } else if (preBreakPointStart >= end) {
821 // selection start was behind breakpoint: move back by length of removed string
822 postStart = preBreakPointStart - (end - start);
823 }
824 if (preBreakPointEnd <= start) {
825 // do nothing
826 } else if (preBreakPointEnd > start && preBreakPointEnd < end) {
827 // selection end was inside breakpoint: move to index before breakpoint
828 postEnd = start;
829 } else if (preBreakPointEnd >= end) {
830 // selection end was behind breakpoint: move back by length of removed string
831 postEnd = preBreakPointEnd - (end - start);
832 }
833 return { start:postStart, end:postEnd };
834 };
835 /**
836 * @param {SlashCommandExecutor} cmd
837 */
838 const addBreakpoint = (cmd)=>{
839 // start at -1 because "/" is not included in start-end
840 let start = cmd.start - 1;
841 let indent = '';
842 // step left until forward slash "/"
843 while (message.value[start] != '/') start--;
844 // step left while whitespace (except newline) before start, collect the whitespace to help build indentation
845 while (/[^\S\n]/.test(message.value[start - 1])) {
846 start--;
847 indent += message.value[start];
848 }
849 // if newline before indent, include the newline
850 if (message.value[start - 1] == '\n') {
851 start--;
852 indent = `\n${indent}`;
853 }
854 const breakpointText = `${indent}/breakpoint |`;
855 message.selectionStart = start;
856 message.selectionEnd = start;
857 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
858 document.execCommand('insertText', false, breakpointText);
859 message.dispatchEvent(new Event('input', { bubbles:true }));
860 return breakpointText.length;
861 };
862 const toggleBreakpoint = ()=>{
863 const idx = message.selectionStart;
864 let postStart = preBreakPointStart;
865 let postEnd = preBreakPointEnd;
866 const parser = new SlashCommandParser();
867 parser.parse(message.value, false);
868 const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx);
869 if (cmdIdx > -1) {
870 const cmd = parser.commandIndex[cmdIdx];
871 if (cmd instanceof SlashCommandBreakPoint) {
872 const bp = cmd;
873 const { start, end } = removeBreakpoint(bp);
874 postStart = start;
875 postEnd = end;
876 } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) {
877 const bp = parser.commandIndex[cmdIdx - 1];
878 const { start, end } = removeBreakpoint(bp);
879 postStart = start;
880 postEnd = end;
881 } else {
882 const len = addBreakpoint(cmd);
883 postStart += len;
884 postEnd += len;
885 }
886 message.selectionStart = postStart;
887 message.selectionEnd = postEnd;
888 }
889 };
890 message.addEventListener('pointerdown', (evt)=>{
891 if (!evt.ctrlKey || !evt.altKey) return;
892 preBreakPointStart = message.selectionStart;
893 preBreakPointEnd = message.selectionEnd;
894 });
895 message.addEventListener('pointerup', async(evt)=>{
896 if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return;
897 toggleBreakpoint();
898 });
359 /** @type {any} */899 /** @type {any} */
360 const resizeListener = debounce((evt) => {900 const resizeListener = debounce((evt) => {
361 updateSyntax();
362 updateScrollDebounced(evt);901 updateScrollDebounced(evt);
363 if (document.activeElement == message) {902 if (document.activeElement == message) {
364 message.blur();903 message.blur();
@@ -367,10 +906,43 @@ export class QuickReply {
367 });906 });
368 window.addEventListener('resize', resizeListener);907 window.addEventListener('resize', resizeListener);
369 updateSyntaxEnabled();908 updateSyntaxEnabled();
909 const updateSyntax = ()=>{
910 if (messageSyntaxInner && syntax.checked) {
911 morphdom(
912 messageSyntaxInner,
913 `<div>${hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value}</div>`,
914 { childrenOnly: true },
915 );
916 updateScrollDebounced();
917 }
918 };
919 let lastSyntaxUpdate = 0;
920 const fpsTime = 1000 / 30;
921 let lastMessageValue = null;
922 let wasSyntax = null;
923 const updateSyntaxLoop = ()=>{
924 const now = Date.now();
925 // fps limit
926 if (now - lastSyntaxUpdate < fpsTime) return requestAnimationFrame(updateSyntaxLoop);
927 // elements don't exist (yet?)
928 if (!messageSyntaxInner || !message) return requestAnimationFrame(updateSyntaxLoop);
929 // elements no longer part of the document
930 if (!messageSyntaxInner.closest('body')) return;
931 // debugger is running
932 if (this.isExecuting) {
933 lastMessageValue = null;
934 return requestAnimationFrame(updateSyntaxLoop);
935 }
936 // value hasn't changed
937 if (wasSyntax == syntax.checked && lastMessageValue == message.value) return requestAnimationFrame(updateSyntaxLoop);
938 wasSyntax = syntax.checked;
939 lastSyntaxUpdate = now;
940 lastMessageValue = message.value;
941 updateSyntax();
942 requestAnimationFrame(updateSyntaxLoop);
943 };
944 requestAnimationFrame(()=>updateSyntaxLoop());
370 message.style.setProperty('text-shadow', 'none', 'important');945 message.style.setProperty('text-shadow', 'none', 'important');
371 /**@type {HTMLElement}*/
372 const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
373 updateSyntax();
374 updateWrap();946 updateWrap();
375 updateTabSize();947 updateTabSize();
376948
@@ -379,7 +951,7 @@ export class QuickReply {
379 const tpl = dom.querySelector('#qr--ctxItem');951 const tpl = dom.querySelector('#qr--ctxItem');
380 const linkList = dom.querySelector('#qr--ctxEditor');952 const linkList = dom.querySelector('#qr--ctxEditor');
381 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {953 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
382 [{ name: 'Select a QR set' }, ...QuickReplySet.list].forEach(qrs => {954 [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => {
383 const opt = document.createElement('option'); {955 const opt = document.createElement('option'); {
384 opt.value = qrs.name;956 opt.value = qrs.name;
385 opt.textContent = qrs.name;957 opt.textContent = qrs.name;
@@ -388,7 +960,7 @@ export class QuickReply {
388 }960 }
389 });961 });
390 };962 };
391 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Number}*/idx) => {963 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {number}*/idx) => {
392 /**@type {HTMLElement} */964 /**@type {HTMLElement} */
393 // @ts-ignore965 // @ts-ignore
394 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {966 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
@@ -490,6 +1062,13 @@ export class QuickReply {
490 this.updateContext();1062 this.updateContext();
491 });1063 });
492 /**@type {HTMLInputElement}*/1064 /**@type {HTMLInputElement}*/
1065 const executeOnNewChat = dom.querySelector('#qr--executeOnNewChat');
1066 executeOnNewChat.checked = this.executeOnNewChat;
1067 executeOnNewChat.addEventListener('click', ()=>{
1068 this.executeOnNewChat = executeOnNewChat.checked;
1069 this.updateContext();
1070 });
1071 /**@type {HTMLInputElement}*/
493 const automationId = dom.querySelector('#qr--automationId');1072 const automationId = dom.querySelector('#qr--automationId');
494 automationId.value = this.automationId;1073 automationId.value = this.automationId;
495 automationId.addEventListener('input', () => {1074 automationId.addEventListener('input', () => {
@@ -506,9 +1085,9 @@ export class QuickReply {
506 /**@type {HTMLElement}*/1085 /**@type {HTMLElement}*/
507 const executeResult = dom.querySelector('#qr--modal-executeResult');1086 const executeResult = dom.querySelector('#qr--modal-executeResult');
508 this.editorExecuteResult = executeResult;1087 this.editorExecuteResult = executeResult;
509 /**@type {HTMLInputElement}*/1088 /**@type {HTMLElement}*/
510 const executeHide = dom.querySelector('#qr--modal-executeHide');1089 const debugState = dom.querySelector('#qr--modal-debugState');
511 this.editorExecuteHide = executeHide;1090 this.editorDebugState = debugState;
512 /**@type {HTMLElement}*/1091 /**@type {HTMLElement}*/
513 const executeBtn = dom.querySelector('#qr--modal-execute');1092 const executeBtn = dom.querySelector('#qr--modal-execute');
514 this.editorExecuteBtn = executeBtn;1093 this.editorExecuteBtn = executeBtn;
@@ -536,6 +1115,76 @@ export class QuickReply {
536 this.abortController?.abort('Stop button clicked');1115 this.abortController?.abort('Stop button clicked');
537 });1116 });
5381117
1118 /**@type {HTMLTextAreaElement} */
1119 const inputOg = document.querySelector('#send_textarea');
1120 const inputMirror = dom.querySelector('#qr--modal-send_textarea');
1121 inputMirror.value = inputOg.value;
1122 const inputOgMo = new MutationObserver(muts=>{
1123 if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) {
1124 inputOg.removeEventListener('input', inputOgListener);
1125 }
1126 });
1127 inputOgMo.observe(document.body, { childList:true });
1128 const inputOgListener = ()=>{
1129 inputMirror.value = inputOg.value;
1130 };
1131 inputOg.addEventListener('input', inputOgListener);
1132 inputMirror.addEventListener('input', ()=>{
1133 inputOg.value = inputMirror.value;
1134 });
1135
1136 /**@type {HTMLElement}*/
1137 const resumeBtn = dom.querySelector('#qr--modal-resume');
1138 resumeBtn.addEventListener('click', ()=>{
1139 this.debugController?.resume();
1140 });
1141 /**@type {HTMLElement}*/
1142 const stepBtn = dom.querySelector('#qr--modal-step');
1143 stepBtn.addEventListener('click', ()=>{
1144 this.debugController?.step();
1145 });
1146 /**@type {HTMLElement}*/
1147 const stepIntoBtn = dom.querySelector('#qr--modal-stepInto');
1148 stepIntoBtn.addEventListener('click', ()=>{
1149 this.debugController?.stepInto();
1150 });
1151 /**@type {HTMLElement}*/
1152 const stepOutBtn = dom.querySelector('#qr--modal-stepOut');
1153 stepOutBtn.addEventListener('click', ()=>{
1154 this.debugController?.stepOut();
1155 });
1156 /**@type {HTMLElement}*/
1157 const minimizeBtn = dom.querySelector('#qr--modal-minimize');
1158 minimizeBtn.addEventListener('click', ()=>{
1159 this.editorDom.classList.add('qr--minimized');
1160 });
1161 const maximizeBtn = dom.querySelector('#qr--modal-maximize');
1162 maximizeBtn.addEventListener('click', ()=>{
1163 this.editorDom.classList.remove('qr--minimized');
1164 });
1165 /**@type {boolean}*/
1166 let isResizing = false;
1167 let resizeStart;
1168 let wStart;
1169 /**@type {HTMLElement}*/
1170 const resizeHandle = dom.querySelector('#qr--resizeHandle');
1171 resizeHandle.addEventListener('pointerdown', (evt)=>{
1172 if (isResizing) return;
1173 isResizing = true;
1174 evt.preventDefault();
1175 resizeStart = evt.x;
1176 wStart = dom.querySelector('#qr--qrOptions').offsetWidth;
1177 const dragListener = debounce((evt)=>{
1178 const w = wStart + resizeStart - evt.x;
1179 dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`);
1180 }, 5);
1181 window.addEventListener('pointerup', ()=>{
1182 window.removeEventListener('pointermove', dragListener);
1183 isResizing = false;
1184 }, { once:true });
1185 window.addEventListener('pointermove', dragListener);
1186 });
1187
539 await popupResult;1188 await popupResult;
5401189
541 window.removeEventListener('resize', resizeListener);1190 window.removeEventListener('resize', resizeListener);
@@ -544,8 +1193,62 @@ export class QuickReply {
544 }1193 }
545 }1194 }
5461195
1196 getEditorPosition(start, end, message = null) {
1197 const inputRect = this.editorMessage.getBoundingClientRect();
1198 const style = window.getComputedStyle(this.editorMessage);
1199 if (!this.clone) {
1200 this.clone = document.createElement('div');
1201 for (const key of style) {
1202 this.clone.style[key] = style[key];
1203 }
1204 this.clone.style.position = 'fixed';
1205 this.clone.style.visibility = 'hidden';
1206 const mo = new MutationObserver(muts=>{
1207 if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) {
1208 this.clone?.remove();
1209 this.clone = null;
1210 }
1211 });
1212 mo.observe(document.body, { childList:true });
1213 }
1214 document.body.append(this.clone);
1215 this.clone.style.width = `${inputRect.width}px`;
1216 this.clone.style.height = `${inputRect.height}px`;
1217 this.clone.style.left = `${inputRect.left}px`;
1218 this.clone.style.top = `${inputRect.top}px`;
1219 this.clone.style.whiteSpace = style.whiteSpace;
1220 this.clone.style.tabSize = style.tabSize;
1221 const text = message ?? this.editorMessage.value;
1222 const before = text.slice(0, start);
1223 this.clone.textContent = before;
1224 const locator = document.createElement('span');
1225 locator.textContent = text.slice(start, end);
1226 this.clone.append(locator);
1227 this.clone.append(text.slice(end));
1228 this.clone.scrollTop = this.editorSyntax.scrollTop;
1229 this.clone.scrollLeft = this.editorSyntax.scrollLeft;
1230 const locatorRect = locator.getBoundingClientRect();
1231 const bodyRect = document.body.getBoundingClientRect();
1232 const location = {
1233 left: locatorRect.left - bodyRect.left,
1234 right: locatorRect.right - bodyRect.left,
1235 top: locatorRect.top - bodyRect.top,
1236 bottom: locatorRect.bottom - bodyRect.top,
1237 };
1238 // this.clone.remove();
1239 return location;
1240 }
547 async executeFromEditor() {1241 async executeFromEditor() {
548 if (this.editorExecutePromise) return;1242 if (this.isExecuting) return;
1243 this.editorPopup.onClosing = ()=>false;
1244 const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/;
1245 const oText = this.message;
1246 this.isExecuting = true;
1247 this.editorDom.classList.add('qr--isExecuting');
1248 const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax');
1249 if (noSyntax) {
1250 this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
1251 }
549 this.editorExecuteBtn.classList.add('qr--busy');1252 this.editorExecuteBtn.classList.add('qr--busy');
550 this.editorExecuteProgress.style.setProperty('--prog', '0');1253 this.editorExecuteProgress.style.setProperty('--prog', '0');
551 this.editorExecuteErrors.classList.remove('qr--hasErrors');1254 this.editorExecuteErrors.classList.remove('qr--hasErrors');
@@ -556,12 +1259,451 @@ export class QuickReply {
556 this.editorExecuteProgress.classList.remove('qr--aborted');1259 this.editorExecuteProgress.classList.remove('qr--aborted');
557 this.editorExecuteErrors.innerHTML = '';1260 this.editorExecuteErrors.innerHTML = '';
558 this.editorExecuteResult.innerHTML = '';1261 this.editorExecuteResult.innerHTML = '';
559 if (this.editorExecuteHide.checked) {1262 const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner');
560 this.editorPopup.dlg.classList.add('qr--hide');1263 const updateScroll = (evt) => {
561 }1264 let left = syntax.scrollLeft;
1265 let top = syntax.scrollTop;
1266 if (evt) {
1267 evt.preventDefault();
1268 left = syntax.scrollLeft + evt.deltaX;
1269 top = syntax.scrollTop + evt.deltaY;
1270 syntax.scrollTo({
1271 behavior: 'instant',
1272 left,
1273 top,
1274 });
1275 }
1276 this.editorMessage.scrollTo({
1277 behavior: 'instant',
1278 left,
1279 top,
1280 });
1281 };
1282 const updateScrollDebounced = updateScroll;
1283 syntax.addEventListener('wheel', (evt)=>{
1284 updateScrollDebounced(evt);
1285 });
1286 syntax.addEventListener('scroll', (evt)=>{
1287 updateScrollDebounced();
1288 });
562 try {1289 try {
563 this.editorExecutePromise = this.execute({}, true);1290 this.abortController = new SlashCommandAbortController();
564 const result = await this.editorExecutePromise;1291 this.debugController = new SlashCommandDebugController();
1292 this.debugController.onBreakPoint = async(closure, executor)=>{
1293 this.editorDom.classList.add('qr--isPaused');
1294 syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
1295 this.editorMessageLabel.innerHTML = '';
1296 if (uuidCheck.test(closure.source)) {
1297 const p0 = document.createElement('span'); {
1298 p0.textContent = 'anonymous: ';
1299 this.editorMessageLabel.append(p0);
1300 }
1301 const p1 = document.createElement('strong'); {
1302 p1.textContent = executor.source.slice(0,5);
1303 this.editorMessageLabel.append(p1);
1304 }
1305 const p2 = document.createElement('span'); {
1306 p2.textContent = executor.source.slice(5, -5);
1307 this.editorMessageLabel.append(p2);
1308 }
1309 const p3 = document.createElement('strong'); {
1310 p3.textContent = executor.source.slice(-5);
1311 this.editorMessageLabel.append(p3);
1312 }
1313 } else {
1314 this.editorMessageLabel.textContent = executor.source;
1315 }
1316 const source = closure.source;
1317 this.editorDebugState.innerHTML = '';
1318 let ci = -1;
1319 const varNames = [];
1320 const macroNames = [];
1321 /**
1322 * @param {SlashCommandScope} scope
1323 */
1324 const buildVars = (scope, isCurrent = false)=>{
1325 if (!isCurrent) {
1326 ci--;
1327 }
1328 const c = this.debugController.stack.slice(ci)[0];
1329 const wrap = document.createElement('div'); {
1330 wrap.classList.add('qr--scope');
1331 if (isCurrent) {
1332 const executor = this.debugController.cmdStack.slice(-1)[0];
1333 { // named args
1334 const namedTitle = document.createElement('div'); {
1335 namedTitle.classList.add('qr--title');
1336 namedTitle.textContent = `Named Args - /${executor.name}`;
1337 if (executor.command.name == 'run') {
1338 namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1339 }
1340 wrap.append(namedTitle);
1341 }
1342 const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it=>it.name)]);
1343 for (const key of keys) {
1344 if (key[0] == '_') continue;
1345 const item = document.createElement('div'); {
1346 item.classList.add('qr--var');
1347 const k = document.createElement('div'); {
1348 k.classList.add('qr--key');
1349 k.textContent = key;
1350 item.append(k);
1351 }
1352 const vUnresolved = document.createElement('div'); {
1353 vUnresolved.classList.add('qr--val');
1354 vUnresolved.classList.add('qr--singleCol');
1355 const val = executor.namedArgumentList.find(it=>it.name == key)?.value;
1356 if (val instanceof SlashCommandClosure) {
1357 vUnresolved.classList.add('qr--closure');
1358 vUnresolved.title = val.rawText;
1359 vUnresolved.textContent = val.toString();
1360 } else if (val === undefined) {
1361 vUnresolved.classList.add('qr--undefined');
1362 vUnresolved.textContent = 'undefined';
1363 } else {
1364 let jsonVal;
1365 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1366 if (jsonVal && typeof jsonVal == 'object') {
1367 vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1368 } else {
1369 vUnresolved.textContent = val;
1370 vUnresolved.classList.add('qr--simple');
1371 }
1372 }
1373 item.append(vUnresolved);
1374 }
1375 const vResolved = document.createElement('div'); {
1376 vResolved.classList.add('qr--val');
1377 vResolved.classList.add('qr--singleCol');
1378 if (this.debugController.namedArguments === undefined) {
1379 vResolved.classList.add('qr--unresolved');
1380 } else {
1381 const val = this.debugController.namedArguments?.[key];
1382 if (val instanceof SlashCommandClosure) {
1383 vResolved.classList.add('qr--closure');
1384 vResolved.title = val.rawText;
1385 vResolved.textContent = val.toString();
1386 } else if (val === undefined) {
1387 vResolved.classList.add('qr--undefined');
1388 vResolved.textContent = 'undefined';
1389 } else {
1390 let jsonVal;
1391 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1392 if (jsonVal && typeof jsonVal == 'object') {
1393 vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1394 } else {
1395 vResolved.textContent = val;
1396 vResolved.classList.add('qr--simple');
1397 }
1398 }
1399 }
1400 item.append(vResolved);
1401 }
1402 wrap.append(item);
1403 }
1404 }
1405 }
1406 { // unnamed args
1407 const unnamedTitle = document.createElement('div'); {
1408 unnamedTitle.classList.add('qr--title');
1409 unnamedTitle.textContent = `Unnamed Args - /${executor.name}`;
1410 if (executor.command.name == 'run') {
1411 unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1412 }
1413 wrap.append(unnamedTitle);
1414 }
1415 let i = 0;
1416 let unnamed = this.debugController.unnamedArguments ?? [];
1417 if (!Array.isArray(unnamed)) unnamed = [unnamed];
1418 while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined);
1419 unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]);
1420 for (const arg of unnamed) {
1421 i++;
1422 const item = document.createElement('div'); {
1423 item.classList.add('qr--var');
1424 const k = document.createElement('div'); {
1425 k.classList.add('qr--key');
1426 k.textContent = i.toString();
1427 item.append(k);
1428 }
1429 const vUnresolved = document.createElement('div'); {
1430 vUnresolved.classList.add('qr--val');
1431 vUnresolved.classList.add('qr--singleCol');
1432 const val = arg[0]?.value;
1433 if (val instanceof SlashCommandClosure) {
1434 vUnresolved.classList.add('qr--closure');
1435 vUnresolved.title = val.rawText;
1436 vUnresolved.textContent = val.toString();
1437 } else if (val === undefined) {
1438 vUnresolved.classList.add('qr--undefined');
1439 vUnresolved.textContent = 'undefined';
1440 } else {
1441 let jsonVal;
1442 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1443 if (jsonVal && typeof jsonVal == 'object') {
1444 vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1445 } else {
1446 vUnresolved.textContent = val;
1447 vUnresolved.classList.add('qr--simple');
1448 }
1449 }
1450 item.append(vUnresolved);
1451 }
1452 const vResolved = document.createElement('div'); {
1453 vResolved.classList.add('qr--val');
1454 vResolved.classList.add('qr--singleCol');
1455 if (this.debugController.unnamedArguments === undefined) {
1456 vResolved.classList.add('qr--unresolved');
1457 } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) {
1458 // do nothing
1459 } else {
1460 const val = arg[1];
1461 if (val instanceof SlashCommandClosure) {
1462 vResolved.classList.add('qr--closure');
1463 vResolved.title = val.rawText;
1464 vResolved.textContent = val.toString();
1465 } else if (val === undefined) {
1466 vResolved.classList.add('qr--undefined');
1467 vResolved.textContent = 'undefined';
1468 } else {
1469 let jsonVal;
1470 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1471 if (jsonVal && typeof jsonVal == 'object') {
1472 vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1473 } else {
1474 vResolved.textContent = val;
1475 vResolved.classList.add('qr--simple');
1476 }
1477 }
1478 }
1479 item.append(vResolved);
1480 }
1481 wrap.append(item);
1482 }
1483 }
1484 }
1485 }
1486 // current scope
1487 const title = document.createElement('div'); {
1488 title.classList.add('qr--title');
1489 title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope';
1490 if (c.source == source) {
1491 let hi;
1492 title.addEventListener('pointerenter', ()=>{
1493 const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText);
1494 const layer = syntax.getBoundingClientRect();
1495 hi = document.createElement('div');
1496 hi.classList.add('qr--highlight-secondary');
1497 hi.style.left = `${loc.left - layer.left}px`;
1498 hi.style.width = `${loc.right - loc.left}px`;
1499 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1500 hi.style.height = `${loc.bottom - loc.top}px`;
1501 syntax.append(hi);
1502 });
1503 title.addEventListener('pointerleave', ()=>hi?.remove());
1504 }
1505 wrap.append(title);
1506 }
1507 for (const key of Object.keys(scope.variables)) {
1508 const isHidden = varNames.includes(key);
1509 if (!isHidden) varNames.push(key);
1510 const item = document.createElement('div'); {
1511 item.classList.add('qr--var');
1512 if (isHidden) item.classList.add('qr--isHidden');
1513 const k = document.createElement('div'); {
1514 k.classList.add('qr--key');
1515 k.textContent = key;
1516 item.append(k);
1517 }
1518 const v = document.createElement('div'); {
1519 v.classList.add('qr--val');
1520 const val = scope.variables[key];
1521 if (val instanceof SlashCommandClosure) {
1522 v.classList.add('qr--closure');
1523 v.title = val.rawText;
1524 v.textContent = val.toString();
1525 } else if (val === undefined) {
1526 v.classList.add('qr--undefined');
1527 v.textContent = 'undefined';
1528 } else {
1529 let jsonVal;
1530 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1531 if (jsonVal && typeof jsonVal == 'object') {
1532 v.textContent = JSON.stringify(jsonVal, null, 2);
1533 } else {
1534 v.textContent = val;
1535 v.classList.add('qr--simple');
1536 }
1537 }
1538 item.append(v);
1539 }
1540 wrap.append(item);
1541 }
1542 }
1543 for (const key of Object.keys(scope.macros)) {
1544 const isHidden = macroNames.includes(key);
1545 if (!isHidden) macroNames.push(key);
1546 const item = document.createElement('div'); {
1547 item.classList.add('qr--macro');
1548 if (isHidden) item.classList.add('qr--isHidden');
1549 const k = document.createElement('div'); {
1550 k.classList.add('qr--key');
1551 k.textContent = key;
1552 item.append(k);
1553 }
1554 const v = document.createElement('div'); {
1555 v.classList.add('qr--val');
1556 const val = scope.macros[key];
1557 if (val instanceof SlashCommandClosure) {
1558 v.classList.add('qr--closure');
1559 v.title = val.rawText;
1560 v.textContent = val.toString();
1561 } else if (val === undefined) {
1562 v.classList.add('qr--undefined');
1563 v.textContent = 'undefined';
1564 } else {
1565 let jsonVal;
1566 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1567 if (jsonVal && typeof jsonVal == 'object') {
1568 v.textContent = JSON.stringify(jsonVal, null, 2);
1569 } else {
1570 v.textContent = val;
1571 v.classList.add('qr--simple');
1572 }
1573 }
1574 item.append(v);
1575 }
1576 wrap.append(item);
1577 }
1578 }
1579 const pipeItem = document.createElement('div'); {
1580 pipeItem.classList.add('qr--pipe');
1581 const k = document.createElement('div'); {
1582 k.classList.add('qr--key');
1583 k.textContent = 'pipe';
1584 pipeItem.append(k);
1585 }
1586 const v = document.createElement('div'); {
1587 v.classList.add('qr--val');
1588 const val = scope.pipe;
1589 if (val instanceof SlashCommandClosure) {
1590 v.classList.add('qr--closure');
1591 v.title = val.rawText;
1592 v.textContent = val.toString();
1593 } else if (val === undefined) {
1594 v.classList.add('qr--undefined');
1595 v.textContent = 'undefined';
1596 } else {
1597 let jsonVal;
1598 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1599 if (jsonVal && typeof jsonVal == 'object') {
1600 v.textContent = JSON.stringify(jsonVal, null, 2);
1601 } else {
1602 v.textContent = val;
1603 v.classList.add('qr--simple');
1604 }
1605 }
1606 pipeItem.append(v);
1607 }
1608 wrap.append(pipeItem);
1609 }
1610 if (scope.parent) {
1611 wrap.append(buildVars(scope.parent));
1612 }
1613 }
1614 return wrap;
1615 };
1616 const buildStack = ()=>{
1617 const wrap = document.createElement('div'); {
1618 wrap.classList.add('qr--stack');
1619 const title = document.createElement('div'); {
1620 title.classList.add('qr--title');
1621 title.textContent = 'Call Stack';
1622 wrap.append(title);
1623 }
1624 let ei = -1;
1625 for (const executor of this.debugController.cmdStack.toReversed()) {
1626 ei++;
1627 const c = this.debugController.stack.toReversed()[ei];
1628 const item = document.createElement('div'); {
1629 item.classList.add('qr--item');
1630 if (executor.source == source) {
1631 let hi;
1632 item.addEventListener('pointerenter', ()=>{
1633 const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText);
1634 const layer = syntax.getBoundingClientRect();
1635 hi = document.createElement('div');
1636 hi.classList.add('qr--highlight-secondary');
1637 hi.style.left = `${loc.left - layer.left}px`;
1638 hi.style.width = `${loc.right - loc.left}px`;
1639 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1640 hi.style.height = `${loc.bottom - loc.top}px`;
1641 syntax.append(hi);
1642 });
1643 item.addEventListener('pointerleave', ()=>hi?.remove());
1644 }
1645 const cmd = document.createElement('div'); {
1646 cmd.classList.add('qr--cmd');
1647 cmd.textContent = `/${executor.name}`;
1648 if (executor.command.name == 'run') {
1649 cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1650 }
1651 item.append(cmd);
1652 }
1653 const src = document.createElement('div'); {
1654 src.classList.add('qr--source');
1655 const line = closure.fullText.slice(0, executor.start).split('\n').length;
1656 if (uuidCheck.test(executor.source)) {
1657 const p1 = document.createElement('span'); {
1658 p1.classList.add('qr--fixed');
1659 p1.textContent = executor.source.slice(0,5);
1660 src.append(p1);
1661 }
1662 const p2 = document.createElement('span'); {
1663 p2.classList.add('qr--truncated');
1664 p2.textContent = '…';
1665 src.append(p2);
1666 }
1667 const p3 = document.createElement('span'); {
1668 p3.classList.add('qr--fixed');
1669 p3.textContent = `${executor.source.slice(-5)}:${line}`;
1670 src.append(p3);
1671 }
1672 src.title = `anonymous: ${executor.source}`;
1673 } else {
1674 src.textContent = `${executor.source}:${line}`;
1675 }
1676 item.append(src);
1677 }
1678 wrap.append(item);
1679 }
1680 }
1681 }
1682 return wrap;
1683 };
1684 this.editorDebugState.append(buildVars(closure.scope, true));
1685 this.editorDebugState.append(buildStack());
1686 this.editorDebugState.classList.add('qr--active');
1687 const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText);
1688 const layer = syntax.getBoundingClientRect();
1689 const hi = document.createElement('div');
1690 hi.classList.add('qr--highlight');
1691 if (this.debugController.namedArguments === undefined) {
1692 hi.classList.add('qr--unresolved');
1693 }
1694 hi.style.left = `${loc.left - layer.left}px`;
1695 hi.style.width = `${loc.right - loc.left}px`;
1696 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1697 hi.style.height = `${loc.bottom - loc.top}px`;
1698 syntax.append(hi);
1699 const isStepping = await this.debugController.awaitContinue();
1700 hi.remove();
1701 this.editorDebugState.textContent = '';
1702 this.editorDebugState.classList.remove('qr--active');
1703 this.editorDom.classList.remove('qr--isPaused');
1704 return isStepping;
1705 };
1706 const result = await this.onDebug(this);
565 if (this.abortController?.signal?.aborted) {1707 if (this.abortController?.signal?.aborted) {
566 this.editorExecuteProgress.classList.add('qr--aborted');1708 this.editorExecuteProgress.classList.add('qr--aborted');
567 } else {1709 } else {
@@ -586,9 +1728,18 @@ export class QuickReply {
586 `;1728 `;
587 }1729 }
588 }1730 }
1731 if (noSyntax) {
1732 this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
1733 }
1734 this.editorMessageLabel.innerHTML = '';
1735 this.editorMessageLabel.textContent = 'Message / Command: ';
1736 this.editorMessage.value = oText;
1737 this.editorMessage.dispatchEvent(new Event('input', { bubbles:true }));
589 this.editorExecutePromise = null;1738 this.editorExecutePromise = null;
590 this.editorExecuteBtn.classList.remove('qr--busy');1739 this.editorExecuteBtn.classList.remove('qr--busy');
591 this.editorPopup.dlg.classList.remove('qr--hide');1740 this.editorDom.classList.remove('qr--isExecuting');
1741 this.isExecuting = false;
1742 this.editorPopup.onClosing = null;
592 }1743 }
5931744
594 updateEditorProgress(done, total) {1745 updateEditorProgress(done, total) {
@@ -623,6 +1774,47 @@ export class QuickReply {
623 /**1774 /**
624 * @param {string} value1775 * @param {string} value
625 */1776 */
1777 updateIcon(value) {
1778 if (this.onUpdate) {
1779 if (value === null) return;
1780 if (this.settingsDomIcon) {
1781 if (this.icon != value) {
1782 if (value == '') {
1783 if (this.icon) {
1784 this.settingsDomIcon.classList.remove(this.icon);
1785 }
1786 this.settingsDomIcon.textContent = '…';
1787 this.settingsDomIcon.classList.remove('fa-solid');
1788 } else {
1789 if (this.icon) {
1790 this.settingsDomIcon.classList.remove(this.icon);
1791 } else {
1792 this.settingsDomIcon.classList.add('fa-solid');
1793 }
1794 this.settingsDomIcon.classList.add(value);
1795 }
1796 }
1797 }
1798 this.icon = value;
1799 this.updateRender();
1800 this.onUpdate(this);
1801 }
1802 }
1803
1804 /**
1805 * @param {boolean} value
1806 */
1807 updateShowLabel(value) {
1808 if (this.onUpdate) {
1809 this.showLabel = value;
1810 this.updateRender();
1811 this.onUpdate(this);
1812 }
1813 }
1814
1815 /**
1816 * @param {string} value
1817 */
626 updateLabel(value) {1818 updateLabel(value) {
627 if (this.onUpdate) {1819 if (this.onUpdate) {
628 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {1820 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {
@@ -670,21 +1862,25 @@ export class QuickReply {
670 }1862 }
6711863
6721864
673 async execute(args = {}, isEditor = false, isRun = false) {1865 async execute(args = {}, isEditor = false, isRun = false, options = {}) {
674 if (this.message?.length > 0 && this.onExecute) {1866 if (this.message?.length > 0 && this.onExecute) {
675 const scope = new SlashCommandScope();1867 const scope = new SlashCommandScope();
676 for (const key of Object.keys(args)) {1868 for (const key of Object.keys(args)) {
1869 if (key[0] == '_') continue;
1870 if (key == 'isAutoExecute') continue;
677 scope.setMacro(`arg::${key}`, args[key]);1871 scope.setMacro(`arg::${key}`, args[key]);
678 }1872 }
1873 scope.setMacro('arg::*', '');
679 if (isEditor) {1874 if (isEditor) {
680 this.abortController = new SlashCommandAbortController();1875 this.abortController = new SlashCommandAbortController();
681 }1876 }
682 return await this.onExecute(this, {1877 return await this.onExecute(this, {
683 message:this.message,1878 message: this.message,
684 isAutoExecute: args.isAutoExecute ?? false,1879 isAutoExecute: args.isAutoExecute ?? false,
685 isEditor,1880 isEditor,
686 isRun,1881 isRun,
687 scope,1882 scope,
1883 executionOptions: options,
688 });1884 });
689 }1885 }
690 }1886 }
@@ -695,6 +1891,8 @@ export class QuickReply {
695 toJSON() {1891 toJSON() {
696 return {1892 return {
697 id: this.id,1893 id: this.id,
1894 icon: this.icon,
1895 showLabel: this.showLabel,
698 label: this.label,1896 label: this.label,
699 title: this.title,1897 title: this.title,
700 message: this.message,1898 message: this.message,
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+6 -1
@@ -60,7 +60,12 @@ export class QuickReplyConfig {
60 /**@type {HTMLElement}*/60 /**@type {HTMLElement}*/
61 this.setListDom = root.querySelector('.qr--setList');61 this.setListDom = root.querySelector('.qr--setList');
62 root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{62 root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
63 this.addSet(QuickReplySet.list[0]);63 const newSet = QuickReplySet.list.find(qr=>!this.setList.find(qrl=>qrl.set == qr));
64 if (newSet) {
65 this.addSet(newSet);
66 } else {
67 toastr.warning('All existing QR Sets have already been added.');
68 }
64 });69 });
65 this.updateSetListDom();70 this.updateSetListDom();
66 }71 }
public/scripts/extensions/quick-reply/src/QuickReplySet.js+191 -20
@@ -1,7 +1,9 @@
1import { getRequestHeaders, substituteParams } from '../../../../script.js';1import { getRequestHeaders, substituteParams } from '../../../../script.js';
2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
2import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';3import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
3import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
4import { debounceAsync, warn } from '../index.js';6import { debounceAsync, log, warn } from '../index.js';
5import { QuickReply } from './QuickReply.js';7import { QuickReply } from './QuickReply.js';
68
7export class QuickReplySet {9export class QuickReplySet {
@@ -16,7 +18,7 @@ export class QuickReplySet {
16 }18 }
1719
18 /**20 /**
19 * @param {String} name - name of the QuickReplySet21 * @param {string} name - name of the QuickReplySet
20 */22 */
21 static get(name) {23 static get(name) {
22 return this.list.find(it=>it.name == name);24 return this.list.find(it=>it.name == name);
@@ -25,17 +27,19 @@ export class QuickReplySet {
2527
2628
2729
28 /**@type {String}*/ name;30 /**@type {string}*/ name;
29 /**@type {Boolean}*/ disableSend = false;31 /**@type {boolean}*/ disableSend = false;
30 /**@type {Boolean}*/ placeBeforeInput = false;32 /**@type {boolean}*/ placeBeforeInput = false;
31 /**@type {Boolean}*/ injectInput = false;33 /**@type {boolean}*/ injectInput = false;
34 /**@type {string}*/ color = 'transparent';
35 /**@type {boolean}*/ onlyBorderColor = false;
32 /**@type {QuickReply[]}*/ qrList = [];36 /**@type {QuickReply[]}*/ qrList = [];
3337
34 /**@type {Number}*/ idIndex = 0;38 /**@type {number}*/ idIndex = 0;
3539
36 /**@type {Boolean}*/ isDeleted = false;40 /**@type {boolean}*/ isDeleted = false;
3741
38 /**@type {Function}*/ save;42 /**@type {function}*/ save;
3943
40 /**@type {HTMLElement}*/ dom;44 /**@type {HTMLElement}*/ dom;
41 /**@type {HTMLElement}*/ settingsDom;45 /**@type {HTMLElement}*/ settingsDom;
@@ -64,6 +68,7 @@ export class QuickReplySet {
64 const root = document.createElement('div'); {68 const root = document.createElement('div'); {
65 this.dom = root;69 this.dom = root;
66 root.classList.add('qr--buttons');70 root.classList.add('qr--buttons');
71 this.updateColor();
67 this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{72 this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
68 root.append(qr.render());73 root.append(qr.render());
69 });74 });
@@ -78,6 +83,22 @@ export class QuickReplySet {
78 this.dom.append(qr.render());83 this.dom.append(qr.render());
79 });84 });
80 }85 }
86 updateColor() {
87 if (!this.dom) return;
88 if (this.color && this.color != 'transparent') {
89 this.dom.style.setProperty('--qr--color', this.color);
90 this.dom.classList.add('qr--color');
91 if (this.onlyBorderColor) {
92 this.dom.classList.add('qr--borderColor');
93 } else {
94 this.dom.classList.remove('qr--borderColor');
95 }
96 } else {
97 this.dom.style.setProperty('--qr--color', 'transparent');
98 this.dom.classList.remove('qr--color');
99 this.dom.classList.remove('qr--borderColor');
100 }
101 }
81102
82103
83104
@@ -93,6 +114,11 @@ export class QuickReplySet {
93 }114 }
94 return this.settingsDom;115 return this.settingsDom;
95 }116 }
117 /**
118 *
119 * @param {QuickReply} qr
120 * @param {number} idx
121 */
96 renderSettingsItem(qr, idx) {122 renderSettingsItem(qr, idx) {
97 this.settingsDom.append(qr.renderSettings(idx));123 this.settingsDom.append(qr.renderSettings(idx));
98 }124 }
@@ -102,6 +128,18 @@ export class QuickReplySet {
102128
103 /**129 /**
104 *130 *
131 * @param {QuickReply} qr
132 */
133 async debug(qr) {
134 const parser = new SlashCommandParser();
135 const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController);
136 closure.source = `${this.name}.${qr.label}`;
137 closure.onProgress = (done, total) => qr.updateEditorProgress(done, total);
138 closure.scope.setMacro('arg::*', '');
139 return (await closure.execute())?.pipe;
140 }
141 /**
142 *
105 * @param {QuickReply} qr The QR to execute.143 * @param {QuickReply} qr The QR to execute.
106 * @param {object} options144 * @param {object} options
107 * @param {string} [options.message] (null) altered message to be used145 * @param {string} [options.message] (null) altered message to be used
@@ -109,6 +147,7 @@ export class QuickReplySet {
109 * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor147 * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
110 * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)148 * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
111 * @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command149 * @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
150 * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options
112 * @returns151 * @returns
113 */152 */
114 async executeWithOptions(qr, options = {}) {153 async executeWithOptions(qr, options = {}) {
@@ -118,7 +157,9 @@ export class QuickReplySet {
118 isEditor:false,157 isEditor:false,
119 isRun:false,158 isRun:false,
120 scope:null,159 scope:null,
160 executionOptions:{},
121 }, options);161 }, options);
162 const execOptions = options.executionOptions;
122 /**@type {HTMLTextAreaElement}*/163 /**@type {HTMLTextAreaElement}*/
123 const ta = document.querySelector('#send_textarea');164 const ta = document.querySelector('#send_textarea');
124 const finalMessage = options.message ?? qr.message;165 const finalMessage = options.message ?? qr.message;
@@ -136,21 +177,24 @@ export class QuickReplySet {
136 if (input[0] == '/' && !this.disableSend) {177 if (input[0] == '/' && !this.disableSend) {
137 let result;178 let result;
138 if (options.isAutoExecute || options.isRun) {179 if (options.isAutoExecute || options.isRun) {
139 result = await executeSlashCommandsWithOptions(input, {180 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
140 handleParserErrors: true,181 handleParserErrors: true,
141 scope: options.scope,182 scope: options.scope,
142 });183 source: `${this.name}.${qr.label}`,
184 }));
143 } else if (options.isEditor) {185 } else if (options.isEditor) {
144 result = await executeSlashCommandsWithOptions(input, {186 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
145 handleParserErrors: false,187 handleParserErrors: false,
146 scope: options.scope,188 scope: options.scope,
147 abortController: qr.abortController,189 abortController: qr.abortController,
190 source: `${this.name}.${qr.label}`,
148 onProgress: (done, total) => qr.updateEditorProgress(done, total),191 onProgress: (done, total) => qr.updateEditorProgress(done, total),
149 });192 }));
150 } else {193 } else {
151 result = await executeSlashCommandsOnChatInput(input, {194 result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, {
152 scope: options.scope,195 scope: options.scope,
153 });196 source: `${this.name}.${qr.label}`,
197 }));
154 }198 }
155 return typeof result === 'object' ? result?.pipe : '';199 return typeof result === 'object' ? result?.pipe : '';
156 }200 }
@@ -165,7 +209,7 @@ export class QuickReplySet {
165 }209 }
166 /**210 /**
167 * @param {QuickReply} qr211 * @param {QuickReply} qr
168 * @param {String} [message] - optional altered message to be used212 * @param {string} [message] - optional altered message to be used
169 * @param {SlashCommandScope} [scope] - optional scope to be used when running the command213 * @param {SlashCommandScope} [scope] - optional scope to be used when running the command
170 */214 */
171 async execute(qr, message = null, isAutoExecute = false, scope = null) {215 async execute(qr, message = null, isAutoExecute = false, scope = null) {
@@ -179,10 +223,11 @@ export class QuickReplySet {
179223
180224
181225
182 addQuickReply() {226 addQuickReply(data = {}) {
183 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;227 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228 data.id =
184 this.idIndex = id + 1;229 this.idIndex = id + 1;
185 const qr = QuickReply.from({ id });230 const qr = QuickReply.from(data);
186 this.qrList.push(qr);231 this.qrList.push(qr);
187 this.hookQuickReply(qr);232 this.hookQuickReply(qr);
188 if (this.settingsDom) {233 if (this.settingsDom) {
@@ -194,11 +239,131 @@ export class QuickReplySet {
194 this.save();239 this.save();
195 return qr;240 return qr;
196 }241 }
242 addQuickReplyFromText(qrJson) {
243 let data;
244 if (qrJson) {
245 try {
246 data = JSON.parse(qrJson ?? '{}');
247 delete data.id;
248 } catch {
249 // not JSON data
250 }
251 if (data) {
252 // JSON data
253 if (data.label === undefined || data.message === undefined) {
254 // not a QR
255 toastr.error('Not a QR.');
256 return;
257 }
258 } else {
259 // no JSON, use plaintext as QR message
260 data = { message: qrJson };
261 }
262 } else {
263 data = {};
264 }
265 const newQr = this.addQuickReply(data);
266 return newQr;
267 }
197268
269 /**
270 *
271 * @param {QuickReply} qr
272 */
198 hookQuickReply(qr) {273 hookQuickReply(qr) {
274 qr.onDebug = ()=>this.debug(qr);
199 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);275 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
200 qr.onDelete = ()=>this.removeQuickReply(qr);276 qr.onDelete = ()=>this.removeQuickReply(qr);
201 qr.onUpdate = ()=>this.save();277 qr.onUpdate = ()=>this.save();
278 qr.onInsertBefore = (qrJson)=>{
279 this.addQuickReplyFromText(qrJson);
280 const newQr = this.qrList.pop();
281 this.qrList.splice(this.qrList.indexOf(qr), 0, newQr);
282 if (qr.settingsDom) {
283 qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom);
284 }
285 this.save();
286 };
287 qr.onTransfer = async()=>{
288 /**@type {HTMLSelectElement} */
289 let sel;
290 let isCopy = false;
291 const dom = document.createElement('div'); {
292 dom.classList.add('qr--transferModal');
293 const title = document.createElement('h3'); {
294 title.textContent = 'Transfer Quick Reply';
295 dom.append(title);
296 }
297 const subTitle = document.createElement('h4'); {
298 const entryName = qr.label;
299 const bookName = this.name;
300 subTitle.textContent = `${bookName}: ${entryName}`;
301 dom.append(subTitle);
302 }
303 sel = document.createElement('select'); {
304 sel.classList.add('qr--transferSelect');
305 sel.setAttribute('autofocus', '1');
306 const noOpt = document.createElement('option'); {
307 noOpt.value = '';
308 noOpt.textContent = '-- Select QR Set --';
309 sel.append(noOpt);
310 }
311 for (const qrs of QuickReplySet.list) {
312 const opt = document.createElement('option'); {
313 opt.value = qrs.name;
314 opt.textContent = qrs.name;
315 sel.append(opt);
316 }
317 }
318 sel.addEventListener('keyup', (evt)=>{
319 if (evt.key == 'Shift') {
320 (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
321 return;
322 }
323 });
324 sel.addEventListener('keydown', (evt)=>{
325 if (evt.key == 'Shift') {
326 (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
327 return;
328 }
329 if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') {
330 evt.preventDefault();
331 if (evt.shiftKey) isCopy = true;
332 dlg.completeAffirmative();
333 }
334 });
335 dom.append(sel);
336 }
337 const hintP = document.createElement('p'); {
338 const hint = document.createElement('small'); {
339 hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.';
340 hintP.append(hint);
341 }
342 dom.append(hintP);
343 }
344 }
345 const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' });
346 const copyBtn = document.createElement('div'); {
347 copyBtn.classList.add('qr--copy');
348 copyBtn.classList.add('menu_button');
349 copyBtn.textContent = 'Copy';
350 copyBtn.addEventListener('click', ()=>{
351 isCopy = true;
352 dlg.completeAffirmative();
353 });
354 (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
355 }
356 const prom = dlg.show();
357 sel.focus();
358 await prom;
359 if (dlg.result == POPUP_RESULT.AFFIRMATIVE) {
360 const qrs = QuickReplySet.list.find(it=>it.name == sel.value);
361 qrs.addQuickReply(qr.toJSON());
362 if (!isCopy) {
363 qr.delete();
364 }
365 }
366 };
202 }367 }
203368
204 removeQuickReply(qr) {369 removeQuickReply(qr) {
@@ -214,6 +379,8 @@ export class QuickReplySet {
214 disableSend: this.disableSend,379 disableSend: this.disableSend,
215 placeBeforeInput: this.placeBeforeInput,380 placeBeforeInput: this.placeBeforeInput,
216 injectInput: this.injectInput,381 injectInput: this.injectInput,
382 color: this.color,
383 onlyBorderColor: this.onlyBorderColor,
217 qrList: this.qrList,384 qrList: this.qrList,
218 idIndex: this.idIndex,385 idIndex: this.idIndex,
219 };386 };
@@ -245,8 +412,12 @@ export class QuickReplySet {
245 if (response.ok) {412 if (response.ok) {
246 this.unrender();413 this.unrender();
247 const idx = QuickReplySet.list.indexOf(this);414 const idx = QuickReplySet.list.indexOf(this);
248 QuickReplySet.list.splice(idx, 1);415 if (idx > -1) {
249 this.isDeleted = true;416 QuickReplySet.list.splice(idx, 1);
417 this.isDeleted = true;
418 } else {
419 warn(`Deleted Quick Reply Set was not found in the list of sets: ${this.name}`);
420 }
250 } else {421 } else {
251 warn(`Failed to delete Quick Reply Set: ${this.name}`);422 warn(`Failed to delete Quick Reply Set: ${this.name}`);
252 }423 }
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js+1 -1
@@ -45,7 +45,7 @@ export class QuickReplySetLink {
45 this.set = QuickReplySet.get(set.value);45 this.set = QuickReplySet.get(set.value);
46 this.update();46 this.update();
47 });47 });
48 QuickReplySet.list.forEach(qrs=>{48 QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
49 const opt = document.createElement('option'); {49 const opt = document.createElement('option'); {
50 opt.value = qrs.name;50 opt.value = qrs.name;
51 opt.textContent = qrs.name;51 opt.textContent = qrs.name;
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+259 -18
@@ -1,8 +1,12 @@
1import { SlashCommand } from '../../../slash-commands/SlashCommand.js';1import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
2import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
2import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
4import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
3import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';5import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';
6import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
4import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';7import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';
5import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';8import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
9import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { isTrueBoolean } from '../../../utils.js';10import { isTrueBoolean } from '../../../utils.js';
7// eslint-disable-next-line no-unused-vars11// eslint-disable-next-line no-unused-vars
8import { QuickReplyApi } from '../api/QuickReplyApi.js';12import { QuickReplyApi } from '../api/QuickReplyApi.js';
@@ -47,6 +51,13 @@ export class SlashCommandHandler {
47 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);51 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);
48 }) ?? [],52 }) ?? [],
4953
54 /** All QRs inside a set, utilizing the "set" named argument, returns the QR's ID */
55 qrIds: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => {
56 const icons = getExecutionIcons(qr);
57 const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim();
58 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr, null, ()=>qr.id.toString(), true);
59 }) ?? [],
60
50 /** All QRs as a set.name string, to be able to execute, for example via the /run command */61 /** All QRs as a set.name string, to be able to execute, for example via the /run command */
51 qrExecutables: () => {62 qrExecutables: () => {
52 const globalSetList = this.api.settings.config.setList;63 const globalSetList = this.api.settings.config.setList;
@@ -63,7 +74,7 @@ export class SlashCommandHandler {
63 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),74 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),
64 ];75 ];
65 },76 },
66 }77 };
6778
68 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;79 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;
6980
@@ -234,8 +245,20 @@ export class SlashCommandHandler {
234 name: 'label',245 name: 'label',
235 description: 'text on the button, e.g., label=MyButton',246 description: 'text on the button, e.g., label=MyButton',
236 typeList: [ARGUMENT_TYPE.STRING],247 typeList: [ARGUMENT_TYPE.STRING],
237 isRequired: true,248 isRequired: false,
238 enumProvider: localEnumProviders.qrLabels,249 enumProvider: localEnumProviders.qrEntries,
250 }),
251 SlashCommandNamedArgument.fromProps({
252 name: 'icon',
253 description: 'icon to show on the button, e.g., icon=fa-pencil',
254 typeList: [ARGUMENT_TYPE.STRING],
255 isRequired: false,
256 }),
257 SlashCommandNamedArgument.fromProps({
258 name: 'showlabel',
259 description: 'whether to show the label even when an icon is assigned, e.g., icon=fa-pencil showlabel=true',
260 typeList: [ARGUMENT_TYPE.BOOLEAN],
261 isRequired: false,
239 }),262 }),
240 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),263 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
241 new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),264 new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
@@ -247,6 +270,13 @@ export class SlashCommandHandler {
247 ];270 ];
248 const qrUpdateArgs = [271 const qrUpdateArgs = [
249 new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),272 new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
273 SlashCommandNamedArgument.fromProps({
274 name: 'id',
275 description: 'numeric ID of the QR, e.g., id=42',
276 typeList: [ARGUMENT_TYPE.NUMBER],
277 isRequired: false,
278 enumProvider: localEnumProviders.qrIds,
279 }),
250 ];280 ];
251281
252 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',282 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
@@ -272,13 +302,61 @@ export class SlashCommandHandler {
272 </div>302 </div>
273 `,303 `,
274 }));304 }));
305 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-get',
306 callback: (args, _) => {
307 return this.getQuickReply(args);
308 },
309 namedArgumentList: [
310 SlashCommandNamedArgument.fromProps({
311 name: 'set',
312 description: 'name of the QR set, e.g., set=PresetName1',
313 typeList: [ARGUMENT_TYPE.STRING],
314 isRequired: true,
315 enumProvider: localEnumProviders.qrSets,
316 }),
317 SlashCommandNamedArgument.fromProps({
318 name: 'label',
319 description: 'text on the button, e.g., label=MyButton',
320 typeList: [ARGUMENT_TYPE.STRING],
321 isRequired: false,
322 enumProvider: localEnumProviders.qrEntries,
323 }),
324 SlashCommandNamedArgument.fromProps({
325 name: 'id',
326 description: 'numeric ID of the QR, e.g., id=42',
327 typeList: [ARGUMENT_TYPE.NUMBER],
328 isRequired: false,
329 enumProvider: localEnumProviders.qrIds,
330 }),
331 ],
332 returns: 'a dictionary with all the QR\'s properties',
333 helpString: `
334 <div>Get a Quick Reply's properties.</div>
335 <div>
336 <strong>Examples:</strong>
337 <ul>
338 <li>
339 <pre><code>/qr-get set=MyPreset label=MyButton | /echo</code></pre>
340 <pre><code>/qr-get set=MyPreset id=42 | /echo</code></pre>
341 </li>
342 </ul>
343 </div>
344 `,
345 }));
275 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',346 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
276 callback: (args, message) => {347 callback: (args, message) => {
277 this.updateQuickReply(args, message);348 this.updateQuickReply(args, message);
278 return '';349 return '';
279 },350 },
280 returns: 'updated quick reply',351 returns: 'updated quick reply',
281 namedArgumentList: [...qrUpdateArgs, ...qrArgs],352 namedArgumentList: [...qrUpdateArgs, ...qrArgs.map(it=>{
353 if (it.name == 'label') {
354 const clone = SlashCommandNamedArgument.fromProps(it);
355 clone.isRequired = false;
356 return clone;
357 }
358 return it;
359 })],
282 unnamedArgumentList: [360 unnamedArgumentList: [
283 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),361 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),
284 ],362 ],
@@ -315,6 +393,12 @@ export class SlashCommandHandler {
315 typeList: [ARGUMENT_TYPE.STRING],393 typeList: [ARGUMENT_TYPE.STRING],
316 enumProvider: localEnumProviders.qrEntries,394 enumProvider: localEnumProviders.qrEntries,
317 }),395 }),
396 SlashCommandNamedArgument.fromProps({
397 name: 'id',
398 description: 'numeric ID of the QR, e.g., id=42',
399 typeList: [ARGUMENT_TYPE.NUMBER],
400 enumProvider: localEnumProviders.qrIds,
401 }),
318 ],402 ],
319 unnamedArgumentList: [403 unnamedArgumentList: [
320 SlashCommandArgument.fromProps({404 SlashCommandArgument.fromProps({
@@ -344,6 +428,12 @@ export class SlashCommandHandler {
344 typeList: [ARGUMENT_TYPE.STRING],428 typeList: [ARGUMENT_TYPE.STRING],
345 enumProvider: localEnumProviders.qrEntries,429 enumProvider: localEnumProviders.qrEntries,
346 }),430 }),
431 SlashCommandNamedArgument.fromProps({
432 name: 'id',
433 description: 'numeric ID of the QR, e.g., id=42',
434 typeList: [ARGUMENT_TYPE.NUMBER],
435 enumProvider: localEnumProviders.qrIds,
436 }),
347 new SlashCommandNamedArgument(437 new SlashCommandNamedArgument(
348 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',438 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
349 ),439 ),
@@ -389,6 +479,12 @@ export class SlashCommandHandler {
389 typeList: [ARGUMENT_TYPE.STRING],479 typeList: [ARGUMENT_TYPE.STRING],
390 enumProvider: localEnumProviders.qrEntries,480 enumProvider: localEnumProviders.qrEntries,
391 }),481 }),
482 SlashCommandNamedArgument.fromProps({
483 name: 'id',
484 description: 'numeric ID of the QR, e.g., id=42',
485 typeList: [ARGUMENT_TYPE.NUMBER],
486 enumProvider: localEnumProviders.qrIds,
487 }),
392 ],488 ],
393 unnamedArgumentList: [489 unnamedArgumentList: [
394 SlashCommandArgument.fromProps({490 SlashCommandArgument.fromProps({
@@ -425,6 +521,12 @@ export class SlashCommandHandler {
425 isRequired: true,521 isRequired: true,
426 enumProvider: localEnumProviders.qrSets,522 enumProvider: localEnumProviders.qrSets,
427 }),523 }),
524 SlashCommandNamedArgument.fromProps({
525 name: 'id',
526 description: 'numeric ID of the QR, e.g., id=42',
527 typeList: [ARGUMENT_TYPE.NUMBER],
528 enumProvider: localEnumProviders.qrIds,
529 }),
428 ],530 ],
429 unnamedArgumentList: [531 unnamedArgumentList: [
430 SlashCommandArgument.fromProps({532 SlashCommandArgument.fromProps({
@@ -454,8 +556,8 @@ export class SlashCommandHandler {
454 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),556 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
455 ];557 ];
456 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',558 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
457 callback: (args, name) => {559 callback: async (args, name) => {
458 this.createSet(name, args);560 await this.createSet(name, args);
459 return '';561 return '';
460 },562 },
461 aliases: ['qr-presetadd'],563 aliases: ['qr-presetadd'],
@@ -485,8 +587,8 @@ export class SlashCommandHandler {
485 }));587 }));
486588
487 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',589 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
488 callback: (args, name) => {590 callback: async (args, name) => {
489 this.updateSet(name, args);591 await this.updateSet(name, args);
490 return '';592 return '';
491 },593 },
492 aliases: ['qr-presetupdate'],594 aliases: ['qr-presetupdate'],
@@ -510,8 +612,8 @@ export class SlashCommandHandler {
510 `,612 `,
511 }));613 }));
512 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',614 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
513 callback: (_, name) => {615 callback: async (_, name) => {
514 this.deleteSet(name);616 await this.deleteSet(name);
515 return '';617 return '';
516 },618 },
517 aliases: ['qr-presetdelete'],619 aliases: ['qr-presetdelete'],
@@ -533,6 +635,134 @@ export class SlashCommandHandler {
533 </div>635 </div>
534 `,636 `,
535 }));637 }));
638
639 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-arg',
640 callback: ({ _scope }, [key, value]) => {
641 _scope.setMacro(`arg::${key}`, value, key.includes('*'));
642 return '';
643 },
644 unnamedArgumentList: [
645 SlashCommandArgument.fromProps({ description: 'argument name',
646 typeList: ARGUMENT_TYPE.STRING,
647 isRequired: true,
648 }),
649 SlashCommandArgument.fromProps({ description: 'argument value',
650 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
651 isRequired: true,
652 }),
653 ],
654 splitUnnamedArgument: true,
655 splitUnnamedArgumentCount: 2,
656 helpString: `
657 <div>
658 Set a fallback value for a Quick Reply argument.
659 </div>
660 <div>
661 <strong>Example:</strong>
662 <pre><code>/qr-arg x foo |\n/echo {{arg::x}}</code></pre>
663 </div>
664 `,
665 }));
666
667 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'import',
668 /**
669 *
670 * @param {{_scope:SlashCommandScope, _abortController:SlashCommandAbortController, _debugController:SlashCommandDebugController, from:string}} args
671 * @param {string} value
672 */
673 callback: (args, value) => {
674 if (!args.from) throw new Error('/import requires from= to be set.');
675 if (!value) throw new Error('/import requires the unnamed argument to be set.');
676 let qr = [...this.api.listGlobalSets(), ...this.api.listChatSets()]
677 .map(it=>this.api.getSetByName(it)?.qrList ?? [])
678 .flat()
679 .find(it=>it.label == args.from)
680 ;
681 if (!qr) {
682 let [setName, ...qrNameParts] = args.from.split('.');
683 let qrName = qrNameParts.join('.');
684 let qrs = QuickReplySet.get(setName);
685 if (qrs) {
686 qr = qrs.qrList.find(it=>it.label == qrName);
687 }
688 }
689 if (qr) {
690 const parser = new SlashCommandParser();
691 const closure = parser.parse(qr.message, true, [], args._abortController, args._debugController);
692 if (args._debugController) {
693 closure.source = args.from;
694 }
695 const testCandidates = (executor)=>{
696 return (
697 executor.namedArgumentList.find(arg=>arg.name == 'key')
698 && executor.unnamedArgumentList.length > 0
699 && executor.unnamedArgumentList[0].value instanceof SlashCommandClosure
700 ) || (
701 !executor.namedArgumentList.find(arg=>arg.name == 'key')
702 && executor.unnamedArgumentList.length > 1
703 && executor.unnamedArgumentList[1].value instanceof SlashCommandClosure
704 );
705 };
706 const candidates = closure.executorList
707 .filter(executor=>['let', 'var'].includes(executor.command.name))
708 .filter(testCandidates)
709 .map(executor=>({
710 key: executor.namedArgumentList.find(arg=>arg.name == 'key')?.value ?? executor.unnamedArgumentList[0].value,
711 value: executor.unnamedArgumentList[executor.namedArgumentList.find(arg=>arg.name == 'key') ? 0 : 1].value,
712 }))
713 ;
714 for (let i = 0; i < value.length; i++) {
715 const srcName = value[i];
716 let dstName = srcName;
717 if (i + 2 < value.length && value[i + 1] == 'as') {
718 dstName = value[i + 2];
719 i += 2;
720 }
721 const pick = candidates.find(it=>it.key == srcName);
722 if (!pick) throw new Error(`No scoped closure named "${srcName}" found in "${args.from}"`);
723 if (args._scope.existsVariableInScope(dstName)) {
724 args._scope.setVariable(dstName, pick.value);
725 } else {
726 args._scope.letVariable(dstName, pick.value);
727 }
728 }
729 } else {
730 throw new Error(`No Quick Reply found for "${name}".`);
731 }
732 return '';
733 },
734 namedArgumentList: [
735 SlashCommandNamedArgument.fromProps({ name: 'from',
736 description: 'Quick Reply to import from (QRSet.QRLabel)',
737 typeList: ARGUMENT_TYPE.STRING,
738 isRequired: true,
739 }),
740 ],
741 unnamedArgumentList: [
742 SlashCommandArgument.fromProps({ description: 'what to import (x or x as y)',
743 acceptsMultiple: true,
744 typeList: ARGUMENT_TYPE.STRING,
745 isRequired: true,
746 }),
747 ],
748 splitUnnamedArgument: true,
749 helpString: `
750 <div>
751 Import one or more closures from another Quick Reply.
752 </div>
753 <div>
754 Only imports closures that are directly assigned a scoped variable via <code>/let</code> or <code>/var</code>.
755 </div>
756 <div>
757 <strong>Examples:</strong>
758 <ul>
759 <li><pre><code>/import from=LibraryQrSet.FooBar foo |\n/:foo</code></pre></li>
760 <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo\n\tbar\n|\n/:foo |\n/:bar</code></pre></li>
761 <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo as x\n\tbar as y\n|\n/:x |\n/:y</code></pre></li>
762 </ul>
763 </div>
764 `,
765 }));
536 }766 }
537767
538768
@@ -618,6 +848,8 @@ export class SlashCommandHandler {
618 args.set ?? '',848 args.set ?? '',
619 args.label ?? '',849 args.label ?? '',
620 {850 {
851 icon: args.icon,
852 showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
621 message: message ?? '',853 message: message ?? '',
622 title: args.title,854 title: args.title,
623 isHidden: isTrueBoolean(args.hidden),855 isHidden: isTrueBoolean(args.hidden),
@@ -633,12 +865,21 @@ export class SlashCommandHandler {
633 toastr.error(ex.message);865 toastr.error(ex.message);
634 }866 }
635 }867 }
868 getQuickReply(args) {
869 try {
870 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
871 } catch (ex) {
872 toastr.error(ex.message);
873 }
874 }
636 updateQuickReply(args, message) {875 updateQuickReply(args, message) {
637 try {876 try {
638 this.api.updateQuickReply(877 this.api.updateQuickReply(
639 args.set ?? '',878 args.set ?? '',
640 args.label ?? '',879 args.id !== undefined ? Number(args.id) : (args.label ?? ''),
641 {880 {
881 icon: args.icon,
882 showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
642 newLabel: args.newlabel,883 newLabel: args.newlabel,
643 message: (message ?? '').trim().length > 0 ? message : undefined,884 message: (message ?? '').trim().length > 0 ? message : undefined,
644 title: args.title,885 title: args.title,
@@ -657,7 +898,7 @@ export class SlashCommandHandler {
657 }898 }
658 deleteQuickReply(args, label) {899 deleteQuickReply(args, label) {
659 try {900 try {
660 this.api.deleteQuickReply(args.set, args.label ?? label);901 this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label));
661 } catch (ex) {902 } catch (ex) {
662 toastr.error(ex.message);903 toastr.error(ex.message);
663 }904 }
@@ -692,9 +933,9 @@ export class SlashCommandHandler {
692 }933 }
693934
694935
695 createSet(name, args) {936 async createSet(name, args) {
696 try {937 try {
697 this.api.createSet(938 await this.api.createSet(
698 args.name ?? name ?? '',939 args.name ?? name ?? '',
699 {940 {
700 disableSend: isTrueBoolean(args.nosend),941 disableSend: isTrueBoolean(args.nosend),
@@ -706,9 +947,9 @@ export class SlashCommandHandler {
706 toastr.error(ex.message);947 toastr.error(ex.message);
707 }948 }
708 }949 }
709 updateSet(name, args) {950 async updateSet(name, args) {
710 try {951 try {
711 this.api.updateSet(952 await this.api.updateSet(
712 args.name ?? name ?? '',953 args.name ?? name ?? '',
713 {954 {
714 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,955 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,
@@ -720,9 +961,9 @@ export class SlashCommandHandler {
720 toastr.error(ex.message);961 toastr.error(ex.message);
721 }962 }
722 }963 }
723 deleteSet(name) {964 async deleteSet(name) {
724 try {965 try {
725 this.api.deleteSet(name ?? '');966 await this.api.deleteSet(name ?? '');
726 } catch (ex) {967 } catch (ex) {
727 toastr.error(ex.message);968 toastr.error(ex.message);
728 }969 }
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+54 -3
@@ -23,6 +23,8 @@ export class SettingsUi {
23 /**@type {HTMLInputElement}*/ disableSend;23 /**@type {HTMLInputElement}*/ disableSend;
24 /**@type {HTMLInputElement}*/ placeBeforeInput;24 /**@type {HTMLInputElement}*/ placeBeforeInput;
25 /**@type {HTMLInputElement}*/ injectInput;25 /**@type {HTMLInputElement}*/ injectInput;
26 /**@type {HTMLInputElement}*/ color;
27 /**@type {HTMLInputElement}*/ onlyBorderColor;
26 /**@type {HTMLSelectElement}*/ currentSet;28 /**@type {HTMLSelectElement}*/ currentSet;
2729
2830
@@ -117,10 +119,29 @@ export class SettingsUi {
117 this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{119 this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
118 this.currentQrSet.addQuickReply();120 this.currentQrSet.addQuickReply();
119 });121 });
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 });
120 this.qrList = this.dom.querySelector('#qr--set-qrList');141 this.qrList = this.dom.querySelector('#qr--set-qrList');
121 this.currentSet = this.dom.querySelector('#qr--set');142 this.currentSet = this.dom.querySelector('#qr--set');
122 this.currentSet.addEventListener('change', ()=>this.onQrSetChange());143 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=>{
124 const opt = document.createElement('option'); {145 const opt = document.createElement('option'); {
125 opt.value = qrs.name;146 opt.value = qrs.name;
126 opt.textContent = qrs.name;147 opt.textContent = qrs.name;
@@ -145,6 +166,34 @@ export class SettingsUi {
145 qrs.injectInput = this.injectInput.checked;166 qrs.injectInput = this.injectInput.checked;
146 qrs.save();167 qrs.save();
147 });168 });
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 });
148 this.onQrSetChange();197 this.onQrSetChange();
149 }198 }
150 onQrSetChange() {199 onQrSetChange() {
@@ -152,6 +201,8 @@ export class SettingsUi {
152 this.disableSend.checked = this.currentQrSet.disableSend;201 this.disableSend.checked = this.currentQrSet.disableSend;
153 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;202 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
154 this.injectInput.checked = this.currentQrSet.injectInput;203 this.injectInput.checked = this.currentQrSet.injectInput;
204 this.color.color = this.currentQrSet.color ?? 'transparent';
205 this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
155 this.qrList.innerHTML = '';206 this.qrList.innerHTML = '';
156 const qrsDom = this.currentQrSet.renderSettings();207 const qrsDom = this.currentQrSet.renderSettings();
157 this.qrList.append(qrsDom);208 this.qrList.append(qrsDom);
@@ -265,7 +316,7 @@ export class SettingsUi {
265 const qrs = new QuickReplySet();316 const qrs = new QuickReplySet();
266 qrs.name = name;317 qrs.name = name;
267 qrs.addQuickReply();318 qrs.addQuickReply();
268 const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);319 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
269 if (idx > -1) {320 if (idx > -1) {
270 QuickReplySet.list.splice(idx, 0, qrs);321 QuickReplySet.list.splice(idx, 0, qrs);
271 } else {322 } else {
@@ -321,7 +372,7 @@ export class SettingsUi {
321 this.prepareChatSetList();372 this.prepareChatSetList();
322 }373 }
323 } else {374 } else {
324 const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(qrs.name) == 1);375 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
325 if (idx > -1) {376 if (idx > -1) {
326 QuickReplySet.list.splice(idx, 0, qrs);377 QuickReplySet.list.splice(idx, 0, qrs);
327 } else {378 } else {
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js+5 -0
@@ -33,11 +33,14 @@ export class ContextMenu {
33 */33 */
34 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {34 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
35 const tree = {35 const tree = {
36 icon: qr.icon,
37 showLabel: qr.showLabel,
36 label: qr.label,38 label: qr.label,
37 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,39 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
38 children: [],40 children: [],
39 };41 };
40 qr.contextList.forEach((cl) => {42 qr.contextList.forEach((cl) => {
43 if (!cl.set) return;
41 if (!hierarchy.includes(cl.set)) {44 if (!hierarchy.includes(cl.set)) {
42 const nextHierarchy = [...hierarchy, cl.set];45 const nextHierarchy = [...hierarchy, cl.set];
43 const nextLabelHierarchy = [...labelHierarchy, tree.label];46 const nextLabelHierarchy = [...labelHierarchy, tree.label];
@@ -45,6 +48,8 @@ export class ContextMenu {
45 cl.set.qrList.forEach(subQr => {48 cl.set.qrList.forEach(subQr => {
46 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);49 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
47 tree.children.push(new MenuItem(50 tree.children.push(new MenuItem(
51 subTree.icon,
52 subTree.showLabel,
48 subTree.label,53 subTree.label,
49 subTree.message,54 subTree.message,
50 (evt) => {55 (evt) => {
public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js+1 -1
@@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js';
22
3export class MenuHeader extends MenuItem {3export class MenuHeader extends MenuItem {
4 constructor(/**@type {String}*/label) {4 constructor(/**@type {String}*/label) {
5 super(label, null, null);5 super(null, null, label, null, null);
6 }6 }
77
88
public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js+34 -7
@@ -1,21 +1,34 @@
1import { SubMenu } from './SubMenu.js';1import { SubMenu } from './SubMenu.js';
22
3export class MenuItem {3export class MenuItem {
4 /**@type {String}*/ label;4 /**@type {string}*/ icon;
5 /**@type {Object}*/ value;5 /**@type {boolean}*/ showLabel;
6 /**@type {Function}*/ callback;6 /**@type {string}*/ label;
7 /**@type {object}*/ value;
8 /**@type {function}*/ callback;
7 /**@type {MenuItem[]}*/ childList = [];9 /**@type {MenuItem[]}*/ childList = [];
8 /**@type {SubMenu}*/ subMenu;10 /**@type {SubMenu}*/ subMenu;
9 /**@type {Boolean}*/ isForceExpanded = false;11 /**@type {boolean}*/ isForceExpanded = false;
1012
11 /**@type {HTMLElement}*/ root;13 /**@type {HTMLElement}*/ root;
1214
13 /**@type {Function}*/ onExpand;15 /**@type {function}*/ onExpand;
1416
1517
1618
1719
18 constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) {20 /**
21 *
22 * @param {string} icon
23 * @param {boolean} showLabel
24 * @param {string} label
25 * @param {object} value
26 * @param {function} callback
27 * @param {MenuItem[]} children
28 */
29 constructor(icon, showLabel, label, value, callback, children = []) {
30 this.icon = icon;
31 this.showLabel = showLabel;
19 this.label = label;32 this.label = label;
20 this.value = value;33 this.value = value;
21 this.callback = callback;34 this.callback = callback;
@@ -33,7 +46,21 @@ export class MenuItem {
33 if (this.callback) {46 if (this.callback) {
34 item.addEventListener('click', (evt) => this.callback(evt, this));47 item.addEventListener('click', (evt) => this.callback(evt, this));
35 }48 }
36 item.append(this.label);49 const icon = document.createElement('div'); {
50 this.domIcon = icon;
51 icon.classList.add('qr--button-icon');
52 icon.classList.add('fa-solid');
53 if (!this.icon) icon.classList.add('qr--hidden');
54 else icon.classList.add(this.icon);
55 item.append(icon);
56 }
57 const lbl = document.createElement('div'); {
58 this.domLabel = lbl;
59 lbl.classList.add('qr--button-label');
60 if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
61 lbl.textContent = this.label;
62 item.append(lbl);
63 }
37 if (this.childList.length > 0) {64 if (this.childList.length > 0) {
38 item.classList.add('ctx-has-children');65 item.classList.add('ctx-has-children');
39 const sub = new SubMenu(this.childList);66 const sub = new SubMenu(this.childList);
public/scripts/extensions/quick-reply/style.css+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}
1#qr--bar {18#qr--bar {
2 outline: none;19 outline: none;
3 margin: 0;20 margin: 0;
@@ -41,6 +58,7 @@
41}58}
42#qr--bar > .qr--buttons,59#qr--bar > .qr--buttons,
43#qr--popout > .qr--body > .qr--buttons {60#qr--popout > .qr--body > .qr--buttons {
61 --qr--color: transparent;
44 margin: 0;62 margin: 0;
45 padding: 0;63 padding: 0;
46 display: flex;64 display: flex;
@@ -49,10 +67,44 @@
49 gap: 5px;67 gap: 5px;
50 width: 100%;68 width: 100%;
51}69}
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}
52#qr--bar > .qr--buttons > .qr--buttons,84#qr--bar > .qr--buttons > .qr--buttons,
53#qr--popout > .qr--body > .qr--buttons > .qr--buttons {85#qr--popout > .qr--body > .qr--buttons > .qr--buttons {
54 display: contents;86 display: contents;
55}87}
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}
56#qr--bar > .qr--buttons .qr--button,108#qr--bar > .qr--buttons .qr--button,
57#qr--popout > .qr--body > .qr--buttons .qr--button {109#qr--popout > .qr--body > .qr--buttons .qr--button {
58 color: var(--SmartThemeBodyColor);110 color: var(--SmartThemeBodyColor);
@@ -66,11 +118,19 @@
66 align-items: center;118 align-items: center;
67 justify-content: center;119 justify-content: center;
68 text-align: center;120 text-align: center;
121 position: relative;
69}122}
70#qr--bar > .qr--buttons .qr--button:hover,123#qr--bar > .qr--buttons .qr--button:hover,
71#qr--popout > .qr--body > .qr--buttons .qr--button:hover {124#qr--popout > .qr--body > .qr--buttons .qr--button:hover {
72 opacity: 1;125 background-color: #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;
74}134}
75#qr--bar > .qr--buttons .qr--button > .qr--button-expander,135#qr--bar > .qr--buttons .qr--button > .qr--button-expander,
76#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {136#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {
@@ -170,36 +230,80 @@
170#qr--settings #qr--set-qrList .qr--set-qrListContents {230#qr--settings #qr--set-qrList .qr--set-qrListContents {
171 padding: 0 0.5em;231 padding: 0 0.5em;
172}232}
173#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {233#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 {
174 display: flex;263 display: flex;
175 flex-direction: row;264 flex-direction: row;
176 gap: 0.5em;265 gap: 0.5em;
177 align-items: baseline;266 align-items: baseline;
178 padding: 0.25em 0;267 padding: 0.25em 0;
179}268}
180#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {269#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
181 flex: 0 0 auto;270 flex: 0 0 auto;
182}271}
183#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {272#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
184 flex: 1 1 25%;273 flex: 1 1 25%;
185}274}
186#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {275#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) {
187 flex: 0 0 auto;276 flex: 0 0 auto;
188}277}
189#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {278#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) {
190 flex: 1 1 75%;279 flex: 1 1 75%;
191}280}
192#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {281#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) {
193 flex: 0 0 auto;282 flex: 0 1 auto;
283 display: flex;
284 gap: 0.25em;
285 justify-content: flex-end;
286 flex-wrap: wrap;
194}287}
195#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle {288#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle {
196 padding: 0.75em;289 padding: 0.75em;
197}290}
198#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,291#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer {
199#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {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 {
200 margin: 0;304 margin: 0;
201}305}
202#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {306#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage {
203 font-size: smaller;307 font-size: smaller;
204}308}
205#qr--settings .qr--set-qrListActions {309#qr--settings .qr--set-qrListActions {
@@ -212,6 +316,7 @@
212#qr--qrOptions {316#qr--qrOptions {
213 display: flex;317 display: flex;
214 flex-direction: column;318 flex-direction: column;
319 padding-right: 1px;
215}320}
216#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {321#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
217 display: flex;322 display: flex;
@@ -219,6 +324,12 @@
219 gap: 0.5em;324 gap: 0.5em;
220 align-items: baseline;325 align-items: baseline;
221}326}
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}
222@media screen and (max-width: 750px) {333@media screen and (max-width: 750px) {
223 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {334 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
224 flex-direction: column;335 flex-direction: column;
@@ -231,13 +342,79 @@
231 flex-direction: column;342 flex-direction: column;
232 }343 }
233 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {344 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
234 min-height: 50svh;345 min-height: 50dvh;
235 height: 50svh;346 height: 50dvh;
236 }347 }
237}348}
238.popup:has(#qr--modalEditor) {349.popup:has(#qr--modalEditor) {
239 aspect-ratio: unset;350 aspect-ratio: unset;
240}351}
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}
241.popup:has(#qr--modalEditor) .popup-content {418.popup:has(#qr--modalEditor) .popup-content {
242 display: flex;419 display: flex;
243 flex-direction: column;420 flex-direction: column;
@@ -249,6 +426,67 @@
249 gap: 1em;426 gap: 1em;
250 overflow: hidden;427 overflow: hidden;
251}428}
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}
252.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {490.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
253 flex: 1 1 auto;491 flex: 1 1 auto;
254 display: flex;492 display: flex;
@@ -260,21 +498,115 @@
260 display: flex;498 display: flex;
261 flex-direction: row;499 flex-direction: row;
262 gap: 0.5em;500 gap: 0.5em;
501 padding: 1px;
263}502}
264.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {503.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 {
265 flex: 1 1 1px;505 flex: 1 1 1px;
266 display: flex;506 display: flex;
267 flex-direction: column;507 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;
268}524}
269.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {525.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 {
270 flex: 1 1 auto;527 flex: 1 1 auto;
271}528}
272.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {529.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 {
273 flex: 1 1 auto;531 flex: 1 1 auto;
274}532}
275.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {533.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 {
276 flex: 0 0 auto;535 flex: 0 0 auto;
277}536}
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}
278.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {610.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
279 flex: 1 1 auto;611 flex: 1 1 auto;
280 display: flex;612 display: flex;
@@ -283,8 +615,9 @@
283}615}
284.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {616.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
285 display: flex;617 display: flex;
618 flex-wrap: wrap;
286 flex-direction: row;619 flex-direction: row;
287 gap: 1em;620 column-gap: 1em;
288 color: var(--grey70);621 color: var(--grey70);
289 font-size: smaller;622 font-size: smaller;
290 align-items: baseline;623 align-items: baseline;
@@ -308,6 +641,11 @@
308 background-color: var(--ac-style-color-background);641 background-color: var(--ac-style-color-background);
309 color: var(--ac-style-color-text);642 color: var(--ac-style-color-text);
310}643}
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}
311.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {649.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
312 color: unset;650 color: unset;
313 background-color: rgba(108 171 251 / 0.25);651 background-color: rgba(108 171 251 / 0.25);
@@ -357,11 +695,15 @@
357 font-family: var(--monoFontFamily);695 font-family: var(--monoFontFamily);
358 padding: 0.75em;696 padding: 0.75em;
359 margin: 0;697 margin: 0;
360 border: none;
361 resize: none;698 resize: none;
362 line-height: 1.2;699 line-height: 1.2;
363 border: 1px solid var(--SmartThemeBorderColor);700 border: 1px solid var(--SmartThemeBorderColor);
364 border-radius: 5px;701 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;
365}707}
366.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {708.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {
367 display: flex;709 display: flex;
@@ -410,6 +752,46 @@
410.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {752.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
411 border-color: #d78872;753 border-color: #d78872;
412}754}
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}
413.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {795.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {
414 --prog: 0;796 --prog: 0;
415 --progColor: #92befc;797 --progColor: #92befc;
@@ -417,6 +799,7 @@
417 --progSuccessColor: #51a351;799 --progSuccessColor: #51a351;
418 --progErrorColor: #bd362f;800 --progErrorColor: #bd362f;
419 --progAbortedColor: #d78872;801 --progAbortedColor: #d78872;
802 flex: 0 0 auto;
420 height: 0.5em;803 height: 0.5em;
421 background-color: var(--black50a);804 background-color: var(--black50a);
422 position: relative;805 position: relative;
@@ -469,6 +852,7 @@
469 overflow: auto;852 overflow: auto;
470 min-width: 100%;853 min-width: 100%;
471 width: 0;854 width: 0;
855 white-space: pre-wrap;
472}856}
473.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {857.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
474 display: block;858 display: block;
@@ -476,6 +860,150 @@
476.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {860.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {
477 content: 'Result: ';861 content: 'Result: ';
478}862}
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}
479@keyframes qr--progressPulse {1007@keyframes qr--progressPulse {
480 0%,1008 0%,
481 100% {1009 100% {
@@ -485,9 +1013,63 @@
485 background-color: var(--progFlashColor);1013 background-color: var(--progFlashColor);
486 }1014 }
487}1015}
1016@keyframes qr--debugPulse {
1017 0%,
1018 100% {
1019 border-color: #51a351;
1020 }
1021 50% {
1022 border-color: #92befc;
1023 }
1024}
488.popup.qr--hide {1025.popup.qr--hide {
489 opacity: 0 !important;1026 opacity: 0 !important;
490}1027}
491.popup.qr--hide::backdrop {1028.popup.qr--hide::backdrop {
492 opacity: 0 !important;1029 opacity: 0 !important;
493}1030}
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+622 -127
@@ -1,3 +1,18 @@
1@keyframes qr--success {
2 0%, 100% {
3 color: var(--SmartThemeBodyColor);
4 }
5 25%, 75% {
6 color: rgb(81, 163, 81);
7 }
8}
9&.qr--success {
10 animation-name: qr--success;
11 animation-duration: 3s;
12 animation-timing-function: linear;
13 animation-delay: 0s;
14 animation-iteration-count: 1;
15}
1#qr--bar {16#qr--bar {
2 outline: none;17 outline: none;
3 margin: 0;18 margin: 0;
@@ -50,6 +65,18 @@
50#qr--bar,65#qr--bar,
51#qr--popout>.qr--body {66#qr--popout>.qr--body {
52 >.qr--buttons {67 >.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 }
53 margin: 0;80 margin: 0;
54 padding: 0;81 padding: 0;
55 display: flex;82 display: flex;
@@ -60,6 +87,25 @@
6087
61 >.qr--buttons {88 >.qr--buttons {
62 display: contents;89 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 }
63 }109 }
64110
65 .qr--button {111 .qr--button {
@@ -75,10 +121,17 @@
75 align-items: center;121 align-items: center;
76 justify-content: center;122 justify-content: center;
77 text-align: center;123 text-align: center;
124 position: relative;
78125
79 &:hover {126 &: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;
82 }135 }
83136
84 >.qr--button-expander {137 >.qr--button-expander {
@@ -211,14 +264,41 @@
211 .qr--set-qrListContents> {264 .qr--set-qrListContents> {
212 padding: 0 0.5em;265 padding: 0 0.5em;
213266
214 >.qr--set-item {267 >.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 {
215 display: flex;295 display: flex;
216 flex-direction: row;296 flex-direction: row;
217 gap: 0.5em;297 gap: 0.5em;
218 align-items: baseline;298 align-items: baseline;
219 padding: 0.25em 0;299 padding: 0.25em 0;
220300
221 > :nth-child(1) {301 > :nth-child(2) {
222 flex: 0 0 auto;302 flex: 0 0 auto;
223 }303 }
224304
@@ -235,13 +315,29 @@
235 }315 }
236316
237 > :nth-child(5) {317 > :nth-child(5) {
238 flex: 0 0 auto;318 flex: 0 1 auto;
319 display: flex;
320 gap: 0.25em;
321 justify-content: flex-end;
322 flex-wrap: wrap;
239 }323 }
240324
241 >.drag-handle {325 >.drag-handle {
242 padding: 0.75em;326 padding: 0.75em;
243 }327 }
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
245 .qr--set-itemLabel,341 .qr--set-itemLabel,
246 .qr--action {342 .qr--action {
247 margin: 0;343 margin: 0;
@@ -251,6 +347,8 @@
251 font-size: smaller;347 font-size: smaller;
252 }348 }
253 }349 }
350
351
254 }352 }
255 }353 }
256354
@@ -270,6 +368,7 @@
270#qr--qrOptions {368#qr--qrOptions {
271 display: flex;369 display: flex;
272 flex-direction: column;370 flex-direction: column;
371 padding-right: 1px;
273372
274 >#qr--ctxEditor {373 >#qr--ctxEditor {
275 .qr--ctxItem {374 .qr--ctxItem {
@@ -279,6 +378,15 @@
279 align-items: baseline;378 align-items: baseline;
280 }379 }
281 }380 }
381 >#qr--autoExec {
382 .checkbox_label {
383 text-wrap: nowrap;
384
385 .fa-fw {
386 margin-right: 2px;
387 }
388 }
389 }
282}390}
283391
284392
@@ -297,8 +405,8 @@
297 }405 }
298406
299 >#qr--main>.qr--modal-messageContainer>#qr--modal-messageHolder {407 >#qr--main>.qr--modal-messageContainer>#qr--modal-messageHolder {
300 min-height: 50svh;408 min-height: 50dvh;
301 height: 50svh;409 height: 50dvh;
302 }410 }
303 }411 }
304}412}
@@ -306,6 +414,78 @@
306.popup:has(#qr--modalEditor) {414.popup:has(#qr--modalEditor) {
307 aspect-ratio: unset;415 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
309 .popup-content {489 .popup-content {
310 display: flex;490 display: flex;
311 flex-direction: column;491 flex-direction: column;
@@ -317,140 +497,262 @@
317 gap: 1em;497 gap: 1em;
318 overflow: hidden;498 overflow: hidden;
319499
320 >#qr--main {500 &.qr--isExecuting {
321 flex: 1 1 auto;501 #qr--main > h3:first-child,
322 display: flex;502 #qr--main > .qr--labels,
323 flex-direction: column;503 #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
324 overflow: hidden;504 #qr--qrOptions > h3:first-child,
325505 #qr--qrOptions > #qr--ctxEditor,
326 >.qr--labels {506 #qr--qrOptions > .qr--ctxEditorActions,
327 flex: 0 0 auto;507 #qr--qrOptions > .qr--ctxEditorActions + h3,
328 display: flex;508 #qr--qrOptions > .qr--ctxEditorActions + h3 + div
329 flex-direction: row;509 {
330 gap: 0.5em;510 display: none;
331511 }
332 >label {512 #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
333 flex: 1 1 1px;513 visibility: hidden;
334 display: flex;514 }
335 flex-direction: column;515 #qr--modal-debugButtons {
336516 display: flex;
337 >.qr--labelText {517 .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
338 flex: 1 1 auto;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;
339 }536 }
340537 &#qr--modal-resume {
341 >.qr--labelHint {538 border-color: rgb(81, 163, 81);
342 flex: 1 1 auto;
343 }539 }
344540 &#qr--modal-step {
345 >input {541 border-color: var(--SmartThemeQuoteColor);
346 flex: 0 0 auto;542 }
543 &#qr--modal-stepInto {
544 border-color: var(--SmartThemeQuoteColor);
545 }
546 &#qr--modal-stepOut {
547 border-color: var(--SmartThemeQuoteColor);
347 }548 }
348 }549 }
349 }550 }
350551 #qr--resizeHandle {
351 >.qr--modal-messageContainer {552 width: 6px;
352 flex: 1 1 auto;553 background-color: var(--SmartThemeBorderColor);
353 display: flex;554 border: 2px solid var(--SmartThemeBlurTintColor);
354 flex-direction: column;555 transition: border-color 200ms, background-color 200ms;
355 overflow: hidden;556 cursor: w-resize;
356557 &:hover {
357 >.qr--modal-editorSettings {558 background-color: var(--SmartThemeQuoteColor);
358 display: flex;559 border-color: var(--SmartThemeQuoteColor);
359 flex-direction: row;560 }
360 gap: 1em;561 }
361 color: var(--grey70);562 #qr--qrOptions {
362 font-size: smaller;563 width: var(--width, auto);
363 align-items: baseline;564 }
364565 }
365 >.checkbox_label {566
366 white-space: nowrap;567 > #qr--main {
367568 flex: 1 1 auto;
368 >input {569 display: flex;
369 font-size: inherit;570 flex-direction: column;
370 }571 overflow: hidden;
572 > .qr--labels {
573 flex: 0 0 auto;
574 display: flex;
575 flex-direction: row;
576 gap: 0.5em;
577 padding: 1px;
578 > label, > .label {
579 flex: 1 1 1px;
580 display: flex;
581 flex-direction: column;
582 position: relative;
583 &.qr--fit {
584 flex: 0 0 auto;
585 justify-content: center;
371 }586 }
372 }587 .qr--inputGroup {
373588 display: flex;
374 >#qr--modal-messageHolder {589 align-items: baseline;
375 flex: 1 1 auto;590 gap: 0.5em;
376 display: grid;591 input {
377 text-align: left;592 flex: 1 1 auto;
378 overflow: hidden;
379
380 &.qr--noSyntax {
381 >#qr--modal-messageSyntax {
382 display: none;
383 }593 }
384594 }
385 >#qr--modal-message {595 .qr--labelText {
386 background-color: var(--ac-style-color-background);596 flex: 1 1 auto;
387 color: var(--ac-style-color-text);597 }
388598 .qr--labelHint {
389 &::selection {599 flex: 1 1 auto;
390 color: unset;600 }
391 background-color: rgba(108 171 251 / 0.25);601 input {
392602 flex: 0 0 auto;
393 @supports (color: rgb(from white r g b / 0.25)) {603 }
394 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);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;
395 }635 }
396 }636 }
397 }637 }
398 }638 .qr--label {
399639 white-space: nowrap;
400 >#qr--modal-messageSyntax {640 .menu_button {
401 grid-column: 1;641 display: inline-block;
402 grid-row: 1;642 height: min-content;
403 padding: 0;643 width: min-content;
404 margin: 0;644 margin: 0 0.5em 0 0;
405 border: none;645 }
406 overflow: hidden;
407 min-width: 100%;
408 width: 0;
409
410 >#qr--modal-messageSyntaxInner {
411 height: 100%;
412 }646 }
413 }647 .qr--id {
414648 opacity: 0.5;
415 >#qr--modal-message {649 &:before { content: "["; }
416 background-color: transparent;650 &:after { content: "]"; }
417 color: transparent;
418 grid-column: 1;
419 grid-row: 1;
420 caret-color: var(--ac-style-color-text);
421 overflow: auto;
422
423 &::-webkit-scrollbar,
424 &::-webkit-scrollbar-thumb {
425 visibility: hidden;
426 cursor: default;
427 }651 }
428652 .qr--message {
429 &::selection {653 height: 1lh;
430 color: transparent;654 overflow: hidden;
431 background-color: rgba(108 171 251 / 0.25);655 text-overflow: ellipsis;
432656 white-space: nowrap;
433 @supports (color: rgb(from white r g b / 0.25)) {657 opacity: 0.5;
434 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
435 }
436 }658 }
437 }659 }
438660 }
439 #qr--modal-message,661 }
440 #qr--modal-messageSyntaxInner {662 > .qr--modal-messageContainer {
441 font-family: var(--monoFontFamily);663 flex: 1 1 auto;
442 padding: 0.75em;664 display: flex;
443 margin: 0;665 flex-direction: column;
444 border: none;666 overflow: hidden;
445 resize: none;667 > .qr--modal-editorSettings {
446 line-height: 1.2;668 display: flex;
447 border: 1px solid var(--SmartThemeBorderColor);669 flex-wrap: wrap;
448 border-radius: 5px;670 flex-direction: row;
449 }671 column-gap: 1em;
450 }672 color: var(--grey70);
451 }673 font-size: smaller;
674 align-items: baseline;
675 > .checkbox_label {
676 white-space: nowrap;
677 > input {
678 font-size: inherit;
679 }
680 }
681 }
682 > #qr--modal-messageHolder {
683 flex: 1 1 auto;
684 display: grid;
685 text-align: left;
686 overflow: hidden;
687 &.qr--noSyntax {
688 > #qr--modal-messageSyntax {
689 display: none;
690 }
691 > #qr--modal-message {
692 background-color: var(--ac-style-color-background);
693 color: var(--ac-style-color-text);
694 &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
695 visibility: visible;
696 cursor: unset;
697 }
698 &::selection {
699 color: unset;
700 background-color: rgba(108 171 251 / 0.25);
701 @supports (color: rgb(from white r g b / 0.25)) {
702 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
703 }
704 }
705 }
706 }
707 > #qr--modal-messageSyntax {
708 grid-column: 1;
709 grid-row: 1;
710 padding: 0;
711 margin: 0;
712 border: none;
713 overflow: hidden;
714 min-width: 100%;
715 width: 0;
716 > #qr--modal-messageSyntaxInner {
717 height: 100%;
718 }
719 }
720 > #qr--modal-message {
721 background-color: transparent;
722 color: transparent;
723 grid-column: 1;
724 grid-row: 1;
725 caret-color: var(--ac-style-color-text);
726 overflow: auto;
727 &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
728 visibility: hidden;
729 cursor: default;
730 }
731 &::selection {
732 color: transparent;
733 background-color: rgba(108 171 251 / 0.25);
734 @supports (color: rgb(from white r g b / 0.25)) {
735 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
736 }
737 }
738 }
739 #qr--modal-message, #qr--modal-messageSyntaxInner {
740 font-family: var(--monoFontFamily);
741 padding: 0.75em;
742 margin: 0;
743 resize: none;
744 line-height: 1.2;
745 border: 1px solid var(--SmartThemeBorderColor);
746 border-radius: 5px;
747 position: relative;
748 }
749 }
750 }
751 }
752 #qr--modal-icon {
753 height: 100%;
754 aspect-ratio: 1 / 1;
452 }755 }
453
454 #qr--modal-executeButtons {756 #qr--modal-executeButtons {
455 display: flex;757 display: flex;
456 gap: 1em;758 gap: 1em;
@@ -510,6 +812,47 @@
510 border-color: rgb(215, 136, 114);812 border-color: rgb(215, 136, 114);
511 }813 }
512 }814 }
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
514 #qr--modal-executeProgress {857 #qr--modal-executeProgress {
515 --prog: 0;858 --prog: 0;
@@ -518,6 +861,7 @@
518 --progSuccessColor: rgb(81, 163, 81);861 --progSuccessColor: rgb(81, 163, 81);
519 --progErrorColor: rgb(189, 54, 47);862 --progErrorColor: rgb(189, 54, 47);
520 --progAbortedColor: rgb(215, 136, 114);863 --progAbortedColor: rgb(215, 136, 114);
864 flex: 0 0 auto;
521 height: 0.5em;865 height: 0.5em;
522 background-color: var(--black50a);866 background-color: var(--black50a);
523 position: relative;867 position: relative;
@@ -588,6 +932,135 @@
588 overflow: auto;932 overflow: auto;
589 min-width: 100%;933 min-width: 100%;
590 width: 0;934 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 }
591 }1064 }
592 }1065 }
593 }1066 }
@@ -605,10 +1078,75 @@
605 }1078 }
606}1079}
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
608.popup.qr--hide {1092.popup.qr--hide {
609 opacity: 0 !important;1093 opacity: 0 !important;
1094 &::backdrop {
1095 opacity: 0 !important;
1096 }
610}1097}
6111098
612.popup.qr--hide::backdrop {1099.popup.qr--hide::backdrop {
613 opacity: 0 !important;1100 opacity: 0 !important;
614}1101}
1102
1103
1104
1105.popup:has(.qr--transferModal) {
1106 .popup-button-ok {
1107 &:after {
1108 content: 'Transfer';
1109 height: 0;
public/scripts/extensions/regex/editor.html+0 -0
public/scripts/extensions/shared.js+0 -0
public/scripts/extensions/stable-diffusion/button.html+0 -0
public/scripts/extensions/stable-diffusion/index.js+0 -0
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