Merge pull request #2705 from SillyTavern/staging Staging

2428c3979fe6dee360426b72fe6aa88e46091dd8

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

Signed
134 files changed, +6095 -856Ignore whitespace
.eslintrc.js+1 -0
@@ -55,6 +55,7 @@ module.exports = {
5555 isProbablyReaderable: 'readonly',
5656 ePub: 'readonly',
5757 diff_match_patch: 'readonly',
58+ SillyTavern: 'readonly',
5859 },
5960 },
6061 ],
default/config.yaml+39 -5
@@ -4,8 +4,22 @@ dataRoot: ./data
44# -- SERVER CONFIGURATION --
55# Listen for incoming connections
66listen: false
7+# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
8+protocol:
9+ ipv4: true
10+ ipv6: false
11+# Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6
12+dnsPreferIPv6: false
13+# The hostname that autorun opens.
14+# - Use "auto" to let the server decide
15+# - Use options like 'localhost', 'st.example.com'
16+autorunHostname: "auto"
717# Server port
818port: 8000
19+# Overrides the port for autorun in browser.
20+# - Use -1 to use the server port.
21+# - Specify a port to override the default.
22+autorunPortOverride: -1
923# -- SECURITY CONFIGURATION --
1024# Toggle whitelist mode
1125whitelistMode: true
@@ -13,6 +27,7 @@ whitelistMode: true
1327enableForwardedWhitelist: true
1428# Whitelist of allowed IP addresses
1529whitelist:
30+ - ::1
1631 - 127.0.0.1
1732# Toggle basic authentication for endpoints
1833basicAuthMode: false
@@ -26,6 +41,11 @@ enableCorsProxy: false
2641enableUserAccounts: false
2742# Enable discreet login mode: hides user list on the login screen
2843enableDiscreetLogin: false
44+# User session timeout *in seconds* (defaults to 24 hours).
45+## Set to a positive number to expire session after a certain time of inactivity
46+## Set to 0 to expire session when the browser is closed
47+## Set to a negative number to disable session expiration
48+sessionTimeout: 86400
2949# Used to sign session cookies. Will be auto-generated if not set
3050cookieSecret: ''
3151# Disable CSRF protection - NOT RECOMMENDED
@@ -35,6 +55,9 @@ securityOverride: false
3555# -- ADVANCED CONFIGURATION --
3656# Open the browser automatically
3757autorun: true
58+# Avoids using 'localhost' for autorun in auto mode.
59+# use if you don't have 'localhost' in your hosts file
60+avoidLocalhost: false
3861# Disable thumbnail generation
3962disableThumbnails: false
4063# Thumbnail quality (0-100)
@@ -98,10 +121,21 @@ mistral:
98121 # Enables prefilling of the reply with the last assistant message in the prompt
99122 # CAUTION: The prefix is echoed into the completion. You may want to use regex to trim it out.
100123 enablePrefix: false
124+# -- OLLAMA API CONFIGURATION --
125+ollama:
126+ # Controls how long the model will stay loaded into memory following the request
127+ # * -1: Keep the model loaded indefinitely
128+ # * 0: Unload the model immediately after the request
129+ # * N (any positive number): Keep the model loaded for N seconds after the request.
130+ keepAlive: -1
131+# -- ANTHROPIC CLAUDE API CONFIGURATION --
132+claude:
133+ # Enables caching of the system prompt (if supported).
134+ # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
135+ # -- IMPORTANT! --
136+ # Use only when the prompt before the chat history is static and doesn't change between requests
137+ # (e.g {{random}} macro or lorebooks not as in-chat injections).
138+ # Otherwise, you'll just waste money on cache misses.
139+ enableSystemPromptCache: false
101140# -- SERVER PLUGIN CONFIGURATION --
102141enableServerPlugins: false
103-# User session timeout *in seconds* (defaults to 24 hours).
104-## Set to a positive number to expire session after a certain time of inactivity
105-## Set to 0 to expire session when the browser is closed
106-## Set to a negative number to disable session expiration
107-sessionTimeout: 86400
default/content/presets/openai/Default.json+4 -4
@@ -1,7 +1,7 @@
11{
22 "chat_completion_source": "openai",
33 "openai_model": "gpt-3.54-turbo",
44 "claude_model": "claude-instant3-v15-sonnet-20240620",
55 "windowai_model": "",
66 "openrouter_model": "OR_Website",
77 "openrouter_use_fallback": false,
@@ -9,7 +9,7 @@
99 "openrouter_group_models": false,
1010 "openrouter_sort_models": "alphabetically",
1111 "ai21_model": "j2-ultra",
1212 "mistralai_model": "mistral-mediumlarge-latest",
1313 "custom_model": "",
1414 "custom_url": "",
1515 "custom_include_body": "",
@@ -22,7 +22,7 @@
2222 "count_penalty": 0,
2323 "top_p": 1,
2424 "top_k": 0,
2525 "top_a": 10,
2626 "min_p": 0,
2727 "repetition_penalty": 1,
2828 "openai_max_context": 4095,
default/content/settings.json+3 -3
@@ -610,9 +610,9 @@
610610 }
611611 ]
612612 },
613- "wi_format": "[Details of the fictional world the RP is set in:\n{0}]\n",
613+ "wi_format": "{0}",
614614 "openai_model": "gpt-3.54-turbo",
615615 "claude_model": "claude-instant3-v15-sonnet-20240620",
616616 "ai21_model": "j2-ultra",
617617 "windowai_model": "",
618618 "openrouter_model": "OR_Website",
index.d.ts+5 -0
@@ -9,6 +9,11 @@ declare global {
99 };
1010 }
1111 }
12+
13+ /**
14+ * The root directory for user data.
15+ */
16+ var DATA_ROOT: string;
1217}
1318
1419declare module 'express-session' {
package-lock.json+54 -27
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.12.45",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.12.45",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
@@ -27,6 +27,7 @@
2727 "google-translate-api-browser": "^3.0.1",
2828 "he": "^1.2.0",
2929 "helmet": "^7.1.0",
30+ "iconv-lite": "^0.6.3",
3031 "ip-matching": "^2.1.2",
3132 "ipaddr.js": "^2.0.1",
3233 "jimp": "^0.22.10",
@@ -42,7 +43,7 @@
4243 "rate-limiter-flexible": "^5.0.0",
4344 "response-time": "^2.3.2",
4445 "sanitize-filename": "^1.6.3",
4546 "sillytavern-transformers": "^2.14.6",
4647 "simple-git": "^3.19.1",
4748 "tiktoken": "^1.0.15",
4849 "vectra": "^0.2.2",
@@ -58,7 +59,7 @@
5859 },
5960 "devDependencies": {
6061 "@types/jquery": "^3.5.29",
6162 "eslint": "^8.5557.0",
6263 "jquery": "^3.6.4"
6364 },
6465 "engines": {
@@ -166,9 +167,9 @@
166167 "license": "MIT"
167168 },
168169 "node_modules/@eslint/js": {
169170 "version": "8.5557.0",
170171 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.5557.0.tgz",
171172 "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLoYs+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/nvNMpJOaJATj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
172173 "dev": true,
173174 "license": "MIT",
174175 "engines": {
@@ -185,14 +186,15 @@
185186 }
186187 },
187188 "node_modules/@humanwhocodes/config-array": {
188189 "version": "0.11.1314",
189190 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.1314.tgz",
190191 "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
192+ "deprecated": "Use @eslint/config-array instead",
191193 "dev": true,
192194 "license": "Apache-2.0",
193195 "dependencies": {
194196 "@humanwhocodes/object-schema": "^2.0.12",
195197 "debug": "^4.13.1",
196198 "minimatch": "^3.0.5"
197199 },
198200 "engines": {
@@ -200,9 +202,9 @@
200202 }
201203 },
202204 "node_modules/@humanwhocodes/config-array/node_modules/debug": {
203205 "version": "4.3.46",
204206 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.46.tgz",
205207 "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7GO/vCNNhehwxfkQ09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
206208 "dev": true,
207209 "license": "MIT",
208210 "dependencies": {
@@ -239,9 +241,10 @@
239241 }
240242 },
241243 "node_modules/@humanwhocodes/object-schema": {
242244 "version": "2.0.13",
243245 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.13.tgz",
244246 "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+1GWiExi9QyzlD8x/yTvvLDHpoxLr0xxxeslWw08Zt7wIKcDcA==",
247+ "deprecated": "Use @eslint/object-schema instead",
245248 "dev": true,
246249 "license": "BSD-3-Clause"
247250 },
@@ -1490,6 +1493,18 @@
14901493 "node": ">= 0.8"
14911494 }
14921495 },
1496+ "node_modules/body-parser/node_modules/iconv-lite": {
1497+ "version": "0.4.24",
1498+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1499+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1500+ "license": "MIT",
1501+ "dependencies": {
1502+ "safer-buffer": ">= 2.1.2 < 3"
1503+ },
1504+ "engines": {
1505+ "node": ">=0.10.0"
1506+ }
1507+ },
14931508 "node_modules/boolbase": {
14941509 "version": "1.0.0",
14951510 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -2455,17 +2470,17 @@
24552470 }
24562471 },
24572472 "node_modules/eslint": {
24582473 "version": "8.5557.0",
24592474 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.5557.0.tgz",
24602475 "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdtdZ6+rCntju0xEH7teIABPwXpahftIaTdAmexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
24612476 "dev": true,
24622477 "license": "MIT",
24632478 "dependencies": {
24642479 "@eslint-community/eslint-utils": "^4.2.0",
24652480 "@eslint-community/regexpp": "^4.6.1",
24662481 "@eslint/eslintrc": "^2.1.4",
24672482 "@eslint/js": "8.5557.0",
24682483 "@humanwhocodes/config-array": "^0.11.1314",
24692484 "@humanwhocodes/module-importer": "^1.0.1",
24702485 "@nodelib/fs.walk": "^1.2.8",
24712486 "@ungap/structured-clone": "^1.2.0",
@@ -3280,12 +3295,12 @@
32803295 }
32813296 },
32823297 "node_modules/iconv-lite": {
32833298 "version": "0.46.243",
32843299 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.46.243.tgz",
32853300 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
32863301 "license": "MIT",
32873302 "dependencies": {
32883303 "safer-buffer": ">= 2.1.2 < 3.0.0"
32893304 },
32903305 "engines": {
32913306 "node": ">=0.10.0"
@@ -4616,6 +4631,18 @@
46164631 "node": ">= 0.8"
46174632 }
46184633 },
4634+ "node_modules/raw-body/node_modules/iconv-lite": {
4635+ "version": "0.4.24",
4636+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
4637+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
4638+ "license": "MIT",
4639+ "dependencies": {
4640+ "safer-buffer": ">= 2.1.2 < 3"
4641+ },
4642+ "engines": {
4643+ "node": ">=0.10.0"
4644+ }
4645+ },
46194646 "node_modules/readable-stream": {
46204647 "version": "2.3.8",
46214648 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
package.json+4 -3
@@ -17,6 +17,7 @@
1717 "google-translate-api-browser": "^3.0.1",
1818 "he": "^1.2.0",
1919 "helmet": "^7.1.0",
20+ "iconv-lite": "^0.6.3",
2021 "ip-matching": "^2.1.2",
2122 "ipaddr.js": "^2.0.1",
2223 "jimp": "^0.22.10",
@@ -32,7 +33,7 @@
3233 "rate-limiter-flexible": "^5.0.0",
3334 "response-time": "^2.3.2",
3435 "sanitize-filename": "^1.6.3",
3536 "sillytavern-transformers": "^2.14.6",
3637 "simple-git": "^3.19.1",
3738 "tiktoken": "^1.0.15",
3839 "vectra": "^0.2.2",
@@ -70,7 +71,7 @@
7071 "type": "git",
7172 "url": "https://github.com/SillyTavern/SillyTavern.git"
7273 },
7374 "version": "1.12.45",
7475 "scripts": {
7576 "start": "node server.js",
7677 "start:no-csrf": "node server.js --disableCsrf",
@@ -90,7 +91,7 @@
9091 "main": "server.js",
9192 "devDependencies": {
9293 "@types/jquery": "^3.5.29",
9394 "eslint": "^8.5557.0",
9495 "jquery": "^3.6.4"
9596 }
9697}
public/css/character-group-overlay.css+1 -1
@@ -99,6 +99,6 @@
9999}
100100
101101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
102102 width: 100pxunset;
103103 padding: 0.25em;
104104}
public/css/world-info.css+12 -0
@@ -120,6 +120,14 @@
120120 flex-wrap: wrap;
121121}
122122
123+.world_entry .inline-drawer-header {
124+ cursor: initial;
125+}
126+
127+.world_entry .killSwitch {
128+ cursor: pointer;
129+}
130+
123131.world_entry_form_control input[type=button] {
124132 cursor: pointer;
125133}
@@ -173,6 +181,10 @@
173181 width: 7em;
174182}
175183
184+.world_entry .killSwitch.fa-toggle-on {
185+ color: var(--SmartThemeQuoteColor);
186+}
187+
176188.wi-card-entry {
177189 border: 1px solid;
178190 border-color: var(--SmartThemeBorderColor);
public/global.d.ts+5 -0
@@ -14,6 +14,11 @@ declare var isProbablyReaderable;
1414declare var ePub;
1515declare var ai;
1616
17+declare var SillyTavern: {
18+ getContext(): any;
19+ llm: any;
20+};
21+
1722// Jquery plugins
1823interface JQuery {
1924 nanogallery2(options?: any): JQuery;
public/img/blockentropy.svg+3 -0
@@ -0,0 +1,3 @@
1+<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236.38 282.41">
2+ <path d="M126.55,0v54.44l-79.87,33.76v93.95l27.53-12.94,43.08,31.09.04-.05v.09l55.21-31.44.13-.08v-80.06l-55.34,24.92v80.2l-42.55-30.7h-.02s0-81.16,0-81.16l57.02-24.11V9.23l93.54,56.12v22.51l-24.34,11.53,1.84,90.56-88.45,51.47-.13.08v34.46L5.23,198.97v-65.56H0v66.92c0,.85.41,1.64,1.11,2.14l113.13,79.91v.05l.04-.02h0s0,0,0,0l121.97-73.54.13-.08v-126.13l-5.84,2.76v-22.94h-.3l.11-.18L126.55,0Z" />
3+</svg>
public/img/step-into.svg+149 -0
@@ -0,0 +1,149 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ width="48"
6+ height="48"
7+ viewBox="0 0 48 48"
8+ version="1.1"
9+ id="svg2120"
10+ xml:space="preserve"
11+ inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12+ sodipodi:docname="step-into.svg"
13+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15+ xmlns:xlink="http://www.w3.org/1999/xlink"
16+ xmlns="http://www.w3.org/2000/svg"
17+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18+ id="namedview2122"
19+ pagecolor="#ffffff"
20+ bordercolor="#000000"
21+ borderopacity="0.25"
22+ inkscape:showpageshadow="2"
23+ inkscape:pageopacity="0.0"
24+ inkscape:pagecheckerboard="0"
25+ inkscape:deskcolor="#d1d1d1"
26+ inkscape:document-units="px"
27+ showgrid="false"
28+ inkscape:zoom="11.313708"
29+ inkscape:cx="58.910834"
30+ inkscape:cy="25.323262"
31+ inkscape:window-width="1920"
32+ inkscape:window-height="992"
33+ inkscape:window-x="-8"
34+ inkscape:window-y="-8"
35+ inkscape:window-maximized="1"
36+ inkscape:current-layer="g2714" /><defs
37+ id="defs2117"><inkscape:path-effect
38+ effect="spiro"
39+ id="path-effect2144"
40+ is_visible="true"
41+ lpeversion="1" /><inkscape:path-effect
42+ effect="bspline"
43+ id="path-effect2138"
44+ is_visible="true"
45+ lpeversion="1"
46+ weight="33.333333"
47+ steps="2"
48+ helper_size="0"
49+ apply_no_weight="true"
50+ apply_with_weight="true"
51+ only_selected="false" /></defs><g
52+ inkscape:groupmode="layer"
53+ id="layer4"
54+ inkscape:label="img"
55+ style="display:none"><image
56+ width="305.68866"
57+ height="70.374367"
58+ preserveAspectRatio="none"
59+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60+eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61+SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62+AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
63+4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64+GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65+G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66+kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67+qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68+KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69+qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70+Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71+sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72+SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73+tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74+Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
75+0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76+WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77+UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78+Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80+i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81+W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82+HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83+O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84+Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85+ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86+AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87+"
88+ id="image2132"
89+ x="-82"
90+ y="-11.9" /></g><g
91+ inkscape:groupmode="layer"
92+ id="layer5"
93+ inkscape:label="dot" /><g
94+ inkscape:label="over"
95+ inkscape:groupmode="layer"
96+ id="layer1"
97+ style="display:none"><ellipse
98+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99+ id="path2637"
100+ cx="24"
101+ cy="33"
102+ rx="4.3600898"
103+ ry="4.3600893" /><path
104+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105+ d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106+ id="path2142"
107+ inkscape:path-effect="#path-effect2144"
108+ inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109+ sodipodi:nodetypes="csc" /><path
110+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111+ d="M 26,22 H 39 V 9"
112+ id="path2626"
113+ sodipodi:nodetypes="ccc" /></g><g
114+ inkscape:groupmode="layer"
115+ id="layer6"
116+ inkscape:label="into"
117+ style="display:inline"><ellipse
118+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119+ id="path2637-3"
120+ cx="24"
121+ cy="38.5"
122+ rx="4.3600898"
123+ ry="4.3600893" /><path
124+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125+ d="M 24,2 V 24"
126+ id="path2668"
127+ sodipodi:nodetypes="cc" /><path
128+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129+ d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130+ id="path2626-8"
131+ sodipodi:nodetypes="ccc" /></g><g
132+ inkscape:groupmode="layer"
133+ id="g2714"
134+ inkscape:label="out"
135+ style="display:none"><ellipse
136+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137+ id="ellipse2708"
138+ cx="24"
139+ cy="38.5"
140+ rx="4.3600898"
141+ ry="4.3600893" /><path
142+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143+ d="M 24,29.722858 V 7.7228579"
144+ id="path2710"
145+ sodipodi:nodetypes="cc" /><path
146+ style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147+ d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148+ id="path2712"
149+ sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-out.svg+149 -0
@@ -0,0 +1,149 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ width="48"
6+ height="48"
7+ viewBox="0 0 48 48"
8+ version="1.1"
9+ id="svg2120"
10+ xml:space="preserve"
11+ inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12+ sodipodi:docname="step-out.svg"
13+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15+ xmlns:xlink="http://www.w3.org/1999/xlink"
16+ xmlns="http://www.w3.org/2000/svg"
17+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18+ id="namedview2122"
19+ pagecolor="#ffffff"
20+ bordercolor="#000000"
21+ borderopacity="0.25"
22+ inkscape:showpageshadow="2"
23+ inkscape:pageopacity="0.0"
24+ inkscape:pagecheckerboard="0"
25+ inkscape:deskcolor="#d1d1d1"
26+ inkscape:document-units="px"
27+ showgrid="false"
28+ inkscape:zoom="11.313708"
29+ inkscape:cx="58.910834"
30+ inkscape:cy="25.323262"
31+ inkscape:window-width="1920"
32+ inkscape:window-height="992"
33+ inkscape:window-x="-8"
34+ inkscape:window-y="-8"
35+ inkscape:window-maximized="1"
36+ inkscape:current-layer="g2714" /><defs
37+ id="defs2117"><inkscape:path-effect
38+ effect="spiro"
39+ id="path-effect2144"
40+ is_visible="true"
41+ lpeversion="1" /><inkscape:path-effect
42+ effect="bspline"
43+ id="path-effect2138"
44+ is_visible="true"
45+ lpeversion="1"
46+ weight="33.333333"
47+ steps="2"
48+ helper_size="0"
49+ apply_no_weight="true"
50+ apply_with_weight="true"
51+ only_selected="false" /></defs><g
52+ inkscape:groupmode="layer"
53+ id="layer4"
54+ inkscape:label="img"
55+ style="display:none"><image
56+ width="305.68866"
57+ height="70.374367"
58+ preserveAspectRatio="none"
59+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60+eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61+SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62+AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
63+4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64+GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65+G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66+kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67+qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68+KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69+qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70+Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71+sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72+SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73+tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74+Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
75+0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76+WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77+UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78+Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80+i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81+W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82+HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83+O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84+Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85+ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86+AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87+"
88+ id="image2132"
89+ x="-82"
90+ y="-11.9" /></g><g
91+ inkscape:groupmode="layer"
92+ id="layer5"
93+ inkscape:label="dot" /><g
94+ inkscape:label="over"
95+ inkscape:groupmode="layer"
96+ id="layer1"
97+ style="display:none"><ellipse
98+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99+ id="path2637"
100+ cx="24"
101+ cy="33"
102+ rx="4.3600898"
103+ ry="4.3600893" /><path
104+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105+ d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106+ id="path2142"
107+ inkscape:path-effect="#path-effect2144"
108+ inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109+ sodipodi:nodetypes="csc" /><path
110+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111+ d="M 26,22 H 39 V 9"
112+ id="path2626"
113+ sodipodi:nodetypes="ccc" /></g><g
114+ inkscape:groupmode="layer"
115+ id="layer6"
116+ inkscape:label="into"
117+ style="display:none"><ellipse
118+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119+ id="path2637-3"
120+ cx="24"
121+ cy="38.5"
122+ rx="4.3600898"
123+ ry="4.3600893" /><path
124+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125+ d="M 24,2 V 24"
126+ id="path2668"
127+ sodipodi:nodetypes="cc" /><path
128+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129+ d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130+ id="path2626-8"
131+ sodipodi:nodetypes="ccc" /></g><g
132+ inkscape:groupmode="layer"
133+ id="g2714"
134+ inkscape:label="out"
135+ style="display:inline"><ellipse
136+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137+ id="ellipse2708"
138+ cx="24"
139+ cy="38.5"
140+ rx="4.3600898"
141+ ry="4.3600893" /><path
142+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143+ d="M 24,29.722858 V 7.7228579"
144+ id="path2710"
145+ sodipodi:nodetypes="cc" /><path
146+ style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147+ d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148+ id="path2712"
149+ sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-over.svg+149 -0
@@ -0,0 +1,149 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ width="48"
6+ height="48"
7+ viewBox="0 0 48 48"
8+ version="1.1"
9+ id="svg2120"
10+ xml:space="preserve"
11+ inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12+ sodipodi:docname="step-over.svg"
13+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15+ xmlns:xlink="http://www.w3.org/1999/xlink"
16+ xmlns="http://www.w3.org/2000/svg"
17+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18+ id="namedview2122"
19+ pagecolor="#ffffff"
20+ bordercolor="#000000"
21+ borderopacity="0.25"
22+ inkscape:showpageshadow="2"
23+ inkscape:pageopacity="0.0"
24+ inkscape:pagecheckerboard="0"
25+ inkscape:deskcolor="#d1d1d1"
26+ inkscape:document-units="px"
27+ showgrid="false"
28+ inkscape:zoom="11.313708"
29+ inkscape:cx="58.910834"
30+ inkscape:cy="25.323262"
31+ inkscape:window-width="1920"
32+ inkscape:window-height="992"
33+ inkscape:window-x="-8"
34+ inkscape:window-y="-8"
35+ inkscape:window-maximized="1"
36+ inkscape:current-layer="g2714" /><defs
37+ id="defs2117"><inkscape:path-effect
38+ effect="spiro"
39+ id="path-effect2144"
40+ is_visible="true"
41+ lpeversion="1" /><inkscape:path-effect
42+ effect="bspline"
43+ id="path-effect2138"
44+ is_visible="true"
45+ lpeversion="1"
46+ weight="33.333333"
47+ steps="2"
48+ helper_size="0"
49+ apply_no_weight="true"
50+ apply_with_weight="true"
51+ only_selected="false" /></defs><g
52+ inkscape:groupmode="layer"
53+ id="layer4"
54+ inkscape:label="img"
55+ style="display:none"><image
56+ width="305.68866"
57+ height="70.374367"
58+ preserveAspectRatio="none"
59+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60+eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61+SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62+AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
63+4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64+GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65+G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66+kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67+qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68+KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69+qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70+Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71+sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72+SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73+tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74+Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
75+0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76+WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77+UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78+Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80+i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81+W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82+HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83+O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84+Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85+ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86+AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87+"
88+ id="image2132"
89+ x="-82"
90+ y="-11.9" /></g><g
91+ inkscape:groupmode="layer"
92+ id="layer5"
93+ inkscape:label="dot" /><g
94+ inkscape:label="over"
95+ inkscape:groupmode="layer"
96+ id="layer1"
97+ style="display:inline"><ellipse
98+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99+ id="path2637"
100+ cx="24"
101+ cy="33"
102+ rx="4.3600898"
103+ ry="4.3600893" /><path
104+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105+ d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106+ id="path2142"
107+ inkscape:path-effect="#path-effect2144"
108+ inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109+ sodipodi:nodetypes="csc" /><path
110+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111+ d="M 26,22 H 39 V 9"
112+ id="path2626"
113+ sodipodi:nodetypes="ccc" /></g><g
114+ inkscape:groupmode="layer"
115+ id="layer6"
116+ inkscape:label="into"
117+ style="display:none"><ellipse
118+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119+ id="path2637-3"
120+ cx="24"
121+ cy="38.5"
122+ rx="4.3600898"
123+ ry="4.3600893" /><path
124+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125+ d="M 24,2 V 24"
126+ id="path2668"
127+ sodipodi:nodetypes="cc" /><path
128+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129+ d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130+ id="path2626-8"
131+ sodipodi:nodetypes="ccc" /></g><g
132+ inkscape:groupmode="layer"
133+ id="g2714"
134+ inkscape:label="out"
135+ style="display:none"><ellipse
136+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137+ id="ellipse2708"
138+ cx="24"
139+ cy="38.5"
140+ rx="4.3600898"
141+ ry="4.3600893" /><path
142+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143+ d="M 24,29.722858 V 7.7228579"
144+ id="path2710"
145+ sodipodi:nodetypes="cc" /><path
146+ style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147+ d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148+ id="path2712"
149+ sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-resume.svg+218 -0
@@ -0,0 +1,218 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+
4+<svg
5+ width="48"
6+ height="48"
7+ viewBox="0 0 48 48"
8+ version="1.1"
9+ id="svg2120"
10+ xml:space="preserve"
11+ inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12+ sodipodi:docname="step-resume.svg"
13+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15+ xmlns:xlink="http://www.w3.org/1999/xlink"
16+ xmlns="http://www.w3.org/2000/svg"
17+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18+ id="namedview2122"
19+ pagecolor="#ffffff"
20+ bordercolor="#000000"
21+ borderopacity="0.25"
22+ inkscape:showpageshadow="2"
23+ inkscape:pageopacity="0.0"
24+ inkscape:pagecheckerboard="0"
25+ inkscape:deskcolor="#d1d1d1"
26+ inkscape:document-units="px"
27+ showgrid="false"
28+ inkscape:zoom="11.315"
29+ inkscape:cx="4.7282369"
30+ inkscape:cy="24.259832"
31+ inkscape:window-width="1920"
32+ inkscape:window-height="992"
33+ inkscape:window-x="-8"
34+ inkscape:window-y="-8"
35+ inkscape:window-maximized="1"
36+ inkscape:current-layer="layer8" /><defs
37+ id="defs2117"><inkscape:path-effect
38+ effect="spiro"
39+ id="path-effect2144"
40+ is_visible="true"
41+ lpeversion="1" /><inkscape:path-effect
42+ effect="bspline"
43+ id="path-effect2138"
44+ is_visible="true"
45+ lpeversion="1"
46+ weight="33.333333"
47+ steps="2"
48+ helper_size="0"
49+ apply_no_weight="true"
50+ apply_with_weight="true"
51+ only_selected="false" /></defs><g
52+ inkscape:groupmode="layer"
53+ id="layer4"
54+ inkscape:label="img"
55+ style="display:none"><image
56+ width="305.68866"
57+ height="70.374367"
58+ preserveAspectRatio="none"
59+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60+eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61+SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62+AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
63+4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64+GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65+G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66+kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67+qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68+KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69+qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70+Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71+sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72+SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73+tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74+Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
75+0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76+WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77+UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78+Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80+i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81+W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82+HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83+O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84+Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85+ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86+AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87+"
88+ id="image2132"
89+ x="-82"
90+ y="-11.9" /><image
91+ width="460"
92+ height="71"
93+ preserveAspectRatio="none"
94+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcwAAABHCAYAAACK23cpAAAABHNCSVQICAgIfAhkiAAACuZJREFU
95+eJzt3T9o3FgeB/Cv77aZBHInYZPD+JLF4iDxDl44ZNiAvVfNhC2vCEqxWQJpJn3MwlRXTeP0drNF
96+NtfIKa5cYnVxkQMLDoztbU6GgBkwDiPOEHuOK/aK7FM0Gs3M01gzkp6+HzBknPFEkSV93z/9NLO6
97+uvoLiIiIaKjfZL0BRERERcDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIi
98+ksDAJCIiksDAJCIiksDAJCIiksDAJCIikvBZ1htAlIZmswlN0wAAvu+j1WplvEX5YVkWOp0OHMfJ
99+elMoBYuLi8Gfj4+PM9yS8mFgEinMsiyYphm8ZmgSjY9DskSKioZlvV5HrVbLcIuoyEzTDEZxyoqB
100+SaSgaFgKDE0aR6PRCI6pMocmA5NIMYPCUmBoUhKNRgOGYQD4eOyUOTQZmEQKGRWWAkOTZPm+3/O6
101+zKHJwCRShGxYCgxNkmHbNlzX7fleWUNzqqtk5xaWMbuwHLx+d+Tg4vw0eH3txs2e19RPXBA1TQuW
102+lHc6nb5WYFj4ouj7ft/BT8UXF5ae50HX9Z7bbQD0XOTq9ToArp6l4WzbBoC+RWQA4Lru0OuPSqYa
103+mLMLy1i69yh4/f5kvycgv3nyI47evuwL0jKzLCuYPxjWmvM8D47jxIanOLDF+xiYahkUlltbW2g2
104+mz3fb7VaPfesAgxNksPQzOGQ7NK9R/jLgw3MhXqiZWSaJjY2NoJhj1FDH4ZhBCvZyjZMUmbDwnKQ
105+VqsV26ji8CyNUvbh2VwWLrh24ya+frCBs5N9uK+fl6q3aRgGarVa0Ksc5+ebzSZ2dnZK0+ors06n
106+0/N6VFgKcT1NVY8Vy7Lgui48z8t6Uwpr1HVpUE9T0zQ8ffp0rDAVo2F5GhHLXQ8zbG5hGd88+RG3
107+l2q4duNm1pszcaKXGHdQioPHtm3Yto2tra2hoViv15XsbY77f9I0DY1GQ7n94TgOdnZ2AMiHpRDu
108+acb1HFTQaDRgmmbP1AYlI86dUfsvrqfp+z42NzfHaowZhgHLsnI18pHLHmaUef+Z8r1NcVBGibnJ
109+uNZx+Hvi4AofrOJ7qhAnrWEYiU5CTdOCuTzLsmDbtlK9Kcdxxl7M1Wq1YJqmkmEZnf8Xv3v2NJMJ
110+r4GQfW+4IS9Cc9yeZp7m2H9769atv03rH5tbWMbcH78MXkcX94QXBEVdv3ETf/rzXwEAF+en+N9/
111+P0xuQ6csfEEXfN/HixcvgovhKL7vY3d3F7quY35+Pvi+rut97yvixdGyLFSrVQBApVJBtVrFwcEB
112+ut0uAGBtbQ2VSgUA0O12sbu7C6B/34r9U8R9MEy73Y79/qD9IvOzRddut1GtVoP/f6VSgWEYaLfb
113+V2owaZqGSqUSHHvTNu1h9MePH/e89jwPvu/3fIWvM4Zh4PLyEr7vB/uo2+3i4OAA8/PzfT876Cv6
114+mb7vZ36s5raHeXF+ig/np32Lf5buPcLnX9Thvn6Os5P9jLYuXdFe4FWetmHbNjqdTqJWYRG4rgvD
115+MIKLhZgbGdbTjGuIAPloqdLkxfVsxu1pip8LD0v6vo+9vb1SHU+Dhv3j6hYD/T3NJFMG0Tl2y7KC
116+sM5KbucwP5yf4s2r9dhhWLEo6OsHG4Wf24ybSBfLt8cVntdShed5fUOpwxYUDArLra0tDsmVSNwc
117+Wlz4DVOr1dBsNvver2ka6vV634W9jCaxejauMTzusG5acjske3F+indHDv5zdox//+sfuP67P+D3
118+c70HrBimFUO0RRymffjwYTBkBCS/oNdqteDADH/put43HAsUd0gWQDAkYxhGzzBbtVrtOYkqlQrW
119+1tb6fr6MYSkzJKs6MRw4zvCszDoAcQxOa99Oe0g2PFo17PpxeHjYNyUUNzwra9DvLTodM0257WFG
120+ua+f482r9dhFP+b9ZzDvPytcbzN6f6Xv+4kv6GIRTNyXigb1NEcpY1jSJ+P2NOMW4sXRNC1RWUJV
121+pd3THPZ7y6Knmds5zDhnJ/v46YfvcPerb/H5F/WegBS3oBSpUlD0BNvb28toS4pFhKbsScOwVEfc
122+MHsS0eNl2Jxm0kanqquNk0q7IpCYK15ZWQl+f4ZhBPt7mnOahelhhv38z78PXPRTpEpBKysrwZ99
123+3y/V4oGriutpxmFYqkWMyoz7Negz43qaSXswcVMgZZV2T9NxnL4Ohfi8aSpUDzPsQwF6kKOMuxI2
124+LMmqM9WM6mkyLEnWoIVjV/2MMptETzNq2o2UQgbm3a++HXjP5rsjB+7r51PeIsrKoNBkWKrpqsNv
125+g0It7ngRTwOSxeHYfmmFZtziK3HuT1OhAnNuYRl37z2KHW49O9nHz29fKnNv5iDiwPN9P6gjqlLV
126+mnFEQ5Nhqa6rjMokvdVIPPlHtucYretLH101NEXJ0LCkZSDTUpjANO8/w+2l+JqC7uvneHek9vyf
127+uH0kSnYuT3Xh1ibD8pNOp8OGFca7L9f3fdi2LbVSVpSwpHjjhmZcydCkBRDSlPvAvL1Ug3n/Wezf
128+qV5fVhhW+Fg8nUTV4tlJMCj7lXmOW7hKEQvP87CzszO0chYX7MlJGppiMVaYaMRkJbeBef3Xaj5x
129+w68X56dKlcYbRvZRX/V6PfOyUUR5k0bFJ8dx4LouS+OlIGloRhf1ZF08P7eBee3GzdhCBGVa1CNK
130+b8m+1zRNnrxEv0qzPKIYBhTzmWVqmIbncXVdly7mkITMU07ysDYht4EZVZZFPVdhGEZpA1MUZtc0
131+DcfHx8GCjbIr836JK2l31YtuWfZdWKfT6SleP6nbZ4aFpmEYmYclUJDALMOinjhJb8pVtRzeMHEr
132+6IBPcx15OMmywP2CYOW0OC/y0EMpItu2p1b0fFBo5mV9Rm6LrwMfe5VvXq3j/QR6lbOzs7i4uEj9
133+c9Ok63rwDEgZ4pmYZVGr1QYWxq5UKkGDI+n9dEXH/fJRt9uF53mYn5/H9va2MmE57eLr3W4X7XYb
134+mqZhZmam52ERk3CVgu2Tlsse5qQX9czOzuL777/H+vr6RD4/LUlP8DLdByY7v1uv16debzJL3C+9
135+srwFQSWe5020wSHzPM08yF0t2XdHDn764buJhuWTJ08m8tlpE08el1Wm+ctRj1wa971Fx/1CRTSJ
136+52lOwlR7mO9P9nH09mXwOloP9s2r9Yku6hFhOTs7O7F/I23RJ8YPMukWYN4kma8VC1/y1FKdFO4X
137+Kqq0a89OwlQD8+xkf2ggMiz7iUUawx5llVWZqKzkqcWZJ9wvVHR5D81czmGmrahhKXieh1arFRQx
138+0HUdmqbBdV24rluqnuW4sj7R8or7hfImz6GpfGAWPSzDHMcp1TzlIGJuV7ZHVZZQ4H4hVeQ1NHO3
139+6CdNKoUl9Yo+TDat9xYd9wupIo8LgZQNTIal2hzHkWpllq0wNvcLqWRQaGZVpEXJwGRYlsPm5ubQ
140+cMjiAbN5wP1CKomGpud5mVX+mWqln2lIEpZsYRdbt9vF7u4ufN9HpVIJqpC4rouDgwNsb2+Xcp6O
141++0Vt0670kweHh4fQdR2Xl5eZ3hEws7q6+ktm/3rKkvYs817ph4iI8kOZVbKi3B0REdEkKNXDJCIi
142+mhQlF/0QERGljYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQk
143+4bPFxcWst4GIiCj32MMkIiKSMHPnzh3WkiUiIhqBPUwiIiIJDEwiIiIJDEwiIiIJ/weH8+Ed/xCz
144+fAAAAABJRU5ErkJggg==
145+"
146+ id="image2845"
147+ x="-15.489594"
148+ y="-8.2945251" /></g><g
149+ inkscape:groupmode="layer"
150+ id="layer5"
151+ inkscape:label="dot" /><g
152+ inkscape:label="over"
153+ inkscape:groupmode="layer"
154+ id="layer1"
155+ style="display:none"><ellipse
156+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
157+ id="path2637"
158+ cx="24"
159+ cy="33"
160+ rx="4.3600898"
161+ ry="4.3600893" /><path
162+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
163+ d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
164+ id="path2142"
165+ inkscape:path-effect="#path-effect2144"
166+ inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
167+ sodipodi:nodetypes="csc" /><path
168+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
169+ d="M 26,22 H 39 V 9"
170+ id="path2626"
171+ sodipodi:nodetypes="ccc" /></g><g
172+ inkscape:groupmode="layer"
173+ id="layer6"
174+ inkscape:label="into"
175+ style="display:none"><ellipse
176+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
177+ id="path2637-3"
178+ cx="24"
179+ cy="38.5"
180+ rx="4.3600898"
181+ ry="4.3600893" /><path
182+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
183+ d="M 24,2 V 24"
184+ id="path2668"
185+ sodipodi:nodetypes="cc" /><path
186+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
187+ d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
188+ id="path2626-8"
189+ sodipodi:nodetypes="ccc" /></g><g
190+ inkscape:groupmode="layer"
191+ id="g2714"
192+ inkscape:label="out"
193+ style="display:none"><ellipse
194+ style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
195+ id="ellipse2708"
196+ cx="24"
197+ cy="38.5"
198+ rx="4.3600898"
199+ ry="4.3600893" /><path
200+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
201+ d="M 24,29.722858 V 7.7228579"
202+ id="path2710"
203+ sodipodi:nodetypes="cc" /><path
204+ style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
205+ d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
206+ id="path2712"
207+ sodipodi:nodetypes="ccc" /></g><g
208+ inkscape:groupmode="layer"
209+ id="layer8"
210+ inkscape:label="resume"><path
211+ style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
212+ d="M 13,12 V 38"
213+ id="path2850"
214+ sodipodi:nodetypes="cc" /><path
215+ style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
216+ d="M 21,16 V 34 L 36.5,25 Z"
217+ id="path2852"
218+ sodipodi:nodetypes="cccc" /></g></svg>
public/index.html+139 -52
@@ -383,7 +383,7 @@
383383 Max Response Length (tokens)
384384 </div>
385385 <div class="wide100p">
386386 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="501" max="800016384">
387387 </div>
388388 </div>
389389 <div class="range-block" data-source="openai,custom">
@@ -1273,7 +1273,7 @@
12731273 <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />
12741274 <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="max_tokens_second_textgenerationwebui" id="max_tokens_second_counter_textgenerationwebui">
12751275 </div>
12761276 <div data-newbie-hidden data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" id="smoothingBlock" name="smoothingBlock" class="wide100p">
12771277 <h4 class="wide100p textAlignCenter">
12781278 <label data-i18n="Smooth Sampling">Smooth Sampling</label>
12791279 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Smooth_Sampling_desc" title="Allows you to use quadratic/cubic transformations to adjust the distribution. Lower Smoothing Factor values will be more creative, usually between 0.2-0.3 is the sweetspot (assuming the curve = 1). Higher Smoothing Curve values will make the curve steeper, which will punish low probability choices more aggressively. 1.0 curve is equivalent to only using Smoothing Factor."></div>
@@ -1358,7 +1358,7 @@
13581358 </div>
13591359 </div>
13601360 </div>
13611361 <div data-newbie-hidden data-tg-type="ooba,aphrodite,infermaticai,koboldcpp,llamacpp,mancer,ollama,tabby" id="mirostat_block_ooba" class="wide100p">
13621362 <h4 class="wide100p textAlignCenter">
13631363 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>
13641364 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat_desc" title="Mirostat is a thermostat for output perplexity.&#13;Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap&#13;(where, as the autoregressive inference produces text, the perplexity of the output tends toward zero)&#13;and the confusion trap (where the perplexity diverges).&#13;For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).&#13;Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>
@@ -1387,7 +1387,7 @@
13871387 </div>
13881388 </div>
13891389 </div>
13901390 <div data-newbie-hidden data-tg-type="ooba, vllm" id="beamSearchBlock" name="beamSearchBlock" class="wide100p">
13911391 <h4 class="wide100p textAlignCenter">
13921392 <label>
13931393 <span data-i18n="Beam search">Beam Search</span>
@@ -1413,7 +1413,7 @@
14131413 </div>
14141414 </div>
14151415 </div>
14161416 <div data-tg-type="ooba" data-newbie-hidden id="contrastiveSearchBlock" name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
14171417 <h4 class="textAlignCenter" data-i18n="Contrastive search">Contrastive Search
14181418 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="Contrastive_search_txt" title="A sampler that encourages diversity while maintaining coherence, by exploiting the isotropicity of the representation space of most LLMs. For details, see the paper A Contrastive Framework for Neural Text Generation by Su et al. (2022)."></div>
14191419 </h4>
@@ -1544,7 +1544,7 @@
15441544 <h4 class="wide100p textAlignCenter">
15451545 <label>
15461546 <span data-i18n="Grammar String">Grammar String</span>
15471547 <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which." title="GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which."></div>
15481548 <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">
15491549 <small>
15501550 <div class="fa-solid fa-up-right-from-square note-link-span"></div>
@@ -1554,13 +1554,13 @@
15541554 </h4>
15551555 <textarea id="grammar_string_textgenerationwebui" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea>
15561556 </div>
15571557 <div id="sampler_order_blocksampler_order_block_kcpp" data-newbie-hidden data-tg-type="koboldcpp" class="range-block flexFlowColumn wide100p">
15581558 <hr class="wide100p">
15591559 <div class="range-block-title">
15601560 <span data-i18n="Samplers Order">Samplers Order</span>
15611561 </div>
15621562 <div class="toggle-description widthUnset" data-i18n="Samplers will be applied in a top-down order. Use with caution.">
15631563 kcpp only. Samplers will be applied in a top-down order.
15641564 Use with caution.
15651565 </div>
15661566 <div id="koboldcpp_order" class="prompt_order">
@@ -1597,10 +1597,10 @@
15971597 <span data-i18n="Load default order">Load default order</span>
15981598 </div>
15991599 </div>
16001600 <div id="sampler_order_block_lcpp" data-newbie-hidden data-tg-type="llamacpp" class="range-block flexFlowColumn wide100p">
16011601 <hr class="wide100p">
16021602 <h4 class="range-block-title justifyCenter">
16031603 <span data-i18n="SamplersSampler Order">SamplersSampler Order</span>
16041604 <div class="margin5 fa-solid fa-circle-info opacity50p" data-i18n="[title]llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored." title="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored."></div>
16051605 </h4>
16061606 <div class="toggle-description widthUnset" data-i18n="llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.">
@@ -1618,7 +1618,7 @@
16181618 <span data-i18n="Load default order">Load default order</span>
16191619 </div>
16201620 </div>
16211621 <div id="sampler_priority_block_ooba" data-newbie-hidden data-tg-type="ooba" class="range-block flexFlowColumn wide100p">
16221622 <hr class="wide100p">
16231623 <h4 class="range-block-title justifyCenter">
16241624 <span data-i18n="Sampler Priority">Sampler Priority</span>
@@ -1770,13 +1770,13 @@
17701770 </div>
17711771 </label>
17721772 </div>
17731773 <div class="range-block" data-source="openai,openrouter,makersuite,claude,custom,01ai">
17741774 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
17751775 <input id="openai_image_inlining" type="checkbox" />
17761776 <span data-i18n="Send inline images">Send inline images</span>
17771777 <div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
17781778 <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).
17791779 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>
17801780 <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>
17811781 </div>
17821782 </label>
@@ -1823,10 +1823,16 @@
18231823 </div>
18241824 <div data-newbie-hidden class="range-block" data-source="claude">
18251825 <div class="wide100p">
1826- <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
1826+ <div class="flex-container alignItemsCenter">
1827- <textarea id="claude_assistant_prefill" class="text_pole textarea_compact autoSetHeight" name="assistant_prefill" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1827+ <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
1828- <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
1828+ <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_prefill" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
1829- <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact autoSetHeight" name="assistant_impersonation" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1829+ </div>
1830+ <textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1831+ <div class="flex-container alignItemsCenter">
1832+ <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
1833+ <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_impersonation" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
1834+ </div>
1835+ <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
18301836 </div>
18311837 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
18321838 <input id="claude_use_sysprompt" type="checkbox" />
@@ -2427,10 +2433,11 @@
24272433 <optgroup>
24282434 <option value="01ai">01.AI (Yi)</option>
24292435 <option value="ai21">AI21</option>
2436+ <option value="blockentropy">Block Entropy</option>
24302437 <option value="claude">Claude</option>
24312438 <option value="cohere">Cohere</option>
24322439 <option value="groq">Groq</option>
24332440 <option value="makersuite">Google MakerSuiteAI Studio</option>
24342441 <option value="mistralai">MistralAI</option>
24352442 <option value="openrouter">OpenRouter</option>
24362443 <option value="perplexity">Perplexity</option>
@@ -2570,7 +2577,9 @@
25702577 </optgroup>
25712578 <optgroup label="GPT-4o">
25722579 <option value="gpt-4o">gpt-4o</option>
2580+ <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
25732581 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2582+ <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
25742583 </optgroup>
25752584 <optgroup label="gpt-4o-mini">
25762585 <option value="gpt-4o-mini">gpt-4o-mini</option>
@@ -2791,7 +2800,7 @@
27912800 </div>
27922801 </form>
27932802 <form id="makersuite_form" data-source="makersuite" action="javascript:void(null);" method="post" enctype="multipart/form-data">
27942803 <h4 data-i18n="MakerSuiteGoogle AI Studio API Key">MakerSuiteGoogle AI Studio API Key</h4>
27952804 <div class="flex-container">
27962805 <input id="api_key_makersuite" name="api_key_makersuite" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
27972806 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_makersuite"></div>
@@ -2802,21 +2811,26 @@
28022811 <div>
28032812 <h4 data-i18n="Google Model">Google Model</h4>
28042813 <select id="model_google_select">
28052814 <optgroup label="LatestPrimary">
2806- <!-- Doesn't work without "latest". Maybe my key is scuffed? -->
2815+ <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
28072816 <option value="gemini-1.5-flash-latest">Gemini 1.5 Flash</option>
2808- <!-- Points to 1.0, no default 1.5 endpoint -->
2817+ <option value="gemini-1.0-pro">Gemini 1.0 Pro</option>
28092818 <option value="gemini-pro">Gemini Pro (1.0)</option>
28102819 <option value="gemini-pro-vision">Gemini Pro Vision (1.0)</option>
28112820 <option value="gemini-ultra">Gemini Ultra (1.0)</option>
2812- <option value="text-bison-001">Bison Text</option>
2813- <option value="chat-bison-001">Bison Chat</option>
2814- </optgroup>
2815- <optgroup label="Sub-versions">
2816- <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro</option>
2817- <option value="gemini-1.0-pro-latest">Gemini 1.0 Pro</option>
2818- <option value="gemini-1.0-pro-vision-latest">Gemini 1.0 Pro Vision</option>
28192821 <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>
28202834 </optgroup>
28212835 </select>
28222836 </div>
@@ -2894,7 +2908,20 @@
28942908 </div>
28952909 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
28962910 <select id="model_perplexity_select">
28972911 <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">
28982925 <option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
28992926 <option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
29002927 <option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
@@ -2903,8 +2930,6 @@
29032930 <option value="sonar-small-online">sonar-small-online</option>
29042931 <option value="sonar-medium-chat">sonar-medium-chat</option>
29052932 <option value="sonar-medium-online">sonar-medium-online</option>
2906- </optgroup>
2907- <optgroup label="Open-Source Models">
29082933 <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
29092934 <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
29102935 <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
@@ -2938,6 +2963,20 @@
29382963 </select>
29392964 </div>
29402965 </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>
29412980 <form id="custom_form" data-source="custom">
29422981 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
29432982 <div class="flex-container">
@@ -3069,7 +3108,11 @@
30693108 </div>
30703109 <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.">
30713110 <input id="context_use_stop_strings" type="checkbox" />
30723111 <small data-i18n="UseSeparators as Stop Strings">UseSeparators as Stop Strings</small>
3112+ </label>
3113+ <label class="checkbox_label" title="Add Character and User names to a list of stopping strings." data-i18n="[title]Add Character and User names to a list of stopping strings.">
3114+ <input id="context_names_as_stop_strings" type="checkbox" />
3115+ <small data-i18n="Names as Stop Strings">Names as Stop Strings</small>
30733116 </label>
30743117 <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">
30753118 <input id="context_allow_jailbreak" type="checkbox" />
@@ -3406,6 +3449,7 @@
34063449 <!-- Option #2 was a legacy GPT-2/3 tokenizer -->
34073450 <option value="3">Llama 1/2</option>
34083451 <option value="12">Llama 3</option>
3452+ <option value="13">Gemma / Gemini</option>
34093453 <option value="4">NerdStash (NovelAI Clio)</option>
34103454 <option value="5">NerdStash v2 (NovelAI Kayra)</option>
34113455 <option value="7">Mistral</option>
@@ -3578,6 +3622,7 @@
35783622 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Scan chronologically until reached min entries or token budget." data-i18n="[title]Scan chronologically until reached min entries or token budget.">
35793623 <small>
35803624 <span data-i18n="Min Activations">Min Activations</span>
3625+ <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title](disabled when max recursion steps are used)" title="(disabled when max recursion steps are used)"></div>
35813626 </small>
35823627 <input class="neo-range-slider" type="range" id="world_info_min_activations" name="world_info_min_activations" min="0" max="100" step="1">
35833628 <input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations" id="world_info_min_activations_counter">
@@ -3591,6 +3636,14 @@
35913636 <input class="neo-range-slider" type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">
35923637 <input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations_depth_max" id="world_info_min_activations_depth_max_counter">
35933638 </div>
3639+ <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" title="Cap the number of entry activation recursions" data-i18n="[title]Cap the number of entry activation recursions">
3640+ <small>
3641+ <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
3642+ <div class="fa-solid fa-triangle-exclamation opacity50p" data-i18n="[title]0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc\n(disabled when min activations are used)" title="0 = unlimited, 1 = scans once and doesn't recurse, 2 = scans once and recurses once, etc&#10;(disabled when min activations are used)"></div>
3643+ </small>
3644+ <input class="neo-range-slider" type="range" id="world_info_max_recursion_steps" name="world_info_max_recursion_steps" min="0" max="10" step="1">
3645+ <input class="neo-range-input" type="number" min="0" max="10" step="1" data-for="world_info_max_recursion_steps" id="world_info_max_recursion_steps_counter">
3646+ </div>
35943647
35953648 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink flexBasis48p">
35963649 <small data-i18n="Insertion Strategy">
@@ -4054,25 +4107,31 @@
40544107 <input id="world_import_dialog" type="checkbox" />
40554108 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>
40564109 </label>
4110+ <label data-newbie-hidden class="checkbox_label" for="enable_auto_select_input" title="Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields." data-i18n="[title]Enable auto-select of input text in some text fields when clicking/selecting them. Applies to popup input textboxes, and possible other custom input fields.">
4111+ <input id="enable_auto_select_input" type="checkbox" />
4112+ <small data-i18n="Auto-select Input Text">Auto-select Input Text</small>
4113+ </label>
40574114 <label class="checkbox_label" for="restore_user_input" title="Restore unsaved user input on page refresh." data-i18n="[title]Restore unsaved user input on page refresh">
40584115 <input id="restore_user_input" type="checkbox" />
40594116 <small data-i18n="Restore User Input">Restore User Input</small>
40604117 </label>
4061- <label data-newbie-hidden id="movingUIModeCheckBlock" for="movingUImode" class="checkbox_label" title="Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile." data-i18n="[title]Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile">
4118+ <div class="flex-container alignItemsCenter">
4062- <input id="movingUImode" type="checkbox" />
4119+ <label data-newbie-hidden id="movingUIModeCheckBlock" for="movingUImode" class="checkbox_label" title="Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile." data-i18n="[title]Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile">
4063- <small data-i18n="Movable UI Panels">MovingUI&nbsp;<i class="fa-solid fa-desktop"></i></small>
4120+ <input id="movingUImode" type="checkbox" />
4064- </label>
4121+ <small data-i18n="Movable UI Panels">MovingUI&nbsp;<i class="fa-solid fa-desktop"></i></small>
4122+ </label>
4123+ <div data-newbie-hidden id="movingUIreset" title="Reset MovingUI panel sizes/locations." class="menu_button margin0" data-i18n="[title]Reset MovingUI panel sizes/locations."><i class=" fa-solid fa-recycle margin-r5"></i> Reset</div>
4124+ </div>
40654125 <div data-newbie-hidden id="MovingUI-presets-block" class="flex-container alignitemscenter">
40664126 <div class="flex-container alignItemsFlexEnd">
40674127 <label for="movingUIPresets" title="MovingUI preset. Predefined/saved draggable positions." data-i18n="[title]MovingUI preset. Predefined/saved draggable positions">
40684128 <small data-i18n="MUI Preset">MUIMovingUI Preset:</small>
40694129 <div class="flex-container flexnowrap">
40704130 <select id="movingUIPresets" class="widthNatural flex1 margin0">
40714131 </select>
40724132 </div>
40734133 </label>
40744134 <div id="movingui-preset-save-button" title="Save changes to a new MovingUI preset file." data-i18n="[title]Save movingUI changes to a new file" class="menu_button margin0 fa-solid fa-save"></div>
4075- <div data-newbie-hidden id="movingUIreset" title="Reset MovingUI panel sizes/locations." class="menu_button fa-solid fa-recycle margin0" data-i18n="[title]Reset MovingUI panel sizes/locations."></div>
40764135 </div>
40774136 </div>
40784137 <div data-newbie-hidden id="CustomCSS-block" class="flex-container flexFlowColumn">
@@ -4144,6 +4203,12 @@
41444203 Quick "Continue" button
41454204 </small>
41464205 </label>
4206+ <label class="checkbox_label" for="quick_impersonate" title="Show a button in the input area to ask the AI to impersonate your character for a single message." data-i18n="[title]Show a button in the input area to ask the AI to impersonate your character for a single message">
4207+ <input id="quick_impersonate" type="checkbox" />
4208+ <small data-i18n="Quick 'Impersonate' button">
4209+ Quick "Impersonate" button
4210+ </small>
4211+ </label>
41474212 <div class="checkbox-container flex-container">
41484213 <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">
41494214 <input id="swipes-checkbox" type="checkbox" />
@@ -4264,6 +4329,16 @@
42644329 </div>
42654330 </div>
42664331 </div>
4332+ <div title="Determines which keys select an item from the AutoComplete suggestions">
4333+ <label data-i18n="Keyboard">
4334+ <small>Keyboard:</small>
4335+ </label>
4336+ <select id="stscript_autocomplete_select">
4337+ <option value="3" data-i18n="Select with Tab or Enter">Select with Tab or Enter</option>
4338+ <option value="1" data-i18n="Select with Tab">Select with Tab</option>
4339+ <option value="2" data-i18n="Select with Enter">Select with Enter</option>
4340+ </select>
4341+ </div>
42674342 <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">
42684343 <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>
42694344 <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">
@@ -4605,7 +4680,7 @@
46054680 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>
46064681 <input type="hidden" id="fav_checkbox" name="fav" />
46074682 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
46084683 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift-click to open 'Link to World Info' popup" data-i18n="[title]Character Loreworld_button_title"></div>
46094684 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
46104685 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
46114686 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
@@ -4923,7 +4998,7 @@
49234998 <div class="popup-crop-wrap">
49244999 <img class="popup-crop-image" src="">
49255000 </div>
49265001 <textarea class="popup-input text_pole result-control auto-select" rows="1" data-result="1" data-result-event="submit"></textarea>
49275002 <div class="popup-inputs"></div>
49285003 <div class="popup-controls">
49295004 <div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>
@@ -5262,21 +5337,22 @@
52625337 <div class="world_entry">
52635338 <form class="world_entry_form wi-card-entry">
52645339 <div class="inline-drawer wide100p">
52655340 <div class="inline-drawer-toggle inline-drawer-header gap5px padding0">
52665341 <span class="drag-handle">&#9776;</span>
52675342 <div class="gap5px world_entry_thin_controls wide100p alignitemscenter">
52685343 <div class="inline-drawer-toggle fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
5344+ <div class="fa-solid fa-toggle-on killSwitch" name="entryKillSwitch" title="Toggle entry's active state."></div>
52695345 <div class="flex-container alignitemscenter wide100p">
5346+
52705347 <div class="WIEntryTitleAndStatus flex-container flex1 alignitemscenter">
5348+
52715349 <div class="flex-container flex1">
52725350 <textarea class="text_pole" rows="1" name="comment" maxlength="5000" data-i18n="[placeholder]Entry Title/Memo" placeholder="Entry Title/Memo"></textarea>
52735351 </div>
5274- <!-- <span class="world_entry_form_position_value"></span> -->
52755352 <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">
52765353 <option value="constant" title="Constant" data-i18n="[title]WI_Entry_Status_Constant">🔵</option>
52775354 <option value="normal" title="Normal" data-i18n="[title]WI_Entry_Status_Normal">🟢</option>
52785355 <option value="vectorized" title="Vectorized" data-i18n="[title]WI_Entry_Status_Vectorized">🔗</option>
5279- <option value="disabled" title="Disabled" data-i18n="[title]WI_Entry_Status_Disabled">❌</option>
52805356 </select>
52815357 </div>
52825358 <div class="WIEnteryHeaderControls flex-container">
@@ -5525,13 +5601,16 @@
55255601 <div class="flex-container wide100p flexGap10">
55265602 <div class="flex4 flex-container flexFlowColumn flexNoGap">
55275603 <div class="flex-container justifySpaceBetween">
55285604 <small for="characterFilter" data-i18n="Filter to Character(s)Characters or Tags">
55295605 Filter to Character(s)Characters or Tags
55305606 </small>
55315607 <label class="checkbox_label flexNoGap margin-r5" for="character_exclusion">
55325608 <input type="checkbox" name="character_exclusion" />
55335609 <span>
5534- <small data-i18n="Character Exclusion">Character Exclusion</small>
5610+ <small title="Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry" data-i18n="[title]Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry">
5611+ <span data-i18n="Exclude">Exclude</span>
5612+ <div class="fa-solid fa-circle-info opacity50p"></div>
5613+ </small>
55355614 </span>
55365615 </label>
55375616 </div>
@@ -5749,6 +5828,11 @@
57495828 <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div>
57505829 <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div>
57515830 </div>
5831+ <div class="mes_img_swipes">
5832+ <div title="Swipe left" class="right_menu_button fa-lg fa-solid fa-chevron-left mes_img_swipe_left" data-i18n="[title]Swipe left"></div>
5833+ <div class="mes_img_swipe_counter">1/1</div>
5834+ <div title="Swipe right" class="right_menu_button fa-lg fa-solid fa-chevron-right mes_img_swipe_right" data-i18n="[title]Swipe right"></div>
5835+ </div>
57525836 <img class="mes_img" src="" />
57535837 </div>
57545838 <div class="mes_bias"></div>
@@ -5811,6 +5895,7 @@
58115895 Enable simple UI mode
58125896 </span>
58135897 </label>
5898+ <div class="expander"></div>
58145899 <div class="textAlignCenter">
58155900 <h3 data-i18n="Looking for AI characters?">
58165901 Looking for AI characters?
@@ -5829,6 +5914,7 @@
58295914 </span>
58305915 </span>
58315916 </div>
5917+ <div class="expander"></div>
58325918 <h3 data-i18n="Your Persona">
58335919 Your Persona
58345920 </h3>
@@ -6342,6 +6428,7 @@
63426428 <div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
63436429 <i class="fa-solid fa-circle-stop"></i>
63446430 </div>
6431+ <div id="mes_impersonate" class="fa-solid fa-user-secret interactable displayNone" title="Ask AI to write your message for you" data-i18n="[title]Ask AI to write your message for you" tabindex="0"></div>
63456432 <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>
63466433 <div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
63476434 </div>
public/locales/ar-sa.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "مخطط جيسون",
208208 "Type in the desired JSON schema": "اكتب مخطط JSON المطلوب",
209209 "Grammar String": "سلسلة القواعد",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "يعتمد GNBFGBNF أو ENBFEBNF على الواجهة الخلفية المستخدمة. إذا كنت تستخدم هذا يجب أن تعرف أي.",
211211 "Top P & Min P": "أعلى ع وأدنى ص",
212212 "Load default order": "تحميل الترتيب الافتراضي",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp فقط. تحديد ترتيب أخذ العينات. إذا لم يكن وضع Mirostat 0، فسيتم تجاهل ترتيب أخذ العينات.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "طريقة بديلة",
391391 "AI21 API Key": "مفتاح API لـ AI21",
392392 "AI21 Model": "نموذج AI21",
393393 "MakerSuiteGoogle AI Studio API Key": "مفتاح واجهة برمجة تطبيقات MakerSuiteGoogle AI Studio",
394394 "Google Model": "نموذج جوجل",
395395 "MistralAI API Key": "مفتاح واجهة برمجة التطبيقات MistralAI",
396396 "MistralAI Model": "نموذج ميسترال آي آي",
public/locales/de-de.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSON-Schema",
208208 "Type in the desired JSON schema": "Geben Sie das gewünschte JSON-Schema ein",
209209 "Grammar String": "Grammatikzeichenfolge",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF oder ENBFEBNF, hängt vom verwendeten Backend ab. Wenn Sie dieses verwenden, sollten Sie wissen, welches.",
211211 "Top P & Min P": "Top P und Min P",
212212 "Load default order": "Standardreihenfolge laden",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Nur llama.cpp. Bestimmt die Reihenfolge der Sampler. Wenn der Mirostat-Modus nicht 0 ist, wird die Sampler-Reihenfolge ignoriert.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "Alternative Methode",
391391 "AI21 API Key": "AI21 API-Schlüssel",
392392 "AI21 Model": "AI21-Modell",
393393 "MakerSuiteGoogle AI Studio API Key": "MakerSuiteGoogle AI Studio API-Schlüssel",
394394 "Google Model": "Google-Modell",
395395 "MistralAI API Key": "MistralAI API-Schlüssel",
396396 "MistralAI Model": "MistralAI-Modell",
public/locales/es-es.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Esquema JSON",
208208 "Type in the desired JSON schema": "Escriba el esquema JSON deseado",
209209 "Grammar String": "Cadena de gramática",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF o ENBFEBNF, depende del backend en uso. Si estás usando esto, debes saber cuál.",
211211 "Top P & Min P": "P superior y P mínima",
212212 "Load default order": "Cargar orden predeterminado",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp únicamente. Determina el orden de los muestreadores. Si el modo Mirostat no es 0, se ignora el orden de las muestras.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "Método alternativo",
391391 "AI21 API Key": "Clave API de AI21",
392392 "AI21 Model": "Modelo de AI21",
393393 "MakerSuiteGoogle AI Studio API Key": "Clave API de MakerSuiteGoogle AI Studio",
394394 "Google Model": "Modelo de Google",
395395 "MistralAI API Key": "Clave API de MistralAI",
396396 "MistralAI Model": "Modelo MistralAI",
public/locales/fr-fr.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Schéma JSON",
208208 "Type in the desired JSON schema": "Tapez le schéma JSON souhaité",
209209 "Grammar String": "Chaîne de grammaire",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF ou ENBFEBNF dépend du backend utilisé. Si vous l'utilisez, vous devez savoir lequel.",
211211 "Top P & Min P": "P supérieur et P minimal",
212212 "Load default order": "Charger l'ordre par défaut",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "lama.cpp uniquement. Détermine l’ordre des échantillonneurs. Si le mode Mirostat n'est pas 0, l'ordre de l'échantillonneur est ignoré.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "Méthode alternative",
391391 "AI21 API Key": "Clé API AI21",
392392 "AI21 Model": "Modèle AI21",
393393 "MakerSuiteGoogle AI Studio API Key": "Clé API MakerSuiteGoogle AI Studio",
394394 "Google Model": "Modèle Google",
395395 "MistralAI API Key": "Clé API MistralAI",
396396 "MistralAI Model": "Modèle MistralAI",
public/locales/is-is.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSON kerfi",
208208 "Type in the desired JSON schema": "Sláðu inn æskilegt JSON skema",
209209 "Grammar String": "Málfræðistrengur",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF eða ENBFEBNF, fer eftir bakendanum sem er í notkun. Ef þú ert að nota þetta ættir þú að vita hvaða.",
211211 "Top P & Min P": "Efstu P & Min P",
212212 "Load default order": "Hlaða sjálfgefna röð",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp eingöngu. Ákveður röð sýnataka. Ef Mirostat hamur er ekki 0, er röð sýnatöku hunsuð.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "Aðferð Bakmenn",
391391 "AI21 API Key": "Lykill API fyrir AI21",
392392 "AI21 Model": "AI21 Módel",
393393 "MakerSuiteGoogle AI Studio API Key": "MakerSuiteGoogle AI Studio API lykill",
394394 "Google Model": "Google líkan",
395395 "MistralAI API Key": "MistralAI API lykill",
396396 "MistralAI Model": "MistralAI líkan",
public/locales/it-it.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Schema JSON",
208208 "Type in the desired JSON schema": "Digita lo schema JSON desiderato",
209209 "Grammar String": "Stringa grammaticale",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF o ENBFEBNF, dipende dal backend in uso. Se stai usando questo dovresti sapere quale.",
211211 "Top P & Min P": "P massimo e P minimo",
212212 "Load default order": "Carica ordine predefinito",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Solo lama.cpp. Determina l'ordine dei campionatori. Se la modalità Mirostat non è 0, l'ordine del campionatore viene ignorato.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "Metodo alternativo",
391391 "AI21 API Key": "Chiave API di AI21",
392392 "AI21 Model": "Modello AI21",
393393 "MakerSuiteGoogle AI Studio API Key": "Chiave API MakerSuiteGoogle AI Studio",
394394 "Google Model": "Modello Google",
395395 "MistralAI API Key": "Chiave API MistralAI",
396396 "MistralAI Model": "Modello MistralAI",
public/locales/ja-jp.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSONスキーマ",
208208 "Type in the desired JSON schema": "希望するJSONスキーマを入力します",
209209 "Grammar String": "文法文字列",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF または ENBFEBNF は、使用するバックエンドによって異なります。これを使用する場合は、どちらであるかを知っておく必要があります。",
211211 "Top P & Min P": "トップPと最小P",
212212 "Load default order": "デフォルトの順序を読み込む",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp のみ。サンプラーの順序を決定します。Mirostat モードが 0 でない場合、サンプラーの順序は無視されます。",
@@ -390,7 +390,7 @@
390390 "Alt Method": "代替手法",
391391 "AI21 API Key": "AI21のAPIキー",
392392 "AI21 Model": "AI21モデル",
393393 "MakerSuiteGoogle AI Studio API Key": "MakerSuiteGoogle AI Studio APIキー",
394394 "Google Model": "Google モデル",
395395 "MistralAI API Key": "MistralAI API キー",
396396 "MistralAI Model": "MistralAI モデル",
public/locales/ko-kr.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSON 스키마",
208208 "Type in the desired JSON schema": "원하는 JSON 스키마를 입력하세요.",
209209 "Grammar String": "문법 문자열",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF 또는 ENBF는EBNF는 사용 중인 백엔드에 따라 다릅니다. 이것을 사용한다면 어느 것이 무엇인지 알아야합니다.",
211211 "Top P & Min P": "상위 P 및 최소 P",
212212 "Load default order": "기본 순서로 로드",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp만 가능합니다. 샘플러의 순서를 결정합니다. Mirostat 모드가 0이 아닌 경우 샘플러 순서는 무시됩니다.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "대체 방법",
391391 "AI21 API Key": "AI21 API 키",
392392 "AI21 Model": "AI21 모델",
393393 "MakerSuiteGoogle AI Studio API Key": "MakerSuiteGoogle AI Studio API 키",
394394 "Google Model": "구글 모델",
395395 "MistralAI API Key": "MistralAI API 키",
396396 "MistralAI Model": "MistralAI 모델",
public/locales/nl-nl.json+1 -1
@@ -207,7 +207,7 @@
207207 "JSON Schema": "JSON-schema",
208208 "Type in the desired JSON schema": "Typ het gewenste JSON-schema",
209209 "Grammar String": "Grammaticareeks",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF of ENBFEBNF, hangt af van de gebruikte backend. Als u dit gebruikt, moet u weten welke.",
211211 "Top P & Min P": "Top P & Min P",
212212 "Load default order": "Standaardvolgorde laden",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "alleen lama.cpp. Bepaalt de volgorde van de samplers. Als de Mirostat-modus niet 0 is, wordt de samplervolgorde genegeerd.",
public/locales/pt-pt.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Esquema JSON",
208208 "Type in the desired JSON schema": "Digite o esquema JSON desejado",
209209 "Grammar String": "Cadeia de Gramática",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF ou ENBFEBNF, depende do backend em uso. Se você estiver usando isso, você deve saber qual.",
211211 "Top P & Min P": "P superior e P mínimo",
212212 "Load default order": "Carregar ordem padrão",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "apenas lhama.cpp. Determina a ordem dos amostradores. Se o modo Mirostat não for 0, a ordem do amostrador será ignorada.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "Método Alternativo",
391391 "AI21 API Key": "Chave da API AI21",
392392 "AI21 Model": "Modelo AI21",
393393 "MakerSuiteGoogle AI Studio API Key": "Chave API MakerSuiteGoogle AI Studio",
394394 "Google Model": "Modelo Google",
395395 "MistralAI API Key": "Chave de API MistralAI",
396396 "MistralAI Model": "Modelo MistralAI",
public/locales/ru-ru.json+2 -2
@@ -722,7 +722,7 @@
722722 "Proxy Server URL": "Адрес прокси-сервера",
723723 "MistralAI Model": "Модель MistralAI",
724724 "MistralAI API Key": "Ключ от API MistralAI",
725725 "MakerSuiteGoogle AI Studio API Key": "Ключ от API MakerSuiteGoogle AI Studio",
726726 "Google Model": "Модель Google",
727727 "Cohere API Key": "Ключ от API Cohere",
728728 "Cohere Model": "Модель Cohere",
@@ -978,7 +978,7 @@
978978 "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в",
979979 "char_import_7": ")",
980980 "Grammar String": "Грамматика",
981981 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF или ENBFEBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
982982 "Account": "Аккаунт",
983983 "Hi,": "Привет,",
984984 "To enable multi-account features, restart the SillyTavern server with": "Чтобы активировать систему аккаунтов, перезапустите SillyTavern, выставив",
public/locales/uk-ua.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Схема JSON",
208208 "Type in the desired JSON schema": "Введіть потрібну схему JSON",
209209 "Grammar String": "Граматичний рядок",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF або ENBFEBNF, залежить від серверної частини, яка використовується. Якщо ви використовуєте це, ви повинні знати, який.",
211211 "Top P & Min P": "Верхній P & Min P",
212212 "Load default order": "Завантажити типовий порядок",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "лише llama.cpp. Визначає порядок пробовідбірників. Якщо режим Mirostat не 0, порядок вибірки ігнорується.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "Альтернативний метод",
391391 "AI21 API Key": "Ключ API для AI21",
392392 "AI21 Model": "Модель AI21",
393393 "MakerSuiteGoogle AI Studio API Key": "Ключ API MakerSuiteGoogle AI Studio",
394394 "Google Model": "Модель Google",
395395 "MistralAI API Key": "Ключ API MistralAI",
396396 "MistralAI Model": "Модель MistralAI",
public/locales/vi-vn.json+2 -2
@@ -207,7 +207,7 @@
207207 "JSON Schema": "Lược đồ JSON",
208208 "Type in the desired JSON schema": "Nhập lược đồ JSON mong muốn",
209209 "Grammar String": "Chuỗi ngữ pháp",
210210 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNF hoặc ENBFEBNF, tùy thuộc vào backend đang sử dụng. Nếu bạn đang sử dụng cái này, bạn nên biết cái nào.",
211211 "Top P & Min P": "P & P tối thiểu hàng đầu",
212212 "Load default order": "Tải thứ tự mặc định",
213213 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "chỉ llama.cpp. Xác định thứ tự lấy mẫu. Nếu chế độ Mirostat khác 0, thứ tự lấy mẫu sẽ bị bỏ qua.",
@@ -390,7 +390,7 @@
390390 "Alt Method": "Phương pháp thay thế",
391391 "AI21 API Key": "Khóa API của AI21",
392392 "AI21 Model": "Mô hình AI21",
393393 "MakerSuiteGoogle AI Studio API Key": "Khóa API MakerSuiteGoogle AI Studio",
394394 "Google Model": "Mô hình Google",
395395 "MistralAI API Key": "Khóa API MistralAI",
396396 "MistralAI Model": "Mô hình MistralAI",
public/locales/zh-cn.json+22 -5
@@ -208,9 +208,10 @@
208208 "JSON Schema": "JSON 结构",
209209 "Type in the desired JSON schema": "输入所需的 JSON 结构",
210210 "Grammar String": "语法字符串",
211211 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNFENBFEBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
212212 "Top P & Min P": "Top P 和 Min P",
213213 "Load default order": "加载默认顺序",
214+ "Sampler Order": "取样器顺序",
214215 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。",
215216 "Sampler Priority": "采样器优先级",
216217 "Ooba only. Determines the order of samplers.": "确定采样器的顺序(仅适用于Ooba)",
@@ -405,7 +406,7 @@
405406 "Alt Method": "备用方法",
406407 "AI21 API Key": "AI21 API 密钥",
407408 "AI21 Model": "AI21 模型",
408409 "MakerSuiteGoogle AI Studio API Key": "MakerSuiteGoogle AI Studio API 密钥",
409410 "Google Model": "Google 模型",
410411 "MistralAI API Key": "MistralAI API 密钥",
411412 "MistralAI Model": "MistralAI 模型",
@@ -443,7 +444,9 @@
443444 "Example Separator": "示例分隔符",
444445 "Chat Start": "聊天开始",
445446 "Add Chat Start and Example Separator to a list of stopping strings.": "将聊天开始和示例分隔符添加到停止字符串列表中。",
446447 "UseSeparators as Stop Strings": "用作停止字符串分隔符作为终止字符串",
448+ "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。",
449+ "Names as Stop Strings": "名称作为终止字符串",
447450 "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。",
448451 "Allow Post-History Instructions": "允许后历史说明",
449452 "Context Order": "上下文顺序",
@@ -704,10 +707,10 @@
704707 "Restore User Input": "恢复用户输入",
705708 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",
706709 "Movable UI Panels": "可移动 UI 面板",
710+ "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
707711 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",
708712 "MUI Preset": "可移动 UI 预设",
709713 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",
710- "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
711714 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",
712715 "Custom CSS": "自定义 CSS",
713716 "Expand the editor": "展开编辑器",
@@ -727,6 +730,8 @@
727730 "Press Send to continue": "按发送键以继续",
728731 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",
729732 "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": "快速“模仿”按钮",
730735 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",
731736 "Swipes": "刷新回复按钮",
732737 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",
@@ -772,6 +777,10 @@
772777 "Autocomplete Style": "风格",
773778 "Follow Theme": "关注主题",
774779 "Dark": "黑暗的",
780+ "Keyboard": "键盘:",
781+ "Select with Tab or Enter": "使用 Tab 或 Enter 选择",
782+ "Select with Tab": "使用 Tab 选择",
783+ "Select with Enter": "按 Enter 键选择",
775784 "Sets the font size of the autocomplete.": "设置自动完成的字体大小。",
776785 "Sets the width of the autocomplete.": "设置自动完成的宽度。",
777786 "Autocomplete Width": "宽度",
@@ -1176,6 +1185,7 @@
11761185 "Pause script execution": "暂停执行脚本",
11771186 "Abort script execution": "中止执行脚本",
11781187 "Abort request": "中止请求",
1188+ "Ask AI to write your message for you": "让AI为您撰写消息",
11791189 "Continue the last message": "继续上一条消息",
11801190 "Send a message": "发送消息",
11811191 "Close chat": "关闭聊天",
@@ -1187,7 +1197,6 @@
11871197 "Manage chat files": "管理聊天文件",
11881198 "Delete messages": "删除消息",
11891199 "Regenerate": "重新生成",
1190- "Ask AI to write your message for you": "请求AI为您撰写消息",
11911200 "Impersonate": "AI 帮答",
11921201 "Continue": "继续",
11931202 "Bind user name to that avatar": "将用户名称绑定到该头像",
@@ -1341,6 +1350,7 @@
13411350 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",
13421351 "Labels and Message": "标签和信息",
13431352 "Label": "标签",
1353+ "(label of the button, if no icon is chosen) ": "(如果没有选择图标,则为按钮的标签)",
13441354 "Title": "标题",
13451355 "(tooltip, leave empty to show message or /command)": "(工具提示,留空以显示消息或/命令)",
13461356 "Message / Command:": "消息/命令:",
@@ -1371,6 +1381,8 @@
13711381 "Inject user input automatically": "自动注入用户输入",
13721382 "(if disabled, use ": "(如果禁用,使用",
13731383 "macro for manual injection)": "宏用于手动注入)",
1384+ "Color": "颜色",
1385+ "Only apply color as accent": "仅应用颜色作为强调",
13741386 "ext_regex_title": "正则",
13751387 "ext_regex_new_global_script_desc": "新的全局正则表达式脚本",
13761388 "ext_regex_new_global_script": "新建全局正则",
@@ -1419,6 +1431,7 @@
14191431 "ext_regex_export_script": "导出脚本",
14201432 "ext_regex_delete_script": "删除脚本",
14211433 "Trigger Stable Diffusion": "触发Stable Diffusion",
1434+ "Abort current image generation task": "中止当前图像生成",
14221435 "sd_Yourself": "你自己",
14231436 "sd_Your_Face": "你的脸",
14241437 "sd_Me": "我",
@@ -1572,6 +1585,10 @@
15721585 "Only used when Main API is selected.": "仅在选择主 API 时使用。",
15731586 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",
15741587 "View Stats": "查看统计数据",
1588+ "Title/Memo": "标题/备忘录",
1589+ "Status": "状态",
1590+ "Position": "位置",
1591+ "Trigger %": "触发率 %",
15751592 "Manager Users": "管理用户",
15761593 "New User": "新用户",
15771594 "Status:": "地位:",
public/locales/zh-tw.json+2 -2
@@ -208,7 +208,7 @@
208208 "JSON Schema": "JSON 結構",
209209 "Type in the desired JSON schema": "輸入所需的 JSON 結構",
210210 "Grammar String": "語法字串",
211211 "GNBFGBNF or ENBFEBNF, depends on the backend in use. If you're using this you should know which.": "GNBFGBNFENBFEBNF,取決於所使用的後端。如果您使用此功能,應該知道是哪一種",
212212 "Top P & Min P": "Top P 和 Min P",
213213 "Load default order": "載入預設順序",
214214 "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。",
@@ -391,7 +391,7 @@
391391 "Alt Method": "替代方法",
392392 "AI21 API Key": "AI21 API 金鑰",
393393 "AI21 Model": "AI21 模型",
394394 "MakerSuiteGoogle AI Studio API Key": "MakerSuiteGoogle AI Studio API 金鑰",
395395 "Google Model": "Google 模型",
396396 "MistralAI API Key": "MistralAI API 金鑰",
397397 "MistralAI Model": "MistralAI 模型",
public/script.js+202 -103
@@ -156,6 +156,7 @@ import {
156156 ensureImageFormatSupported,
157157 flashHighlight,
158158 isTrueBoolean,
159+ toggleDrawer,
159160} from './scripts/utils.js';
160161import { debounce_timeout } from './scripts/constants.js';
161162
@@ -224,10 +225,10 @@ import {
224225import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
225226import { hideLoader, showLoader } from './scripts/loader.js';
226227import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
227228import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels } from './scripts/textgen-models.js';
228229import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
229230import { initPresetManager } from './scripts/preset-manager.js';
230231import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros.js';
231232import { currentUser, setUserControls } from './scripts/user.js';
232233import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
233234import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
@@ -241,7 +242,7 @@ import { DragAndDropHandler } from './scripts/dragdrop.js';
241242import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
242243import { initDynamicStyles } from './scripts/dynamic-styles.js';
243244import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
244245import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
245246
246247//exporting functions and vars for mods
247248export {
@@ -424,6 +425,8 @@ export const event_types = {
424425 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
425426 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
426427 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',
428+ OAI_PRESET_EXPORT_READY: 'oai_preset_export_ready',
429+ OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready',
427430 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
428431 WORLDINFO_UPDATED: 'worldinfo_updated',
429432 CHARACTER_EDITED: 'character_edited',
@@ -439,6 +442,7 @@ export const event_types = {
439442 GROUP_CHAT_CREATED: 'group_chat_created',
440443 GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
441444 GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
445+ GENERATE_AFTER_DATA: 'generate_after_data',
442446 GROUP_MEMBER_DRAFTED: 'group_member_drafted',
443447 WORLD_INFO_ACTIVATED: 'world_info_activated',
444448 TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
@@ -455,6 +459,7 @@ export const event_types = {
455459 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
456460 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
457461 ONLINE_STATUS_CHANGED: 'online_status_changed',
462+ IMAGE_SWIPED: 'image_swiped',
458463};
459464
460465export const eventSource = new EventEmitter();
@@ -910,6 +915,7 @@ async function firstLoadInit() {
910915 await readSecretState();
911916 initLocales();
912917 initDefaultSlashCommands();
918+ initTextGenModels();
913919 await getSystemMessages();
914920 sendSystemMessage(system_message_types.WELCOME);
915921 await getSettings();
@@ -1720,16 +1726,24 @@ export async function replaceCurrentChat() {
17201726}
17211727
17221728export function showMoreMessages() {
17231729 letconst messageIdfirstDisplayedMesId = Number($('#chat').children('.mes').first().attr('mesid'));
1730+ let messageId = Number(firstDisplayedMesId);
17241731 let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
17251732
1733+ // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id,
1734+ // so the first "new" message being shown will be the last available message
1735+ if (isNaN(messageId)) {
1736+ messageId = getLastMessageId() + 1;
1737+ }
1738+
17261739 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
17271740 const prevHeight = $('#chat').prop('scrollHeight');
17281741
17291742 while (messageId > 0 && count > 0) {
1743+ let newMessageId = messageId - 1;
1744+ addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId });
17301745 count--;
17311746 messageId--;
1732- addOneMessage(chat[messageId], { insertBefore: messageId + 1, scroll: false, forceId: messageId });
17331747 }
17341748
17351749 if (messageId == 0) {
@@ -1865,7 +1879,12 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
18651879 }
18661880
18671881 if (Number(messageId) === 0 && !isSystem && !isUser) {
1882+ const mesBeforeReplace = mes;
1883+ const chatMessage = chat[messageId];
18681884 mes = substituteParams(mes, undefined, ch_name);
1885+ if (chatMessage && chatMessage.mes === mesBeforeReplace && chatMessage.extra?.display_text !== mesBeforeReplace) {
1886+ chatMessage.mes = mes;
1887+ }
18691888 }
18701889
18711890 mesForShowdownParse = mes;
@@ -2099,6 +2118,7 @@ export function updateMessageBlock(messageId, message) {
20992118export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
21002119 // Add image to message
21012120 if (mes.extra?.image) {
2121+ const container = messageElement.find('.mes_img_container');
21022122 const chatHeight = $('#chat').prop('scrollHeight');
21032123 const image = messageElement.find('.mes_img');
21042124 const text = messageElement.find('.mes_text');
@@ -2114,9 +2134,27 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
21142134 });
21152135 image.attr('src', mes.extra?.image);
21162136 image.attr('title', mes.extra?.title || mes.title || '');
21172137 messageElement.find('.mes_img_container')container.addClass('img_extra');
21182138 image.toggleClass('img_inline', isInline);
21192139 text.toggleClass('displayNone', !isInline);
2140+
2141+ const imageSwipes = mes.extra.image_swipes;
2142+ if (Array.isArray(imageSwipes) && imageSwipes.length > 0) {
2143+ container.addClass('img_swipes');
2144+ const counter = container.find('.mes_img_swipe_counter');
2145+ const currentImage = imageSwipes.indexOf(mes.extra.image) + 1;
2146+ counter.text(`${currentImage}/${imageSwipes.length}`);
2147+
2148+ const swipeLeft = container.find('.mes_img_swipe_left');
2149+ swipeLeft.off('click').on('click', function () {
2150+ eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'left' });
2151+ });
2152+
2153+ const swipeRight = container.find('.mes_img_swipe_right');
2154+ swipeRight.off('click').on('click', function () {
2155+ eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' });
2156+ });
2157+ }
21202158 }
21212159
21222160 // Add file to message
@@ -2470,26 +2508,30 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
24702508 * @returns {string[]} Array of stopping strings
24712509 */
24722510export function getStoppingStrings(isImpersonate, isContinue) {
24732511 const charStringresult = `\n${name2}:`[];
2474- const userString = `\n${name1}:`;
2475- const result = isImpersonate ? [charString] : [userString];
24762512
2477- result.push(userString);
2513+ if (power_user.context.names_as_stop_strings) {
2514+ const charString = `\n${name2}:`;
2515+ const userString = `\n${name1}:`;
2516+ result.push(isImpersonate ? charString : userString);
24782517
2479- if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) {
2518+ result.push(userString);
2480- result.push(charString);
2481- }
24822519
2483- // Add other group members as the stopping strings
2520+ if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) {
2484- if (selected_group) {
2521+ result.push(charString);
2485- const group = groups.find(x => x.id === selected_group);
2522+ }
24862523
2487- if (group && Array.isArray(group.members)) {
2524+ // Add group members as stopping strings if generating for a specific group member or user. (Allow slash commands to work around name stopping string restrictions)
2488- const names = group.members
2525+ if (selected_group && (name2 || isImpersonate)) {
24892526 .map(x const group => charactersgroups.find(yx => yx.avatarid === x)selected_group);
2490- .filter(x => x && x.name && x.name !== name2)
2527+
2491- .map(x => `\n${x.name}:`);
2528+ if (group && Array.isArray(group.members)) {
2492- result.push(...names);
2529+ const names = group.members
2530+ .map(x => characters.find(y => y.avatar == x))
2531+ .filter(x => x && x.name && x.name !== name2)
2532+ .map(x => `\n${x.name}:`);
2533+ result.push(...names);
2534+ }
24932535 }
24942536 }
24952537
@@ -2802,7 +2844,14 @@ function hideStopButton() {
28022844}
28032845
28042846class StreamingProcessor {
2805- constructor(type, force_name2, timeStarted, messageAlreadyGenerated) {
2847+ /**
2848+ * Creates a new streaming processor.
2849+ * @param {string} type Generation type
2850+ * @param {boolean} forceName2 If true, force the use of name2
2851+ * @param {Date} timeStarted Date when generation was started
2852+ * @param {string} continueMessage Previous message if the type is 'continue'
2853+ */
2854+ constructor(type, forceName2, timeStarted, continueMessage) {
28062855 this.result = '';
28072856 this.messageId = -1;
28082857 this.messageDom = null;
@@ -2812,14 +2861,14 @@ class StreamingProcessor {
28122861 /** @type {HTMLTextAreaElement} */
28132862 this.sendTextarea = document.querySelector('#send_textarea');
28142863 this.type = type;
28152864 this.force_name2 = force_name2forceName2;
28162865 this.isStopped = false;
28172866 this.isFinished = false;
28182867 this.generator = this.nullStreamingGeneration;
28192868 this.abortController = new AbortController();
28202869 this.firstMessageText = '...';
28212870 this.timeStarted = timeStarted;
2822- this.messageAlreadyGenerated = messageAlreadyGenerated;
2871+ this.continueMessage = type === 'continue' ? continueMessage : '';
28232872 this.swipes = [];
28242873 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
28252874 this.messageLogprobs = [];
@@ -2972,8 +3021,7 @@ class StreamingProcessor {
29723021 await eventSource.emit(event_types.IMPERSONATE_READY, text);
29733022 }
29743023
2975- const continueMsg = this.type === 'continue' ? this.messageAlreadyGenerated : undefined;
3024+ saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
2976- saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), continueMsg);
29773025 await saveChatConditional();
29783026 unblockGeneration();
29793027 generatedPromptCache = '';
@@ -3069,7 +3117,7 @@ class StreamingProcessor {
30693117 if (logprobs) {
30703118 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
30713119 }
30723120 await sw.tick(() => this.onProgressStreaming(this.messageId, this.messageAlreadyGeneratedcontinueMessage + text));
30733121 }
30743122 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
30753123 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
@@ -3262,8 +3310,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
32623310 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
32633311 const isImpersonate = type == 'impersonate';
32643312
3265- let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
3266-
32673313 if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
32683314 const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
32693315
@@ -3702,7 +3748,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37023748 let oaiMessageExamples = [];
37033749
37043750 if (main_api === 'openai') {
3705- message_already_generated = '';
37063751 oaiMessages = setOpenAIMessages(coreChat);
37073752 oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray);
37083753 }
@@ -3845,7 +3890,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38453890 cyclePrompt += oai_settings.continue_postfix;
38463891 continue_mag += oai_settings.continue_postfix;
38473892 }
3848- message_already_generated = continue_mag;
38493893 }
38503894
38513895 const originalType = type;
@@ -3930,7 +3974,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
39303974
39313975 // Get instruct mode line
39323976 if (isInstruct && !isContinue) {
39333977 const name = (quiet_prompt && !quietToLoud && !isImpersonate) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
39343978 const isQuiet = quiet_prompt && type == 'quiet';
39353979 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);
39363980 }
@@ -4211,6 +4255,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42114255 }
42124256 }
42134257
4258+ await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data);
4259+
42144260 if (dryRun) {
42154261 generatedPromptCache = '';
42164262 return Promise.resolve();
@@ -4270,7 +4316,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42704316 console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);
42714317
42724318 if (isStreamingEnabled() && type !== 'quiet') {
42734319 streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, message_already_generatedcontinue_mag);
42744320 if (isContinue) {
42754321 // Save reply does add cycle text to the prompt, so it's not needed here
42764322 streamingProcessor.firstMessageText = '';
@@ -5074,7 +5120,7 @@ function setInContextMessages(lastmsg, type) {
50745120 * @param {object} data Generation data
50755121 * @returns {Promise<object>} Response data from the API
50765122 */
50775123export async function sendGenerationRequest(type, data) {
50785124 if (main_api === 'openai') {
50795125 return await sendOpenAIRequest(type, data.prompt, abortController.signal);
50805126 }
@@ -5106,7 +5152,7 @@ async function sendGenerationRequest(type, data) {
51065152 * @param {object} data Generation data
51075153 * @returns {Promise<any>} Streaming generator
51085154 */
51095155export async function sendStreamingRequest(type, data) {
51105156 if (abortController?.signal?.aborted) {
51115157 throw new Error('Generation was aborted.');
51125158 }
@@ -5387,9 +5433,11 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
53875433 getMessage = fixMarkdown(getMessage, false);
53885434 }
53895435
53905436 const nameToTrim2 = isImpersonate ? name1 : name2;
5437+ ? (!power_user.allow_name1_display ? name1 : '')
5438+ : (!power_user.allow_name2_display ? name2 : '');
53915439
53925440 if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) {
53935441 getMessage = getMessage.replace(nameToTrim2 + ':', '');
53945442 getMessage = getMessage.trimStart();
53955443 }
@@ -5610,6 +5658,7 @@ export function activateSendButtons() {
56105658 is_send_press = false;
56115659 $('#send_but').removeClass('displayNone');
56125660 $('#mes_continue').removeClass('displayNone');
5661+ $('#mes_impersonate').removeClass('displayNone');
56135662 $('.mes_buttons:last').show();
56145663 hideStopButton();
56155664}
@@ -5617,6 +5666,7 @@ export function activateSendButtons() {
56175666export function deactivateSendButtons() {
56185667 $('#send_but').addClass('displayNone');
56195668 $('#mes_continue').addClass('displayNone');
5669+ $('#mes_impersonate').addClass('displayNone');
56205670 showStopButton();
56215671}
56225672
@@ -6407,7 +6457,7 @@ export async function getSettings() {
64076457 loadHordeSettings(settings);
64086458
64096459 // Load power user settings
64106460 await loadPowerUserSettings(settings, data);
64116461
64126462 // Load character tags
64136463 loadTagsSettings(settings);
@@ -7917,6 +7967,8 @@ window['SillyTavern'].getContext = function () {
79177967 eventTypes: event_types,
79187968 addOneMessage: addOneMessage,
79197969 generate: Generate,
7970+ sendStreamingRequest: sendStreamingRequest,
7971+ sendGenerationRequest: sendGenerationRequest,
79207972 stopGeneration: stopGeneration,
79217973 getTokenCount: getTokenCount,
79227974 extensionPrompts: extension_prompts,
@@ -8334,6 +8386,12 @@ const CONNECT_API_MAP = {
83348386 button: '#api_button_openai',
83358387 source: chat_completion_sources.OPENAI,
83368388 },
8389+ // Google alias
8390+ 'google': {
8391+ selected: 'openai',
8392+ button: '#api_button_openai',
8393+ source: chat_completion_sources.MAKERSUITE,
8394+ },
83378395 // OpenRouter special naming, to differentiate between chat comp and text comp
83388396 'openrouter': {
83398397 selected: 'openai',
@@ -8347,6 +8405,9 @@ const CONNECT_API_MAP = {
83478405 },
83488406};
83498407
8408+// Collect all unique API names in an array
8409+export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8410+
83508411// Fill connections map from textgen_types and chat_completion_sources
83518412for (const textGenType of Object.values(textgen_types)) {
83528413 if (CONNECT_API_MAP[textGenType]) continue;
@@ -8416,7 +8477,7 @@ async function disableInstructCallback() {
84168477/**
84178478 * @param {string} text API name
84188479 */
84198480async function connectAPISlash(_args, text) {
84208481 if (!text.trim()) {
84218482 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
84228483 if (config.selected !== main_api) continue;
@@ -8439,12 +8500,15 @@ async function connectAPISlash(_, text) {
84398500
84408501 return key;
84418502 }
8503+
8504+ console.error('FIXME: The current API is not in the API map');
8505+ return '';
84428506 }
84438507
84448508 const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
84458509 if (!apiConfig) {
84468510 toastr.error(`Error: ${text} is not a valid API`);
84478511 return '';
84488512 }
84498513
84508514 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
@@ -8464,14 +8528,18 @@ async function connectAPISlash(_, text) {
84648528 $(apiConfig.button).trigger('click');
84658529 }
84668530
8467- toastr.info(`API set to ${text}, trying to connect..`);
8531+ const quiet = isTrueBoolean(args?.quiet);
8532+ const toast = quiet ? jQuery() : toastr.info(`API set to ${text}, trying to connect..`);
84688533
84698534 try {
84708535 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
84718536 console.log('Connection successful');
84728537 } catch {
84738538 console.log('Could not connect after 510 seconds, skipping.');
84748539 }
8540+
8541+ toastr.clear(toast);
8542+ return text;
84758543}
84768544
84778545/**
@@ -8496,7 +8564,7 @@ export async function processDroppedFiles(files, data = new Map()) {
84968564
84978565 for (const file of files) {
84988566 const extension = file.name.split('.').pop().toLowerCase();
84998567 if (allowedMimeTypes.includessome(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) {
85008568 const preservedName = data instanceof Map && data.get(file);
85018569 await importCharacter(file, preservedName);
85028570 } else {
@@ -8921,9 +8989,6 @@ jQuery(async function () {
89218989 return '';
89228990 }
89238991
8924- // Collect all unique API names in an array
8925- const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8926-
89278992 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89288993 name: 'dupe',
89298994 callback: duplicateCharacter,
@@ -8932,13 +8997,22 @@ jQuery(async function () {
89328997 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89338998 name: 'api',
89348999 callback: connectAPISlash,
9000+ returns: 'the current API',
9001+ namedArgumentList: [
9002+ SlashCommandNamedArgument.fromProps({
9003+ name: 'quiet',
9004+ description: 'Suppress the toast message on connection',
9005+ typeList: [ARGUMENT_TYPE.BOOLEAN],
9006+ defaultValue: 'false',
9007+ enumList: commonEnumProviders.boolean('trueFalse')(),
9008+ }),
9009+ ],
89359010 unnamedArgumentList: [
89369011 SlashCommandArgument.fromProps({
89379012 description: 'API to connect to',
89389013 typeList: [ARGUMENT_TYPE.STRING],
8939- isRequired: false,
89409014 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
89419015 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIsUNIQUE_APIS.findIndex(x => x === selected)),
89429016 selected[0].toUpperCase() ?? enumIcons.default)),
89439017 }),
89449018 ],
@@ -9095,14 +9169,14 @@ jQuery(async function () {
90959169 $('#send_textarea').on('focusin focus click', () => {
90969170 S_TAPreviouslyFocused = true;
90979171 });
90989172 $('#send_but, #option_regenerate, #option_continue, #mes_continue, #mes_impersonate').on('click', () => {
90999173 if (S_TAPreviouslyFocused) {
91009174 $('#send_textarea').focus();
91019175 }
91029176 });
91039177 $(document).click(event => {
91049178 if ($(':focus').attr('id') !== 'send_textarea') {
91059179 var validIDs = ['options_button', 'send_but', 'mes_impersonate', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];
91069180 if (!validIDs.includes($(event.target).attr('id'))) {
91079181 S_TAPreviouslyFocused = false;
91089182 }
@@ -9138,6 +9212,9 @@ jQuery(async function () {
91389212 debouncedCharacterSearch(searchQuery);
91399213 });
91409214
9215+ $('#mes_impersonate').on('click', function () {
9216+ $('#option_impersonate').trigger('click');
9217+ });
91419218
91429219 $('#mes_continue').on('click', function () {
91439220 $('#option_continue').trigger('click');
@@ -10435,8 +10512,9 @@ jQuery(async function () {
1043510512 }
1043610513
1043710514 // Set the height of "autoSetHeight" textareas within the drawer to their scroll height
1043810515 $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {
1043910516 await resetScrollHeight($(this));
10517+ return;
1044010518 });
1044110519
1044210520 } else if (drawerWasOpenAlready) { //to close manually
@@ -10509,8 +10587,9 @@ jQuery(async function () {
1050910587 $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle();
1051010588
1051110589 // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height
1051210590 $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(async function () {
1051310591 await resetScrollHeight($(this));
10592+ return;
1051410593 });
1051510594 });
1051610595
@@ -10591,15 +10670,31 @@ jQuery(async function () {
1059110670 }
1059210671 });
1059310672
1059410673 $(document).onaddEventListener('click', '#OpenAllWIEntries', function (e) {
10595- $('#world_popup_entries_list').children().find('.down').click();
10674+ if (!(e.target instanceof HTMLElement)) return;
10596- });
10675+ if (e.target.matches('#OpenAllWIEntries')) {
10597- $(document).on('click', '#CloseAllWIEntries', function () {
10676+ document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10598- $('#world_popup_entries_list').children().find('.up').click();
10677+ toggleDrawer(drawer, true);
10678+ });
10679+ } else if (e.target.matches('#CloseAllWIEntries')) {
10680+ document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10681+ toggleDrawer(drawer, false);
10682+ });
10683+ }
1059910684 });
10685+
1060010686 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
1060110687 /* $('#set_character_world').on('click', openCharacterWorldPopup); */
1060210688
10689+ $(document).on('focus', 'input.auto-select, textarea.auto-select', function () {
10690+ if (!power_user.enable_auto_select_input) return;
10691+ const control = $(this)[0];
10692+ if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {
10693+ control.select();
10694+ console.debug('Auto-selecting content of input control', control);
10695+ }
10696+ });
10697+
1060310698 $(document).keyup(function (e) {
1060410699 if (e.key === 'Escape') {
1060510700 const isEditVisible = $('#curEditTextarea').is(':visible');
@@ -10683,7 +10778,7 @@ jQuery(async function () {
1068310778 }
1068410779 } break;
1068510780 case 'import_tags': {
1068610781 await importTags(characters[this_chid], { forceShowimportSetting: truetag_import_setting.ASK });
1068710782 } break;
1068810783 /*case 'delete_button':
1068910784 popup_type = "del_ch";
@@ -10712,62 +10807,66 @@ jQuery(async function () {
1071210807 var isManualInput = false;
1071310808 var valueBeforeManualInput;
1071410809
1071510810 $(document).on('input', '.range-block-counter input, .neo-range-input').on('click', function () {
1071610811 valueBeforeManualInput = $(this).val();
1071710812 console.log(valueBeforeManualInput);
1071810813 });
10719- .on('change', function (e) {
10814+
10720- e.target.focus();
10815+ $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {
1072110816 e.target.dispatchEvent(new Eventfocus('keyup'));
10722- })
10817+ e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
10723- .on('keydown', function (e) {
10818+ });
10724- const masterSelector = '#' + $(this).data('for');
10819+
10725- const masterElement = $(masterSelector);
10820+ $(document).on('keydown', '.range-block-counter input, .neo-range-input', function (e) {
10726- if (e.key === 'Enter') {
10821+ const masterSelector = '#' + $(this).data('for');
1072710822 let const manualInputmasterElement = Number($(this).val()masterSelector);
10728- if (isManualInput) {
10823+ if (e.key === 'Enter') {
10729- //disallow manual inputs outside acceptable range
10730- if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10731- //if value is ok, assign to slider and update handle text and position
10732- //newSlider.val(manualInput)
10733- //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
10734- valueBeforeManualInput = manualInput;
10735- $(masterElement).val($(this).val()).trigger('input', { forced: true });
10736- } else {
10737- //if value not ok, warn and reset to last known valid value
10738- toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10739- console.log(valueBeforeManualInput);
10740- //newSlider.val(valueBeforeManualInput)
10741- $(this).val(valueBeforeManualInput);
10742- }
10743- }
10744- }
10745- })
10746- .on('keyup', function () {
10747- valueBeforeManualInput = $(this).val();
10748- console.log(valueBeforeManualInput);
10749- isManualInput = true;
10750- })
10751- //trigger slider changes when user clicks away
10752- .on('mouseup blur', function () {
10753- const masterSelector = '#' + $(this).data('for');
10754- const masterElement = $(masterSelector);
1075510824 let manualInput = Number($(this).val());
1075610825 if (isManualInput) {
1075710826 //ifdisallow valuemanual isinputs betweenoutside correctacceptable range for the slider
1075810827 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10828+ //if value is ok, assign to slider and update handle text and position
10829+ //newSlider.val(manualInput)
10830+ //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
1075910831 valueBeforeManualInput = manualInput;
10760- //set the slider value to input value
1076110832 $(masterElement).val($(this).val()).trigger('input', { forced: true });
1076210833 } else {
1076310834 //if value not ok, warn and reset to last known valid value
1076410835 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
1076510836 console.log(valueBeforeManualInput);
10837+ //newSlider.val(valueBeforeManualInput)
1076610838 $(this).val(valueBeforeManualInput);
1076710839 }
1076810840 }
10769- isManualInput = false;
10841+ }
1077010842 });
10843+
10844+ $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {
10845+ valueBeforeManualInput = $(this).val();
10846+ console.log(valueBeforeManualInput);
10847+ isManualInput = true;
10848+ });
10849+
10850+ //trigger slider changes when user clicks away
10851+ $(document).on('mouseup blur', '.range-block-counter input, .neo-range-input', function () {
10852+ const masterSelector = '#' + $(this).data('for');
10853+ const masterElement = $(masterSelector);
10854+ let manualInput = Number($(this).val());
10855+ if (isManualInput) {
10856+ //if value is between correct range for the slider
10857+ if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10858+ valueBeforeManualInput = manualInput;
10859+ //set the slider value to input value
10860+ $(masterElement).val($(this).val()).trigger('input', { forced: true });
10861+ } else {
10862+ //if value not ok, warn and reset to last known valid value
10863+ toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10864+ console.log(valueBeforeManualInput);
10865+ $(this).val(valueBeforeManualInput);
10866+ }
10867+ }
10868+ isManualInput = false;
10869+ });
1077110870
1077210871 $('.user_stats_button').on('click', function () {
1077310872 userStatsHandler();
public/scripts/BulkEditOverlay.js+34 -4
@@ -18,7 +18,7 @@ import {
1818import { favsToHotswap } from './RossAscends-mods.js';
1919import { hideLoader, showLoader } from './loader.js';
2020import { convertCharacterToPersona } from './personas.js';
2121import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
2222
2323/**
2424 * Static object representing the actions of the
@@ -197,10 +197,10 @@ class BulkTagPopupHandler {
197197 #getHtml = () => {
198198 const characterData = JSON.stringify({ characterIds: this.characterIds });
199199 return `<div id="bulk_tag_shadow_popup">
200200 <div id="bulk_tag_popup" class="wider_dialogue_popup">
201201 <div id="bulk_tag_popup_holder">
202202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
203203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters. Import all or existing tags for all selected characters.</small>
204204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
205205 <br>
206206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
@@ -219,6 +219,12 @@ class BulkTagPopupHandler {
219219 <i class="fa-solid fa-trash-can margin-right-10px"></i>
220220 Mutual
221221 </div>
222+ <div id="bulk_tag_popup_import_all_tags" class="menu_button" title="Import all tags from selected characters" data-i18n="[title]Import all tags from selected characters">
223+ Import All
224+ </div>
225+ <div id="bulk_tag_popup_import_existing_tags" class="menu_button" title="Import existing tags from selected characters" data-i18n="[title]Import existing tags from selected characters">
226+ Import Existing
227+ </div>
222228 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
223229 </div>
224230 </div>
@@ -254,6 +260,30 @@ class BulkTagPopupHandler {
254260 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
255261 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
256262 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
263+ document.querySelector('#bulk_tag_popup_import_all_tags').addEventListener('click', this.importAllTags.bind(this));
264+ document.querySelector('#bulk_tag_popup_import_existing_tags').addEventListener('click', this.importExistingTags.bind(this));
265+ }
266+
267+ /**
268+ * Import existing tags for all selected characters
269+ */
270+ async importExistingTags() {
271+ for (const characterId of this.characterIds) {
272+ await importTags(characters[characterId], { importSetting: tag_import_setting.ONLY_EXISTING });
273+ }
274+
275+ $('#bulkTagList').empty();
276+ }
277+
278+ /**
279+ * Import all tags for all selected characters
280+ */
281+ async importAllTags() {
282+ for (const characterId of this.characterIds) {
283+ await importTags(characters[characterId], { importSetting: tag_import_setting.ALL });
284+ }
285+
286+ $('#bulkTagList').empty();
257287 }
258288
259289 /**
@@ -570,7 +600,7 @@ class BulkEditOverlay {
570600 this.container.removeEventListener('mouseup', cancelHold);
571601 this.container.removeEventListener('touchend', cancelHold);
572602 },
573603 BulkEditOverlay.longPressDelay);
574604 };
575605
576606 handleLongPressEnd = (event) => {
public/scripts/RossAscends-mods.js+8 -4
@@ -311,6 +311,7 @@ function RA_checkOnlineStatus() {
311311 $('#send_form').addClass('no-connection'); //entire input form area is red when not connected
312312 $('#send_but').addClass('displayNone'); //send button is hidden when not connected;
313313 $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
314+ $('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected;
314315 $('#API-status-top').removeClass('fa-plug');
315316 $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow');
316317 connection_made = false;
@@ -327,6 +328,7 @@ function RA_checkOnlineStatus() {
327328 if (!is_send_press && !(selected_group && is_group_generating)) {
328329 $('#send_but').removeClass('displayNone'); //on connect, send button shows
329330 $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected
331+ $('#mes_impersonate').removeClass('displayNone'); //continue button is shown when connected
330332 }
331333 }
332334 }
@@ -378,6 +380,7 @@ function RA_autoconnect(PrevApi) {
378380 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
379381 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
380382 || (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)
381384 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
382385 ) {
383386 $('#api_button_openai').trigger('click');
@@ -951,6 +954,11 @@ export function initRossMods() {
951954 * @param {KeyboardEvent} event
952955 */
953956 async function processHotkeys(event) {
957+ // Default hotkeys and shortcuts shouldn't work if any popup is currently open
958+ if (Popup.util.isPopupOpen()) {
959+ return;
960+ }
961+
954962 //Enter to send when send_textarea in focus
955963 if (document.activeElement == hotkeyTargets['send_textarea']) {
956964 const sendOnEnter = shouldSendOnEnter();
@@ -1104,10 +1112,6 @@ export function initRossMods() {
11041112 }
11051113
11061114 if (event.key == 'Escape') { //closes various panels
1107- // Do not close panels if we are currently inside a popup
1108- if (Popup.util.isPopupOpen())
1109- return;
1110-
11111115 //dont override Escape hotkey functions from script.js
11121116 //"close edit box" and "cancel stream generation".
11131117 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {
public/scripts/autocomplete/AutoComplete.js+71 -48
@@ -16,8 +16,15 @@ export const AUTOCOMPLETE_WIDTH = {
1616 'FULL': 2,
1717};
1818
19+/**@readonly*/
20+/**@enum {Number}*/
21+export const AUTOCOMPLETE_SELECT_KEY = {
22+ 'TAB': 1, // 2^0
23+ 'ENTER': 2, // 2^1
24+};
25+
1926export class AutoComplete {
2027 /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
2128 /**@type {boolean}*/ isFloating = false;
2229 /**@type {()=>boolean}*/ checkIfActivate;
2330 /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
@@ -56,6 +63,8 @@ export class AutoComplete {
5663 /**@type {function}*/ updateDetailsPositionDebounced;
5764 /**@type {function}*/ updateFloatingPositionDebounced;
5865
66+ /**@type {(item:AutoCompleteOption)=>any}*/ onSelect;
67+
5968 get matchType() {
6069 return power_user.stscript.matching ?? 'fuzzy';
6170 }
@@ -68,7 +77,7 @@ export class AutoComplete {
6877
6978
7079 /**
7180 * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete.
7281 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
7382 * @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.
7483 * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
@@ -102,10 +111,15 @@ export class AutoComplete {
102111 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
103112 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
104113
105- textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced));
114+ textarea.addEventListener('input', ()=>{
115+ this.selectionStart = this.textarea.selectionStart;
116+ if (this.text != this.textarea.value) this.show(true, this.wasForced);
117+ });
106118 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
107119 textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);{
108- textarea.addEventListener('selectionchange', ()=>this.show());
120+ this.selectionStart = this.textarea.selectionStart;
121+ if (this.isActive) this.show();
122+ });
109123 textarea.addEventListener('blur', ()=>this.hide());
110124 if (isFloating) {
111125 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
@@ -189,6 +203,11 @@ export class AutoComplete {
189203 * @returns The option.
190204 */
191205 fuzzyScore(option) {
206+ // might have been matched by the options matchProvider function instead
207+ if (!this.fuzzyRegex.test(option.name)) {
208+ option.score = new AutoCompleteFuzzyScore(Number.MAX_SAFE_INTEGER, -1);
209+ return option;
210+ }
192211 const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
193212 let start = null;
194213 let consecutive = [];
@@ -339,7 +358,7 @@ export class AutoComplete {
339358
340359 this.result = this.effectiveParserResult.optionList
341360 // filter the list of options by the partial name according to the matching type
342361 .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)
343362 // remove aliases
344363 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
345364
@@ -357,10 +376,11 @@ export class AutoComplete {
357376 // build element
358377 option.dom = this.makeItem(option);
359378 // update replacer and add quotes if necessary
379+ const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name;
360380 if (this.effectiveParserResult.canBeQuoted) {
361381 option.replacer = option.nameoptionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionoptionName.namereplace(/"/g, '\\"')}"` : `${option.nameoptionName}`;
362382 } else {
363383 option.replacer = option.nameoptionName;
364384 }
365385 // calculate fuzzy score if matching is fuzzy
366386 if (this.matchType == 'fuzzy') this.fuzzyScore(option);
@@ -399,7 +419,7 @@ export class AutoComplete {
399419 ,
400420 );
401421 this.result.push(option);
402422 } else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) {
403423 // only one result that is exactly the current value? just show hint, no autocomplete
404424 this.isReplaceable = false;
405425 this.isShowingDetails = false;
@@ -439,11 +459,14 @@ export class AutoComplete {
439459 } else {
440460 item.dom.classList.remove('selected');
441461 }
462+ if (!item.isSelectable) {
463+ item.dom.classList.add('not-selectable');
464+ }
442465 frag.append(item.dom);
443466 }
444467 this.dom.append(frag);
445468 this.updatePosition();
446469 getTopmostModalLayerthis.getLayer().append(this.domWrap);
447470 } else {
448471 this.domWrap.remove();
449472 }
@@ -458,10 +481,17 @@ export class AutoComplete {
458481 if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
459482 this.detailsDom.innerHTML = '';
460483 this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
461484 getTopmostModalLayerthis.getLayer().append(this.detailsWrap);
462485 this.updateDetailsPositionDebounced();
463486 }
464487
488+ /**
489+ * @returns {HTMLElement} closest ancestor dialog or body
490+ */
491+ getLayer() {
492+ return this.textarea.closest('dialog, body');
493+ }
494+
465495
466496
467497 /**
@@ -474,7 +504,7 @@ export class AutoComplete {
474504 const rect = {};
475505 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
476506 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
477507 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayerthis.getLayer().getBoundingClientRect();
478508 this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
479509 this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
480510 this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
@@ -501,7 +531,7 @@ export class AutoComplete {
501531 const rect = {};
502532 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
503533 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
504534 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayerthis.getLayer().getBoundingClientRect();
505535 if (this.isReplaceable) {
506536 this.detailsWrap.classList.remove('full');
507537 const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
@@ -527,32 +557,34 @@ export class AutoComplete {
527557 updateFloatingPosition() {
528558 const location = this.getCursorPosition();
529559 const rect = this.textarea.getBoundingClientRect();
560+ const layerRect = this.getLayer().getBoundingClientRect();
530561 // cursor is out of view -> hide
531562 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
532563 return this.hide();
533564 }
534565 const left = Math.max(rect.left, location.left) - layerRect.left;
535566 this.domWrap.style.setProperty('--targetOffset', `${left}`);
536567 if (location.top <= window.innerHeight / 2) {
537568 // if cursor is in lower half of window, show list above line
538569 this.domWrap.style.top = `${location.bottom - layerRect.top}px`;
539570 this.domWrap.style.bottom = 'auto';
540571 this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
541572 } else {
542573 // if cursor is in upper half of window, show list below line
543574 this.domWrap.style.top = 'auto';
544575 this.domWrap.style.bottom = `calc(100vh${layerRect.height}px - ${location.top - layerRect.top}px)`;
545576 this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
546577 }
547578 }
548579
549580 updateFloatingDetailsPosition(location = null) {
550581 if (!location) location = this.getCursorPosition();
551582 const rect = this.textarea.getBoundingClientRect();
583+ const layerRect = this.getLayer().getBoundingClientRect();
552584 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
553585 return this.hide();
554586 }
555587 const left = Math.max(rect.left, location.left) - layerRect.left;
556588 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
557589 if (this.isReplaceable) {
558590 this.detailsWrap.classList.remove('full');
@@ -572,14 +604,14 @@ export class AutoComplete {
572604 }
573605 if (location.top <= window.innerHeight / 2) {
574606 // if cursor is in lower half of window, show list above line
575607 this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`;
576608 this.detailsWrap.style.bottom = 'auto';
577609 this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
578610 } else {
579611 // if cursor is in upper half of window, show list below line
580612 this.detailsWrap.style.top = 'auto';
581613 this.detailsWrap.style.bottom = `calc(100vh${layerRect.height}px - ${location.top - layerRect.top}px)`;
582614 this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
583615 }
584616 }
585617
@@ -597,7 +629,7 @@ export class AutoComplete {
597629 }
598630 this.clone.style.position = 'fixed';
599631 this.clone.style.visibility = 'hidden';
600632 getTopmostModalLayer()document.body.append(this.clone);
601633 const mo = new MutationObserver(muts=>{
602634 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
603635 this.clone.remove();
@@ -656,6 +688,7 @@ export class AutoComplete {
656688 }
657689 this.wasForced = false;
658690 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
691+ this.onSelect?.(this.selectedItem);
659692 }
660693
661694
@@ -708,8 +741,10 @@ export class AutoComplete {
708741 }
709742 case 'Enter': {
710743 // pick the selected item to autocomplete
744+ if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break;
711745 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
712746 if (this.selectedItem.name == this.name) break;
747+ if (!this.selectedItem.isSelectable) break;
713748 evt.preventDefault();
714749 evt.stopImmediatePropagation();
715750 this.select();
@@ -717,9 +752,11 @@ export class AutoComplete {
717752 }
718753 case 'Tab': {
719754 // pick the selected item to autocomplete
755+ if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break;
720756 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
721757 evt.preventDefault();
722758 evt.stopImmediatePropagation();
759+ if (!this.selectedItem.isSelectable) break;
723760 this.select();
724761 return;
725762 }
@@ -772,30 +809,16 @@ export class AutoComplete {
772809 // ignore keydown on modifier keys
773810 return;
774811 }
775- switch (evt.key) {
812+ // await keyup to see if cursor position or text has changed
776- case 'ArrowUp':
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+ });
780817 if (this.isActiveselectionStart != this.textarea.selectionStart) {
781- // keyboard navigation, wait for keyup to complete cursor move
818+ this.selectionStart = this.textarea.selectionStart;
782819 const 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- }
799822 }
800823 }
801824}
public/scripts/autocomplete/AutoCompleteNameResult.js+3 -30
@@ -1,36 +1,9 @@
11import { SlashCommandNamedArgumentAutoCompleteOptionAutoCompleteNameResultBase } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOptionAutoCompleteNameResultBase.js';
22import { AutoCompleteOptionAutoCompleteSecondaryNameResult } from './AutoCompleteOptionAutoCompleteSecondaryNameResult.js';
3-// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
43
54
65
76export class AutoCompleteNameResult extends AutoCompleteNameResultBase {
8- /**@type {string} */ name;
9- /**@type {number} */ start;
10- /**@type {AutoCompleteOption[]} */ optionList = [];
11- /**@type {boolean} */ canBeQuoted = false;
12- /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
13- /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
14-
15-
16- /**
17- * @param {string} name Name (potentially partial) of the name at the requested index.
18- * @param {number} start Index where the name starts.
19- * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
20- * @param {boolean} canBeQuoted Whether the name can be inside quotes.
21- * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
22- * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
23- */
24- constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
25- this.name = name;
26- this.start = start;
27- this.optionList = optionList;
28- this.canBeQuoted = canBeQuoted;
29- this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
30- this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
31- }
32-
33-
347 /**
358 *
369 * @param {string} text The whole text
public/scripts/autocomplete/AutoCompleteNameResultBase.js+31 -0
@@ -0,0 +1,31 @@
1+import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
2+import { AutoCompleteOption } from './AutoCompleteOption.js';
3+
4+
5+
6+export class AutoCompleteNameResultBase {
7+ /**@type {string} */ name;
8+ /**@type {number} */ start;
9+ /**@type {AutoCompleteOption[]} */ optionList = [];
10+ /**@type {boolean} */ canBeQuoted = false;
11+ /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
12+ /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
13+
14+
15+ /**
16+ * @param {string} name Name (potentially partial) of the name at the requested index.
17+ * @param {number} start Index where the name starts.
18+ * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
19+ * @param {boolean} canBeQuoted Whether the name can be inside quotes.
20+ * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
21+ * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
22+ */
23+ constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
24+ this.name = name;
25+ this.start = start;
26+ this.optionList = optionList;
27+ this.canBeQuoted = canBeQuoted;
28+ this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
29+ this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
30+ }
31+}
public/scripts/autocomplete/AutoCompleteOption.js+11 -1
@@ -11,6 +11,9 @@ export class AutoCompleteOption {
1111 /**@type {AutoCompleteFuzzyScore}*/ score;
1212 /**@type {string}*/ replacer;
1313 /**@type {HTMLElement}*/ dom;
14+ /**@type {(input:string)=>boolean}*/ matchProvider;
15+ /**@type {(input:string)=>string}*/ valueProvider;
16+ /**@type {boolean}*/ makeSelectable = false;
1417
1518
1619 /**
@@ -21,14 +24,21 @@ export class AutoCompleteOption {
2124 return this.name;
2225 }
2326
27+ get isSelectable() {
28+ return this.makeSelectable || !this.valueProvider;
29+ }
30+
2431
2532 /**
2633 * @param {string} name
2734 */
2835 constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) {
2936 this.name = name;
3037 this.typeIcon = typeIcon;
3138 this.type = type;
39+ this.matchProvider = matchProvider;
40+ this.valueProvider = valueProvider;
41+ this.makeSelectable = makeSelectable;
3242 }
3343
3444
public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js+2 -2
@@ -1,6 +1,6 @@
11import { AutoCompleteNameResultAutoCompleteNameResultBase } from './AutoCompleteNameResultAutoCompleteNameResultBase.js';
22
33export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultAutoCompleteNameResultBase {
44 /**@type {boolean}*/ isRequired = false;
55 /**@type {boolean}*/ forceMatch = true;
66}
public/scripts/extensions.js+49 -22
@@ -21,6 +21,7 @@ const defaultUrl = 'http://localhost:5100';
2121let saveMetadataTimeout = null;
2222
2323let requiresReload = false;
24+let stateChanged = false;
2425
2526export function saveMetadataDebounced() {
2627 const context = getContext();
@@ -238,6 +239,7 @@ function onEnableExtensionClick() {
238239
239240async function enableExtension(name, reload = true) {
240241 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
242+ stateChanged = true;
241243 await saveSettings();
242244 if (reload) {
243245 location.reload();
@@ -248,6 +250,7 @@ async function enableExtension(name, reload = true) {
248250
249251async function disableExtension(name, reload = true) {
250252 extension_settings.disabledExtensions.push(name);
253+ stateChanged = true;
251254 await saveSettings();
252255 if (reload) {
253256 location.reload();
@@ -657,7 +660,29 @@ async function showExtensionsDetails() {
657660 await oldPopup.complete(POPUP_RESULT.CANCELLED);
658661 }
659662
660- const popup = new Popup(html, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton], allowVerticalScrolling: true });
663+ let waitingForSave = false;
664+
665+ const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
666+ okButton: 'Close',
667+ wide: true,
668+ large: true,
669+ customButtons: [updateAllButton],
670+ allowVerticalScrolling: true,
671+ onClosing: async () => {
672+ if (waitingForSave) {
673+ return false;
674+ }
675+ if (stateChanged) {
676+ waitingForSave = true;
677+ const toast = toastr.info('The page will be reloaded shortly...', 'Extensions state changed');
678+ await saveSettings();
679+ toastr.clear(toast);
680+ waitingForSave = false;
681+ requiresReload = true;
682+ }
683+ return true;
684+ },
685+ });
661686 popupPromise = popup.show();
662687 } catch (error) {
663688 toastr.error('Error loading extensions. See browser console for details.');
@@ -989,6 +1014,28 @@ export async function writeExtensionField(characterId, key, value) {
9891014 }
9901015}
9911016
1017+/**
1018+ * Prompts the user to enter the Git URL of the extension to import.
1019+ * After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
1020+ * If the extension is imported successfully, a success message is displayed.
1021+ * If the extension import fails, an error message is displayed and the error is logged to the console.
1022+ * After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
1023+ * @param {string} [suggestUrl] Suggested URL to install
1024+ * @returns {Promise<void>}
1025+ */
1026+export async function openThirdPartyExtensionMenu(suggestUrl = '') {
1027+ const html = await renderTemplateAsync('installExtension');
1028+ const input = await callGenericPopup(html, POPUP_TYPE.INPUT, suggestUrl ?? '');
1029+
1030+ if (!input) {
1031+ console.debug('Extension install cancelled');
1032+ return;
1033+ }
1034+
1035+ const url = String(input).trim();
1036+ await installExtension(url);
1037+}
1038+
9921039jQuery(async function () {
9931040 await addExtensionsButtonAndMenu();
9941041 $('#extensionsMenuButton').css('display', 'flex');
@@ -1004,28 +1051,8 @@ jQuery(async function () {
10041051
10051052 /**
10061053 * 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.
10121054 *
10131055 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
10141056 */
10151057 $('#third_party_extension_button').on('click', async () => {openThirdPartyExtensionMenu());
1016- const html = `<h3>Enter the Git URL of the extension to install</h3>
1017- <br>
1018- <p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
1019- <br>
1020- <p>Example: <tt> https://github.com/author/extension-name </tt></p>`;
1021- const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '');
1022-
1023- if (!input) {
1024- console.debug('Extension install cancelled');
1025- return;
1026- }
1027-
1028- const url = String(input).trim();
1029- await installExtension(url);
1030- });
10311058});
public/scripts/extensions/caption/index.js+7 -3
@@ -8,13 +8,12 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
88import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
99import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1010import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
11-import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
1211import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
1312export { MODULE_NAME };
1413
1514const MODULE_NAME = 'caption';
1615
1716const PROMPT_DEFAULT = 'What\'s in this image?';
1817const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';
1918
2019/**
@@ -170,7 +169,11 @@ async function sendCaptionedMessage(caption, image) {
170169 },
171170 };
172171 context.chat.push(message);
172+ const messageId = context.chat.length - 1;
173+ await eventSource.emit(event_types.MESSAGE_SENT, messageId);
173174 context.addOneMessage(message);
175+ await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
176+ await context.saveChat();
174177}
175178
176179/**
@@ -334,7 +337,7 @@ async function getCaptionForFile(file, prompt, quiet) {
334337 }
335338 catch (error) {
336339 const errorMessage = error.message || 'Unknown error';
337340 toastr.error(errorMessage, "'Failed to caption image."');
338341 console.error(error);
339342 return '';
340343 }
@@ -399,6 +402,7 @@ jQuery(async function () {
399402 (modules.includes('caption') && extension_settings.caption.source === 'extras') ||
400403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
401404 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
405+ (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||
402406 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
403407 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
404408 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
public/scripts/extensions/caption/settings.html+6 -1
@@ -17,9 +17,10 @@
1717 <div class="flex1 flex-container flexFlowColumn flexNoGap">
1818 <label for="caption_multimodal_api" data-i18n="API">API</label>
1919 <select id="caption_multimodal_api" class="flex1 text_pole">
20+ <option value="zerooneai">01.AI (Yi)</option>
2021 <option value="anthropic">Anthropic</option>
2122 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
2223 <option value="google">Google MakerSuiteAI Studio</option>
2324 <option value="koboldcpp">KoboldCpp</option>
2425 <option value="llamacpp">llama.cpp</option>
2526 <option value="ollama">Ollama</option>
@@ -32,16 +33,20 @@
3233 <div class="flex1 flex-container flexFlowColumn flexNoGap">
3334 <label for="caption_multimodal_model" data-i18n="Model">Model</label>
3435 <select id="caption_multimodal_model" class="flex1 text_pole">
36+ <option data-type="zerooneai" value="yi-vision">yi-vision</option>
3537 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
3638 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
3739 <option data-type="openai" value="gpt-4o">gpt-4o</option>
3840 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>
41+ <option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
3942 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
4043 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
4144 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
4245 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
4346 <option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
4447 <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
48+ <option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
49+ <option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
4550 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
4651 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
4752 <option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
public/scripts/extensions/expressions/index.js+2 -2
@@ -1,4 +1,4 @@
11import { callPopup, eventSource, event_types, generateQuietPromptgenerateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';
22import { dragElement, isMobile } from '../../RossAscends-mods.js';
33import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
44import { loadMovingUIState, power_user } from '../../power-user.js';
@@ -1156,7 +1156,7 @@ async function getExpressionLabel(text) {
11561156
11571157 functionResult = args?.arguments;
11581158 });
11591159 const emotionResponse = await generateQuietPromptgenerateRaw(prompttext, main_api, false, false, prompt);
11601160 return parseLlmResponse(functionResult || emotionResponse, expressionsList);
11611161 }
11621162 // Extras
public/scripts/extensions/memory/index.js+160 -31
@@ -1,4 +1,4 @@
11import { getStringHash, debounce, waitUntilCondition, extractAllWords, isTrueBoolean } from '../../utils.js';
22import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
33import {
44 activateSendButtons,
@@ -25,6 +25,8 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
2525import { SlashCommand } from '../../slash-commands/SlashCommand.js';
2626import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
2727import { MacrosParser } from '../../macros.js';
28+import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
29+import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
2830export { MODULE_NAME };
2931
3032const MODULE_NAME = '1_memory';
@@ -36,6 +38,41 @@ let lastMessageHash = null;
3638let lastMessageId = null;
3739let inApiCall = false;
3840
41+/**
42+ * Count the number of tokens in the provided text.
43+ * @param {string} text Text to count tokens for
44+ * @param {number} padding Number of additional tokens to add to the count
45+ * @returns {Promise<number>} Number of tokens in the text
46+ */
47+async function countSourceTokens(text, padding = 0) {
48+ if (extension_settings.memory.source === summary_sources.webllm) {
49+ const count = await countWebLlmTokens(text);
50+ return count + padding;
51+ }
52+
53+ if (extension_settings.memory.source === summary_sources.extras) {
54+ const count = getTextTokens(tokenizers.GPT2, text).length;
55+ return count + padding;
56+ }
57+
58+ return await getTokenCountAsync(text, padding);
59+}
60+
61+async function getSourceContextSize() {
62+ const overrideLength = extension_settings.memory.overrideResponseLength;
63+
64+ if (extension_settings.memory.source === summary_sources.webllm) {
65+ const maxContext = await getWebLlmContextSize();
66+ return overrideLength > 0 ? (maxContext - overrideLength) : Math.round(maxContext * 0.75);
67+ }
68+
69+ if (extension_settings.source === summary_sources.extras) {
70+ return 1024 - 64;
71+ }
72+
73+ return getMaxContextSize(overrideLength);
74+}
75+
3976const formatMemoryValue = function (value) {
4077 if (!value) {
4178 return '';
@@ -55,6 +92,7 @@ const saveChatDebounced = debounce(() => getContext().saveChat(), debounce_timeo
5592const summary_sources = {
5693 'extras': 'extras',
5794 'main': 'main',
95+ 'webllm': 'webllm',
5896};
5997
6098const prompt_builders = {
@@ -130,12 +168,12 @@ function loadSettings() {
130168
131169async function onPromptForceWordsAutoClick() {
132170 const context = getContext();
133171 const maxPromptLength = getMaxContextSizeawait getSourceContextSize(extension_settings.memory.overrideResponseLength);
134172 const chat = context.chat;
135173 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
136174 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
137175 const averageMessageWordCount = messagesWordCount / allMessages.length;
138176 const tokensPerWord = await getTokenCountAsynccountSourceTokens(allMessages.join('\n')) / messagesWordCount;
139177 const wordsPerToken = 1 / tokensPerWord;
140178 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);
141179 // How many words should pass so that messages will start be dropped out of context;
@@ -168,15 +206,15 @@ async function onPromptForceWordsAutoClick() {
168206
169207async function onPromptIntervalAutoClick() {
170208 const context = getContext();
171209 const maxPromptLength = getMaxContextSizeawait getSourceContextSize(extension_settings.memory.overrideResponseLength);
172210 const chat = context.chat;
173211 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
174212 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
175213 const messagesTokenCount = await getTokenCountAsynccountSourceTokens(allMessages.join('\n'));
176214 const tokensPerWord = messagesTokenCount / messagesWordCount;
177215 const averageMessageTokenCount = messagesTokenCount / allMessages.length;
178216 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);
179217 const promptTokens = await getTokenCountAsynccountSourceTokens(extension_settings.memory.prompt);
180218 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;
181219 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
182220 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);
@@ -213,8 +251,8 @@ function onSummarySourceChange(event) {
213251
214252function switchSourceControls(value) {
215253 $('#memory_settings [data-summary-source]').each((_, element) => {
216254 const source = $(element).datadataset.summarySource.split('summary-source,').map(s => s.trim());
217255 $(element).toggle(source === .includes(value));
218256 });
219257}
220258
@@ -353,10 +391,13 @@ function getIndexOfLatestChatSummary(chat) {
353391
354392async function onChatEvent() {
355393 // Module not enabled
356394 if (extension_settings.memory.source === summary_sources.extras && !modules.includes('summarize')) {
357- if (!modules.includes('summarize')) {
395+ return;
358- return;
396+ }
359- }
397+
398+ // WebLLM is not supported
399+ if (extension_settings.memory.source === summary_sources.webllm && !isWebLlmSupported()) {
400+ return;
360401 }
361402
362403 const context = getContext();
@@ -416,7 +457,12 @@ async function onChatEvent() {
416457 }
417458}
418459
419-async function forceSummarizeChat() {
460+/**
461+ * Forces a summary generation for the current chat.
462+ * @param {boolean} quiet If an informational toast should be displayed
463+ * @returns {Promise<string>} Summarized text
464+ */
465+async function forceSummarizeChat(quiet) {
420466 if (extension_settings.memory.source === summary_sources.extras) {
421467 toastr.warning('Force summarization is not supported for Extras API');
422468 return;
@@ -431,8 +477,12 @@ async function forceSummarizeChat() {
431477 return '';
432478 }
433479
434480 const toast = quiet ? jQuery() : toastr.info('Summarizing chat...', 'Please wait', { timeOut: 0, extendedTimeOut: 0 });
435- const value = await summarizeChatMain(context, true, skipWIAN);
481+ const value = extension_settings.memory.source === summary_sources.main
482+ ? await summarizeChatMain(context, true, skipWIAN)
483+ : await summarizeChatWebLLM(context, true);
484+
485+ toastr.clear(toast);
436486
437487 if (!value) {
438488 toastr.warning('Failed to summarize chat');
@@ -450,9 +500,10 @@ async function forceSummarizeChat() {
450500async function summarizeCallback(args, text) {
451501 text = text.trim();
452502
453503 // Using forceSummarizeChat to summarizeSummarize the current chat if no text provided
454504 if (!text) {
455505 returnconst awaitquiet forceSummarizeChat= isTrueBoolean(args.quiet);
506+ return await forceSummarizeChat(quiet);
456507 }
457508
458509 const source = args.source || extension_settings.memory.source;
@@ -464,6 +515,11 @@ async function summarizeCallback(args, text) {
464515 return await callExtrasSummarizeAPI(text);
465516 case summary_sources.main:
466517 return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
518+ case summary_sources.webllm: {
519+ const messages = [{ role: 'system', content: prompt }, { role: 'user', content: text }].filter(m => m.content);
520+ const params = extension_settings.memory.overrideResponseLength > 0 ? { max_tokens: extension_settings.memory.overrideResponseLength } : {};
521+ return await generateWebLlmChatPrompt(messages, params);
522+ }
467523 default:
468524 toastr.warning('Invalid summarization source specified');
469525 return '';
@@ -484,16 +540,25 @@ async function summarizeChat(context) {
484540 case summary_sources.main:
485541 await summarizeChatMain(context, false, skipWIAN);
486542 break;
543+ case summary_sources.webllm:
544+ await summarizeChatWebLLM(context, false);
545+ break;
487546 default:
488547 break;
489548 }
490549}
491550
492-async function summarizeChatMain(context, force, skipWIAN) {
551+/**
493-
552+ * Check if the chat should be summarized based on the current conditions.
553+ * Return summary prompt if it should be summarized.
554+ * @param {any} context ST context
555+ * @param {boolean} force Summarize the chat regardless of the conditions
556+ * @returns {Promise<string>} Summary prompt or empty string
557+ */
558+async function getSummaryPromptForNow(context, force) {
494559 if (extension_settings.memory.promptInterval === 0 && !force) {
495560 console.debug('Prompt interval is set to 0, skipping summarization');
496561 return '';
497562 }
498563
499564 try {
@@ -505,17 +570,17 @@ async function summarizeChatMain(context, force, skipWIAN) {
505570 waitUntilCondition(() => is_send_press === false, 30000, 100);
506571 } catch {
507572 console.debug('Timeout waiting for is_send_press');
508573 return '';
509574 }
510575
511576 if (!context.chat.length) {
512577 console.debug('No messages in chat to summarize');
513578 return '';
514579 }
515580
516581 if (context.chat.length < extension_settings.memory.promptInterval && !force) {
517582 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);
518583 return '';
519584 }
520585
521586 let messagesSinceLastSummary = 0;
@@ -539,7 +604,7 @@ async function summarizeChatMain(context, force, skipWIAN) {
539604
540605 if (!conditionSatisfied && !force) {
541606 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);
542607 return '';
543608 }
544609
545610 console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);
@@ -547,6 +612,63 @@ async function summarizeChatMain(context, force, skipWIAN) {
547612
548613 if (!prompt) {
549614 console.debug('Summarization prompt is empty. Skipping summarization.');
615+ return '';
616+ }
617+
618+ return prompt;
619+}
620+
621+async function summarizeChatWebLLM(context, force) {
622+ if (!isWebLlmSupported()) {
623+ return;
624+ }
625+
626+ const prompt = await getSummaryPromptForNow(context, force);
627+
628+ if (!prompt) {
629+ return;
630+ }
631+
632+ const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
633+
634+ if (lastUsedIndex === null || lastUsedIndex === -1) {
635+ if (force) {
636+ toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');
637+ }
638+
639+ return null;
640+ }
641+
642+ const messages = [
643+ { role: 'system', content: prompt },
644+ { role: 'user', content: rawPrompt },
645+ ];
646+
647+ const params = {};
648+
649+ if (extension_settings.memory.overrideResponseLength > 0) {
650+ params.max_tokens = extension_settings.memory.overrideResponseLength;
651+ }
652+
653+ const summary = await generateWebLlmChatPrompt(messages, params);
654+ const newContext = getContext();
655+
656+ // something changed during summarization request
657+ if (newContext.groupId !== context.groupId ||
658+ newContext.chatId !== context.chatId ||
659+ (!newContext.groupId && (newContext.characterId !== context.characterId))) {
660+ console.log('Context changed, summary discarded');
661+ return;
662+ }
663+
664+ setMemoryContext(summary, true, lastUsedIndex);
665+ return summary;
666+}
667+
668+async function summarizeChatMain(context, force, skipWIAN) {
669+ const prompt = await getSummaryPromptForNow(context, force);
670+
671+ if (!prompt) {
550672 return;
551673 }
552674
@@ -634,7 +756,7 @@ async function getRawSummaryPrompt(context, prompt) {
634756 chat.pop(); // We always exclude the last message from the buffer
635757 const chatBuffer = [];
636758 const PADDING = 64;
637759 const PROMPT_SIZE = getMaxContextSizeawait getSourceContextSize(extension_settings.memory.overrideResponseLength);
638760 let latestUsedMessage = null;
639761
640762 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {
@@ -651,7 +773,7 @@ async function getRawSummaryPrompt(context, prompt) {
651773 const entry = `${message.name}:\n${message.mes}`;
652774 chatBuffer.push(entry);
653775
654776 const tokens = await getTokenCountAsynccountSourceTokens(getMemoryString(true), PADDING);
655777
656778 if (tokens > PROMPT_SIZE) {
657779 chatBuffer.pop();
@@ -680,7 +802,7 @@ async function summarizeChatExtras(context) {
680802 const reversedChat = chat.slice().reverse();
681803 reversedChat.shift();
682804 const memoryBuffer = [];
683805 const CONTEXT_SIZE = 1024 -await 64getSourceContextSize();
684806
685807 for (const message of reversedChat) {
686808 // we reached the point of latest memory
@@ -698,14 +820,14 @@ async function summarizeChatExtras(context) {
698820 memoryBuffer.push(entry);
699821
700822 // check if token limit was reached
701823 const tokens = getTextTokens(tokenizers.GPT2,await countSourceTokens(getMemoryString()).length;
702824 if (tokens >= CONTEXT_SIZE) {
703825 break;
704826 }
705827 }
706828
707829 const resultingString = getMemoryString();
708830 const resultingTokens = getTextTokens(tokenizers.GPT2,await countSourceTokens(resultingString).length;
709831
710832 if (!resultingString || resultingTokens < CONTEXT_SIZE) {
711833 console.debug('Not enough context to summarize');
@@ -890,7 +1012,7 @@ function setupListeners() {
8901012 $('#memory_prompt_words').off('click').on('input', onMemoryPromptWordsInput);
8911013 $('#memory_prompt_interval').off('click').on('input', onMemoryPromptIntervalInput);
8921014 $('#memory_prompt').off('click').on('input', onMemoryPromptInput);
8931015 $('#memory_force_summarize').off('click').on('click', () => forceSummarizeChat(false));
8941016 $('#memory_template').off('click').on('input', onMemoryTemplateInput);
8951017 $('#memory_depth').off('click').on('input', onMemoryDepthInput);
8961018 $('#memory_role').off('click').on('input', onMemoryRoleInput);
@@ -933,13 +1055,20 @@ jQuery(async function () {
9331055 name: 'summarize',
9341056 callback: summarizeCallback,
9351057 namedArgumentList: [
9361058 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']Object.values(summary_sources)),
9371059 SlashCommandNamedArgument.fromProps({
9381060 name: 'prompt',
9391061 description: 'prompt to use for summarization',
9401062 typeList: [ARGUMENT_TYPE.STRING],
9411063 defaultValue: '',
9421064 }),
1065+ SlashCommandNamedArgument.fromProps({
1066+ name: 'quiet',
1067+ description: 'suppress the toast message when summarizing the chat',
1068+ typeList: [ARGUMENT_TYPE.BOOLEAN],
1069+ defaultValue: 'false',
1070+ enumList: commonEnumProviders.boolean('trueFalse')(),
1071+ }),
9431072 ],
9441073 unnamedArgumentList: [
9451074 new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
public/scripts/extensions/memory/settings.html+4 -3
@@ -13,6 +13,7 @@
1313 <select id="summary_source">
1414 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
1515 <option value="extras">Extras API</option>
16+ <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
1617 </select><br>
1718
1819 <div class="flex-container justifyspacebetween alignitemscenter">
@@ -24,7 +25,7 @@
2425
2526 <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>
2627 <div class="memory_contents_controls">
2728 <div id="memory_force_summarize" data-summary-source="main,webllm" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="[title]ext_sum_force_tip">
2829 <i class="fa-solid fa-database"></i>
2930 <span data-i18n="ext_sum_force_text">Summarize now</span>
3031 </div>
@@ -58,7 +59,7 @@
5859 <span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>
5960 </label>
6061 </div>
6162 <div data-summary-source="main,webllm">
6263 <label for="memory_prompt" class="title_restorable">
6364 <span data-i18n="Summary Prompt">Summary Prompt</span>
6465 <div id="memory_prompt_restore" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">
@@ -74,7 +75,7 @@
7475 </label>
7576 <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />
7677 <label for="memory_max_messages_per_request">
7778 <span data-i18n="ext_sum_raw_max_msg">[Raw/WebLLM] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)
7879 <small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>
7980 </label>
8081 <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />
public/scripts/extensions/quick-reply/api/QuickReplyApi.js+94 -66
@@ -24,9 +24,17 @@ export class QuickReplyApi {
2424
2525
2626 /**
27+ * @param {QuickReply} qr
28+ * @returns {QuickReplySet}
29+ */
30+ getSetByQr(qr) {
31+ return QuickReplySet.list.find(it=>it.qrList.includes(qr));
32+ }
33+
34+ /**
2735 * Finds and returns an existing Quick Reply Set by its name.
2836 *
2937 * @param {Stringstring} name name of the quick reply set
3038 * @returns the quick reply set, or undefined if not found
3139 */
3240 getSetByName(name) {
@@ -36,13 +44,14 @@ export class QuickReplyApi {
3644 /**
3745 * Finds and returns an existing Quick Reply by its set's name and its label.
3846 *
3947 * @param {Stringstring} setName name of the quick reply set
4048 * @param {Stringstring|number} label label or numeric ID of the quick reply
4149 * @returns the quick reply, or undefined if not found
4250 */
4351 getQrByLabel(setName, label) {
4452 const set = this.getSetByName(setName);
4553 if (!set) return;
54+ if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label);
4655 return set.qrList.find(it=>it.label == label);
4756 }
4857
@@ -70,24 +79,25 @@ export class QuickReplyApi {
7079 /**
7180 * Executes an existing quick reply.
7281 *
7382 * @param {Stringstring} setName name of the existing quick reply set
7483 * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID
7584 * @param {Objectobject} [args] optional arguments
85+ * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
7686 */
7787 async executeQuickReply(setName, label, args = {}, options = {}) {
7888 const qr = this.getQrByLabel(setName, label);
7989 if (!qr) {
8090 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
8191 }
8292 return await qr.execute(args, false, false, options);
8393 }
8494
8595
8696 /**
8797 * Adds or removes a quick reply set to the list of globally active quick reply sets.
8898 *
8999 * @param {Stringstring} name the name of the set
90100 * @param {Booleanboolean} isVisible whether to show the set's buttons or not
91101 */
92102 toggleGlobalSet(name, isVisible = true) {
93103 const set = this.getSetByName(name);
@@ -104,8 +114,8 @@ export class QuickReplyApi {
104114 /**
105115 * Adds a quick reply set to the list of globally active quick reply sets.
106116 *
107117 * @param {Stringstring} name the name of the set
108118 * @param {Booleanboolean} isVisible whether to show the set's buttons or not
109119 */
110120 addGlobalSet(name, isVisible = true) {
111121 const set = this.getSetByName(name);
@@ -118,7 +128,7 @@ export class QuickReplyApi {
118128 /**
119129 * Removes a quick reply set from the list of globally active quick reply sets.
120130 *
121131 * @param {Stringstring} name the name of the set
122132 */
123133 removeGlobalSet(name) {
124134 const set = this.getSetByName(name);
@@ -132,8 +142,8 @@ export class QuickReplyApi {
132142 /**
133143 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
134144 *
135145 * @param {Stringstring} name the name of the set
136146 * @param {Booleanboolean} isVisible whether to show the set's buttons or not
137147 */
138148 toggleChatSet(name, isVisible = true) {
139149 if (!this.settings.chatConfig) return;
@@ -151,8 +161,8 @@ export class QuickReplyApi {
151161 /**
152162 * Adds a quick reply set to the list of the current chat's active quick reply sets.
153163 *
154164 * @param {Stringstring} name the name of the set
155165 * @param {Booleanboolean} isVisible whether to show the set's buttons or not
156166 */
157167 addChatSet(name, isVisible = true) {
158168 if (!this.settings.chatConfig) return;
@@ -166,7 +176,7 @@ export class QuickReplyApi {
166176 /**
167177 * Removes a quick reply set from the list of the current chat's active quick reply sets.
168178 *
169179 * @param {Stringstring} name the name of the set
170180 */
171181 removeChatSet(name) {
172182 if (!this.settings.chatConfig) return;
@@ -181,21 +191,26 @@ export class QuickReplyApi {
181191 /**
182192 * Creates a new quick reply in an existing quick reply set.
183193 *
184194 * @param {Stringstring} setName name of the quick reply set to insert the new quick reply into
185195 * @param {Stringstring} label label for the new quick reply (text on the button)
186196 * @param {Objectobject} [props]
187197 * @param {Stringstring} [props.messageicon] the message to be sent or slash commandicon to be executedshow byon the new quickQR replybutton
188198 * @param {Stringboolean} [props.titleshowLabel] the titlewhether /to tooltipshow tothe belabel showneven onwhen thean quickicon replyis buttonassigned
189199 * @param {Booleanstring} [props.isHiddenmessage] whetherthe message to hidebe sent or showslash command to be executed by the buttonnew quick reply
190200 * @param {Booleanstring} [props.executeOnStartuptitle] whetherthe title / tooltip to executebe shown on the quick reply when SillyTavern startsbutton
191201 * @param {Booleanboolean} [props.executeOnUserisHidden] whether to execute the quick reply after a userhide hasor sentshow athe messagebutton
192202 * @param {Booleanboolean} [props.executeOnAiexecuteOnStartup] whether to execute the quick reply after the AI has sentwhen aSillyTavern messagestarts
193203 * @param {Booleanboolean} [props.executeOnChatChangeexecuteOnUser] whether to execute the quick reply whenafter a newuser chathas issent loadeda message
194204 * @param {Booleanboolean} [props.executeOnGroupMemberDraftexecuteOnAi] whether to execute the quick reply whenafter athe groupAI memberhas issent selecteda message
195205 * @param {Stringboolean} [props.automationIdexecuteOnChatChange] whenwhether notto empty,execute the quick reply will be executed when the WI with the givena automationnew IDchat is activatedloaded
206+ * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
207+ * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
208+ * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
196209 * @returns {QuickReply} the new quick reply
197210 */
198211 createQuickReply(setName, label, {
212+ icon,
213+ showLabel,
199214 message,
200215 title,
201216 isHidden,
@@ -204,6 +219,7 @@ export class QuickReplyApi {
204219 executeOnAi,
205220 executeOnChatChange,
206221 executeOnGroupMemberDraft,
222+ executeOnNewChat,
207223 automationId,
208224 } = {}) {
209225 const set = this.getSetByName(setName);
@@ -212,6 +228,8 @@ export class QuickReplyApi {
212228 }
213229 const qr = set.addQuickReply();
214230 qr.label = label ?? '';
231+ qr.icon = icon ?? '';
232+ qr.showLabel = showLabel ?? false;
215233 qr.message = message ?? '';
216234 qr.title = title ?? '';
217235 qr.isHidden = isHidden ?? false;
@@ -220,6 +238,7 @@ export class QuickReplyApi {
220238 qr.executeOnAi = executeOnAi ?? false;
221239 qr.executeOnChatChange = executeOnChatChange ?? false;
222240 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
241+ qr.executeOnNewChat = executeOnNewChat ?? false;
223242 qr.automationId = automationId ?? '';
224243 qr.onUpdate();
225244 return qr;
@@ -228,22 +247,27 @@ export class QuickReplyApi {
228247 /**
229248 * Updates an existing quick reply.
230249 *
231250 * @param {Stringstring} setName name of the existing quick reply set
232251 * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID
233252 * @param {Objectobject} [props]
234253 * @param {Stringstring} [props.newLabelicon] new label forthe quickicon replyto (textshow on the QR button)
235254 * @param {Stringboolean} [props.messageshowLabel] the messagewhether to be sent or slashshow commandthe tolabel beeven executedwhen byan theicon quickis replyassigned
236255 * @param {Stringstring} [props.titlenewLabel] the titlenew /label tooltipfor toquick bereply shown(text on the quick reply button)
237256 * @param {Booleanstring} [props.isHiddenmessage] whetherthe message to hidebe sent or showslash command to be executed by the buttonquick reply
238257 * @param {Booleanstring} [props.executeOnStartuptitle] whetherthe title / tooltip to executebe shown on the quick reply when SillyTavern startsbutton
239258 * @param {Booleanboolean} [props.executeOnUserisHidden] whether to execute the quick reply after a userhide hasor sentshow athe messagebutton
240259 * @param {Booleanboolean} [props.executeOnAiexecuteOnStartup] whether to execute the quick reply after the AI has sentwhen aSillyTavern messagestarts
241260 * @param {Booleanboolean} [props.executeOnChatChangeexecuteOnUser] whether to execute the quick reply whenafter a newuser chathas issent loadeda message
242261 * @param {Booleanboolean} [props.executeOnGroupMemberDraftexecuteOnAi] whether to execute the quick reply whenafter athe groupAI memberhas issent selecteda message
243262 * @param {Stringboolean} [props.automationIdexecuteOnChatChange] whenwhether notto empty,execute the quick reply will be executed when the WI with the givena automationnew IDchat is activatedloaded
263+ * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
264+ * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
265+ * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
244266 * @returns {QuickReply} the altered quick reply
245267 */
246268 updateQuickReply(setName, label, {
269+ icon,
270+ showLabel,
247271 newLabel,
248272 message,
249273 title,
@@ -253,12 +277,15 @@ export class QuickReplyApi {
253277 executeOnAi,
254278 executeOnChatChange,
255279 executeOnGroupMemberDraft,
280+ executeOnNewChat,
256281 automationId,
257282 } = {}) {
258283 const qr = this.getQrByLabel(setName, label);
259284 if (!qr) {
260285 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
261286 }
287+ qr.updateIcon(icon ?? qr.icon);
288+ qr.updateShowLabel(showLabel ?? qr.showLabel);
262289 qr.updateLabel(newLabel ?? qr.label);
263290 qr.updateMessage(message ?? qr.message);
264291 qr.updateTitle(title ?? qr.title);
@@ -268,6 +295,7 @@ export class QuickReplyApi {
268295 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
269296 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
270297 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
298+ qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
271299 qr.automationId = automationId ?? qr.automationId;
272300 qr.onUpdate();
273301 return qr;
@@ -276,8 +304,8 @@ export class QuickReplyApi {
276304 /**
277305 * Deletes an existing quick reply.
278306 *
279307 * @param {Stringstring} setName name of the existing quick reply set
280308 * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID
281309 */
282310 deleteQuickReply(setName, label) {
283311 const qr = this.getQrByLabel(setName, label);
@@ -291,10 +319,10 @@ export class QuickReplyApi {
291319 /**
292320 * Adds an existing quick reply set as a context menu to an existing quick reply.
293321 *
294322 * @param {Stringstring} setName name of the existing quick reply set containing the quick reply
295323 * @param {Stringstring|number} label label of the existing quick reply or its numeric ID
296324 * @param {Stringstring} contextSetName name of the existing quick reply set to be used as a context menu
297325 * @param {Booleanboolean} isChained whether or not to chain the context menu quick replies
298326 */
299327 createContextItem(setName, label, contextSetName, isChained = false) {
300328 const qr = this.getQrByLabel(setName, label);
@@ -314,9 +342,9 @@ export class QuickReplyApi {
314342 /**
315343 * Removes a quick reply set from a quick reply's context menu.
316344 *
317345 * @param {Stringstring} setName name of the existing quick reply set containing the quick reply
318346 * @param {Stringstring|number} label label of the existing quick reply or its numeric ID
319347 * @param {Stringstring} contextSetName name of the existing quick reply set to be used as a context menu
320348 */
321349 deleteContextItem(setName, label, contextSetName) {
322350 const qr = this.getQrByLabel(setName, label);
@@ -333,8 +361,8 @@ export class QuickReplyApi {
333361 /**
334362 * Removes all entries from a quick reply's context menu.
335363 *
336364 * @param {Stringstring} setName name of the existing quick reply set containing the quick reply
337365 * @param {Stringstring|number} label label of the existing quick reply or its numeric ID
338366 */
339367 clearContextMenu(setName, label) {
340368 const qr = this.getQrByLabel(setName, label);
@@ -348,11 +376,11 @@ export class QuickReplyApi {
348376 /**
349377 * Create a new quick reply set.
350378 *
351379 * @param {Stringstring} name name of the new quick reply set
352380 * @param {Objectobject} [props]
353381 * @param {Booleanboolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
354382 * @param {Booleanboolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
355383 * @param {Booleanboolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
356384 * @returns {Promise<QuickReplySet>} the new quick reply set
357385 */
358386 async createSet(name, {
@@ -384,11 +412,11 @@ export class QuickReplyApi {
384412 /**
385413 * Update an existing quick reply set.
386414 *
387415 * @param {Stringstring} name name of the existing quick reply set
388416 * @param {Objectobject} [props]
389417 * @param {Booleanboolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
390418 * @param {Booleanboolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
391419 * @param {Booleanboolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
392420 * @returns {Promise<QuickReplySet>} the altered quick reply set
393421 */
394422 async updateSet(name, {
@@ -411,7 +439,7 @@ export class QuickReplyApi {
411439 /**
412440 * Delete an existing quick reply set.
413441 *
414442 * @param {Stringstring} name name of the existing quick reply set
415443 */
416444 async deleteSet(name) {
417445 const set = this.getSetByName(name);
@@ -451,7 +479,7 @@ export class QuickReplyApi {
451479 /**
452480 * Gets a list of all quick replies in the quick reply set.
453481 *
454482 * @param {Stringstring} setName name of the existing quick reply set
455483 * @returns array with the labels of this set's quick replies
456484 */
457485 listQuickReplies(setName) {
public/scripts/extensions/quick-reply/html/qrEditor.html+38 -8
@@ -2,10 +2,23 @@
22 <div id="qr--main">
33 <h3 data-i18n="Labels and Message">Labels and Message</h3>
44 <div class="qr--labels">
5- <label>
5+ <label class="qr--fit">
66 <span class="qr--labelText" data-i18n="Label">LabelIcon</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>
89 </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>
922 <label>
1023 <span class="qr--labelText" data-i18n="Title">Title</span>
1124 <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>
@@ -33,6 +46,8 @@
3346 <input type="checkbox" id="qr--modal-syntax">
3447 <span>Syntax highlight</span>
3548 </label>
49+ <small>Ctrl+Alt+Click (or F9) to set / remove breakpoints</small>
50+ <small>Ctrl+<span id="qr--modal-commentKey"></span> to toggle block comments</small>
3651 </div>
3752 <div id="qr--modal-messageHolder">
3853 <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
@@ -43,6 +58,10 @@
4358
4459
4560
61+ <div id="qr--resizeHandle"></div>
62+
63+
64+
4665 <div id="qr--qrOptions">
4766 <h3 data-i18n="Context Menu">Context Menu</h3>
4867 <div id="qr--ctxEditor">
@@ -64,7 +83,7 @@
6483
6584
6685 <h3 data-i18n="Auto-Execute">Auto-Execute</h3>
6786 <div id="qr--autoExec" class="flex-container flexFlowColumn">
6887 <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
6988 <input type="checkbox" id="qr--preventAutoExecute" >
7089 <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>
@@ -90,6 +109,10 @@
90109 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>
91110 </label>
92111 <label class="checkbox_label">
112+ <input type="checkbox" id="qr--executeOnNewChat">
113+ <span><i class="fa-solid fa-fw fa-comments"></i><span data-i18n="Execute on new chat">Execute on new chat</span></span>
114+ </label>
115+ <label class="checkbox_label">
93116 <input type="checkbox" id="qr--executeOnGroupMemberDraft">
94117 <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>
95118 </label>
@@ -117,11 +140,18 @@
117140 </div>
118141 </div>
119142 <div id="qr--modal-executeProgress"></div>
120- <label class="checkbox_label">
121- <input type="checkbox" id="qr--modal-executeHide">
122- <span title="Hide editor while executing"> Hide editor while executing</span>
123- </label>
124143 <div id="qr--modal-executeErrors"></div>
125144 <div id="qr--modal-executeResult"></div>
145+
146+ <div id="qr--modal-debugButtons">
147+ <div title="Resume" id="qr--modal-resume" class="qr--modal-debugButton menu_button"></div>
148+ <div title="Step Over" id="qr--modal-step" class="qr--modal-debugButton menu_button"></div>
149+ <div title="Step Into" id="qr--modal-stepInto" class="qr--modal-debugButton menu_button"></div>
150+ <div title="Step Out" id="qr--modal-stepOut" class="qr--modal-debugButton menu_button"></div>
151+ <div title="Minimize" id="qr--modal-minimize" class="qr--modal-debugButton menu_button fa-solid fa-minimize"></div>
152+ <div title="Maximize" id="qr--modal-maximize" class="qr--modal-debugButton menu_button fa-solid fa-maximize"></div>
153+ </div>
154+ <textarea rows="1" id="qr--modal-send_textarea" placeholder="Chat input for use with {{input}}" title="Chat input for use with {{input}}"></textarea>
155+ <div id="qr--modal-debugState"></div>
126156 </div>
127157</div>
public/scripts/extensions/quick-reply/html/settings.html+13 -0
@@ -11,6 +11,9 @@
1111 <label class="flex-container">
1212 <input type="checkbox" id="qr--isCombined"><span data-i18n="Combine Quick Replies">Combine Quick Replies</span>
1313 </label>
14+ <label class="flex-container">
15+ <input type="checkbox" id="qr--showPopoutButton"><span data-i18n="Show Popout Button">Show Popout Button</span>
16+ </label>
1417
1518 <hr>
1619
@@ -60,10 +63,20 @@
6063 <label class="flex-container" id="qr--injectInputContainer">
6164 <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>
6265 </label>
66+ <div class="flex-container alignItemsCenter">
67+ <toolcool-color-picker id="qr--color"></toolcool-color-picker>
68+ <div class="menu_button" id="qr--colorClear">Clear</div>
69+ <span data-i18n="Color">Color</span>
70+ </div>
71+ <label class="flex-container" id="qr--onlyBorderColorContainer">
72+ <input type="checkbox" id="qr--onlyBorderColor"> <span data-i18n="Only apply color as accent">Only apply color as accent</span>
73+ </label>
6374 </div>
6475 <div id="qr--set-qrList" class="qr--qrList"></div>
6576 <div class="qr--set-qrListActions">
6677 <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
78+ <div class="qr--paste menu_button menu_button_icon fa-solid fa-paste" id="qr--set-paste" title="Paste quick reply from clipboard"></div>
79+ <div class="qr--import menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-importQr" title="Import quick reply from file"></div>
6780 </div>
6881 </div>
6982 </div>
public/scripts/extensions/quick-reply/index.js+8 -2
@@ -105,6 +105,7 @@ const loadSets = async () => {
105105 qr.executeOnAi = slot.autoExecute_botMessage ?? false;
106106 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
107107 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
108+ qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
108109 qr.automationId = slot.automationId ?? '';
109110 qr.contextList = (slot.contextMenu ?? []).map(it=>({
110111 set: it.preset,
@@ -176,7 +177,7 @@ const init = async () => {
176177 buttons.show();
177178 settings.onSave = ()=>buttons.refresh();
178179
179180 window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
180181 let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
181182 .map(it=>it.set.qrList)
182183 .flat()
@@ -191,7 +192,7 @@ const init = async () => {
191192 }
192193 }
193194 if (qr && qr.onExecute) {
194195 return await qr.execute(args, false, true, options);
195196 } else {
196197 throw new Error(`No Quick Reply found for "${name}".`);
197198 }
@@ -260,3 +261,8 @@ const onWIActivation = async (entries) => {
260261 await autoExec.handleWIActivation(entries);
261262};
262263eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));
264+
265+const onNewChat = async () => {
266+ await autoExec.handleNewChat();
267+};
268+eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
public/scripts/extensions/quick-reply/lib/morphdom-esm.js+769 -0
@@ -0,0 +1,769 @@
1+var DOCUMENT_FRAGMENT_NODE = 11;
2+
3+function morphAttrs(fromNode, toNode) {
4+ var toNodeAttrs = toNode.attributes;
5+ var attr;
6+ var attrName;
7+ var attrNamespaceURI;
8+ var attrValue;
9+ var fromValue;
10+
11+ // document-fragments dont have attributes so lets not do anything
12+ if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
13+ return;
14+ }
15+
16+ // update attributes on original DOM element
17+ for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
18+ attr = toNodeAttrs[i];
19+ attrName = attr.name;
20+ attrNamespaceURI = attr.namespaceURI;
21+ attrValue = attr.value;
22+
23+ if (attrNamespaceURI) {
24+ attrName = attr.localName || attrName;
25+ fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
26+
27+ if (fromValue !== attrValue) {
28+ if (attr.prefix === 'xmlns'){
29+ attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
30+ }
31+ fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
32+ }
33+ } else {
34+ fromValue = fromNode.getAttribute(attrName);
35+
36+ if (fromValue !== attrValue) {
37+ fromNode.setAttribute(attrName, attrValue);
38+ }
39+ }
40+ }
41+
42+ // Remove any extra attributes found on the original DOM element that
43+ // weren't found on the target element.
44+ var fromNodeAttrs = fromNode.attributes;
45+
46+ for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
47+ attr = fromNodeAttrs[d];
48+ attrName = attr.name;
49+ attrNamespaceURI = attr.namespaceURI;
50+
51+ if (attrNamespaceURI) {
52+ attrName = attr.localName || attrName;
53+
54+ if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
55+ fromNode.removeAttributeNS(attrNamespaceURI, attrName);
56+ }
57+ } else {
58+ if (!toNode.hasAttribute(attrName)) {
59+ fromNode.removeAttribute(attrName);
60+ }
61+ }
62+ }
63+}
64+
65+var range; // Create a range object for efficently rendering strings to elements.
66+var NS_XHTML = 'http://www.w3.org/1999/xhtml';
67+
68+var doc = typeof document === 'undefined' ? undefined : document;
69+var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
70+var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
71+
72+function createFragmentFromTemplate(str) {
73+ var template = doc.createElement('template');
74+ template.innerHTML = str;
75+ return template.content.childNodes[0];
76+}
77+
78+function createFragmentFromRange(str) {
79+ if (!range) {
80+ range = doc.createRange();
81+ range.selectNode(doc.body);
82+ }
83+
84+ var fragment = range.createContextualFragment(str);
85+ return fragment.childNodes[0];
86+}
87+
88+function createFragmentFromWrap(str) {
89+ var fragment = doc.createElement('body');
90+ fragment.innerHTML = str;
91+ return fragment.childNodes[0];
92+}
93+
94+/**
95+ * This is about the same
96+ * var html = new DOMParser().parseFromString(str, 'text/html');
97+ * return html.body.firstChild;
98+ *
99+ * @method toElement
100+ * @param {String} str
101+ */
102+function toElement(str) {
103+ str = str.trim();
104+ if (HAS_TEMPLATE_SUPPORT) {
105+ // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
106+ // createContextualFragment doesn't support
107+ // <template> support not available in IE
108+ return createFragmentFromTemplate(str);
109+ } else if (HAS_RANGE_SUPPORT) {
110+ return createFragmentFromRange(str);
111+ }
112+
113+ return createFragmentFromWrap(str);
114+}
115+
116+/**
117+ * Returns true if two node's names are the same.
118+ *
119+ * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
120+ * nodeName and different namespace URIs.
121+ *
122+ * @param {Element} a
123+ * @param {Element} b The target element
124+ * @return {boolean}
125+ */
126+function compareNodeNames(fromEl, toEl) {
127+ var fromNodeName = fromEl.nodeName;
128+ var toNodeName = toEl.nodeName;
129+ var fromCodeStart, toCodeStart;
130+
131+ if (fromNodeName === toNodeName) {
132+ return true;
133+ }
134+
135+ fromCodeStart = fromNodeName.charCodeAt(0);
136+ toCodeStart = toNodeName.charCodeAt(0);
137+
138+ // If the target element is a virtual DOM node or SVG node then we may
139+ // need to normalize the tag name before comparing. Normal HTML elements that are
140+ // in the "http://www.w3.org/1999/xhtml"
141+ // are converted to upper case
142+ if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
143+ return fromNodeName === toNodeName.toUpperCase();
144+ } else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
145+ return toNodeName === fromNodeName.toUpperCase();
146+ } else {
147+ return false;
148+ }
149+}
150+
151+/**
152+ * Create an element, optionally with a known namespace URI.
153+ *
154+ * @param {string} name the element name, e.g. 'div' or 'svg'
155+ * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
156+ * its `xmlns` attribute or its inferred namespace.
157+ *
158+ * @return {Element}
159+ */
160+function createElementNS(name, namespaceURI) {
161+ return !namespaceURI || namespaceURI === NS_XHTML ?
162+ doc.createElement(name) :
163+ doc.createElementNS(namespaceURI, name);
164+}
165+
166+/**
167+ * Copies the children of one DOM element to another DOM element
168+ */
169+function moveChildren(fromEl, toEl) {
170+ var curChild = fromEl.firstChild;
171+ while (curChild) {
172+ var nextChild = curChild.nextSibling;
173+ toEl.appendChild(curChild);
174+ curChild = nextChild;
175+ }
176+ return toEl;
177+}
178+
179+function syncBooleanAttrProp(fromEl, toEl, name) {
180+ if (fromEl[name] !== toEl[name]) {
181+ fromEl[name] = toEl[name];
182+ if (fromEl[name]) {
183+ fromEl.setAttribute(name, '');
184+ } else {
185+ fromEl.removeAttribute(name);
186+ }
187+ }
188+}
189+
190+var specialElHandlers = {
191+ OPTION: function(fromEl, toEl) {
192+ var parentNode = fromEl.parentNode;
193+ if (parentNode) {
194+ var parentName = parentNode.nodeName.toUpperCase();
195+ if (parentName === 'OPTGROUP') {
196+ parentNode = parentNode.parentNode;
197+ parentName = parentNode && parentNode.nodeName.toUpperCase();
198+ }
199+ if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
200+ if (fromEl.hasAttribute('selected') && !toEl.selected) {
201+ // Workaround for MS Edge bug where the 'selected' attribute can only be
202+ // removed if set to a non-empty value:
203+ // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
204+ fromEl.setAttribute('selected', 'selected');
205+ fromEl.removeAttribute('selected');
206+ }
207+ // We have to reset select element's selectedIndex to -1, otherwise setting
208+ // fromEl.selected using the syncBooleanAttrProp below has no effect.
209+ // The correct selectedIndex will be set in the SELECT special handler below.
210+ parentNode.selectedIndex = -1;
211+ }
212+ }
213+ syncBooleanAttrProp(fromEl, toEl, 'selected');
214+ },
215+ /**
216+ * The "value" attribute is special for the <input> element since it sets
217+ * the initial value. Changing the "value" attribute without changing the
218+ * "value" property will have no effect since it is only used to the set the
219+ * initial value. Similar for the "checked" attribute, and "disabled".
220+ */
221+ INPUT: function(fromEl, toEl) {
222+ syncBooleanAttrProp(fromEl, toEl, 'checked');
223+ syncBooleanAttrProp(fromEl, toEl, 'disabled');
224+
225+ if (fromEl.value !== toEl.value) {
226+ fromEl.value = toEl.value;
227+ }
228+
229+ if (!toEl.hasAttribute('value')) {
230+ fromEl.removeAttribute('value');
231+ }
232+ },
233+
234+ TEXTAREA: function(fromEl, toEl) {
235+ var newValue = toEl.value;
236+ if (fromEl.value !== newValue) {
237+ fromEl.value = newValue;
238+ }
239+
240+ var firstChild = fromEl.firstChild;
241+ if (firstChild) {
242+ // Needed for IE. Apparently IE sets the placeholder as the
243+ // node value and vise versa. This ignores an empty update.
244+ var oldValue = firstChild.nodeValue;
245+
246+ if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
247+ return;
248+ }
249+
250+ firstChild.nodeValue = newValue;
251+ }
252+ },
253+ SELECT: function(fromEl, toEl) {
254+ if (!toEl.hasAttribute('multiple')) {
255+ var selectedIndex = -1;
256+ var i = 0;
257+ // We have to loop through children of fromEl, not toEl since nodes can be moved
258+ // from toEl to fromEl directly when morphing.
259+ // At the time this special handler is invoked, all children have already been morphed
260+ // and appended to / removed from fromEl, so using fromEl here is safe and correct.
261+ var curChild = fromEl.firstChild;
262+ var optgroup;
263+ var nodeName;
264+ while(curChild) {
265+ nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
266+ if (nodeName === 'OPTGROUP') {
267+ optgroup = curChild;
268+ curChild = optgroup.firstChild;
269+ } else {
270+ if (nodeName === 'OPTION') {
271+ if (curChild.hasAttribute('selected')) {
272+ selectedIndex = i;
273+ break;
274+ }
275+ i++;
276+ }
277+ curChild = curChild.nextSibling;
278+ if (!curChild && optgroup) {
279+ curChild = optgroup.nextSibling;
280+ optgroup = null;
281+ }
282+ }
283+ }
284+
285+ fromEl.selectedIndex = selectedIndex;
286+ }
287+ }
288+};
289+
290+var ELEMENT_NODE = 1;
291+var DOCUMENT_FRAGMENT_NODE$1 = 11;
292+var TEXT_NODE = 3;
293+var COMMENT_NODE = 8;
294+
295+function noop() {}
296+
297+function defaultGetNodeKey(node) {
298+ if (node) {
299+ return (node.getAttribute && node.getAttribute('id')) || node.id;
300+ }
301+}
302+
303+function morphdomFactory(morphAttrs) {
304+
305+ return function morphdom(fromNode, toNode, options) {
306+ if (!options) {
307+ options = {};
308+ }
309+
310+ if (typeof toNode === 'string') {
311+ if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') {
312+ var toNodeHtml = toNode;
313+ toNode = doc.createElement('html');
314+ toNode.innerHTML = toNodeHtml;
315+ } else {
316+ toNode = toElement(toNode);
317+ }
318+ } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
319+ toNode = toNode.firstElementChild;
320+ }
321+
322+ var getNodeKey = options.getNodeKey || defaultGetNodeKey;
323+ var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
324+ var onNodeAdded = options.onNodeAdded || noop;
325+ var onBeforeElUpdated = options.onBeforeElUpdated || noop;
326+ var onElUpdated = options.onElUpdated || noop;
327+ var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
328+ var onNodeDiscarded = options.onNodeDiscarded || noop;
329+ var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
330+ var skipFromChildren = options.skipFromChildren || noop;
331+ var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); };
332+ var childrenOnly = options.childrenOnly === true;
333+
334+ // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
335+ var fromNodesLookup = Object.create(null);
336+ var keyedRemovalList = [];
337+
338+ function addKeyedRemoval(key) {
339+ keyedRemovalList.push(key);
340+ }
341+
342+ function walkDiscardedChildNodes(node, skipKeyedNodes) {
343+ if (node.nodeType === ELEMENT_NODE) {
344+ var curChild = node.firstChild;
345+ while (curChild) {
346+
347+ var key = undefined;
348+
349+ if (skipKeyedNodes && (key = getNodeKey(curChild))) {
350+ // If we are skipping keyed nodes then we add the key
351+ // to a list so that it can be handled at the very end.
352+ addKeyedRemoval(key);
353+ } else {
354+ // Only report the node as discarded if it is not keyed. We do this because
355+ // at the end we loop through all keyed elements that were unmatched
356+ // and then discard them in one final pass.
357+ onNodeDiscarded(curChild);
358+ if (curChild.firstChild) {
359+ walkDiscardedChildNodes(curChild, skipKeyedNodes);
360+ }
361+ }
362+
363+ curChild = curChild.nextSibling;
364+ }
365+ }
366+ }
367+
368+ /**
369+ * Removes a DOM node out of the original DOM
370+ *
371+ * @param {Node} node The node to remove
372+ * @param {Node} parentNode The nodes parent
373+ * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
374+ * @return {undefined}
375+ */
376+ function removeNode(node, parentNode, skipKeyedNodes) {
377+ if (onBeforeNodeDiscarded(node) === false) {
378+ return;
379+ }
380+
381+ if (parentNode) {
382+ parentNode.removeChild(node);
383+ }
384+
385+ onNodeDiscarded(node);
386+ walkDiscardedChildNodes(node, skipKeyedNodes);
387+ }
388+
389+ // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
390+ // function indexTree(root) {
391+ // var treeWalker = document.createTreeWalker(
392+ // root,
393+ // NodeFilter.SHOW_ELEMENT);
394+ //
395+ // var el;
396+ // while((el = treeWalker.nextNode())) {
397+ // var key = getNodeKey(el);
398+ // if (key) {
399+ // fromNodesLookup[key] = el;
400+ // }
401+ // }
402+ // }
403+
404+ // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
405+ //
406+ // function indexTree(node) {
407+ // var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
408+ // var el;
409+ // while((el = nodeIterator.nextNode())) {
410+ // var key = getNodeKey(el);
411+ // if (key) {
412+ // fromNodesLookup[key] = el;
413+ // }
414+ // }
415+ // }
416+
417+ function indexTree(node) {
418+ if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
419+ var curChild = node.firstChild;
420+ while (curChild) {
421+ var key = getNodeKey(curChild);
422+ if (key) {
423+ fromNodesLookup[key] = curChild;
424+ }
425+
426+ // Walk recursively
427+ indexTree(curChild);
428+
429+ curChild = curChild.nextSibling;
430+ }
431+ }
432+ }
433+
434+ indexTree(fromNode);
435+
436+ function handleNodeAdded(el) {
437+ onNodeAdded(el);
438+
439+ var curChild = el.firstChild;
440+ while (curChild) {
441+ var nextSibling = curChild.nextSibling;
442+
443+ var key = getNodeKey(curChild);
444+ if (key) {
445+ var unmatchedFromEl = fromNodesLookup[key];
446+ // if we find a duplicate #id node in cache, replace `el` with cache value
447+ // and morph it to the child node.
448+ if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
449+ curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
450+ morphEl(unmatchedFromEl, curChild);
451+ } else {
452+ handleNodeAdded(curChild);
453+ }
454+ } else {
455+ // recursively call for curChild and it's children to see if we find something in
456+ // fromNodesLookup
457+ handleNodeAdded(curChild);
458+ }
459+
460+ curChild = nextSibling;
461+ }
462+ }
463+
464+ function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
465+ // We have processed all of the "to nodes". If curFromNodeChild is
466+ // non-null then we still have some from nodes left over that need
467+ // to be removed
468+ while (curFromNodeChild) {
469+ var fromNextSibling = curFromNodeChild.nextSibling;
470+ if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
471+ // Since the node is keyed it might be matched up later so we defer
472+ // the actual removal to later
473+ addKeyedRemoval(curFromNodeKey);
474+ } else {
475+ // NOTE: we skip nested keyed nodes from being removed since there is
476+ // still a chance they will be matched up later
477+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
478+ }
479+ curFromNodeChild = fromNextSibling;
480+ }
481+ }
482+
483+ function morphEl(fromEl, toEl, childrenOnly) {
484+ var toElKey = getNodeKey(toEl);
485+
486+ if (toElKey) {
487+ // If an element with an ID is being morphed then it will be in the final
488+ // DOM so clear it out of the saved elements collection
489+ delete fromNodesLookup[toElKey];
490+ }
491+
492+ if (!childrenOnly) {
493+ // optional
494+ var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);
495+ if (beforeUpdateResult === false) {
496+ return;
497+ } else if (beforeUpdateResult instanceof HTMLElement) {
498+ fromEl = beforeUpdateResult;
499+ // reindex the new fromEl in case it's not in the same
500+ // tree as the original fromEl
501+ // (Phoenix LiveView sometimes returns a cloned tree,
502+ // but keyed lookups would still point to the original tree)
503+ indexTree(fromEl);
504+ }
505+
506+ // update attributes on original DOM element first
507+ morphAttrs(fromEl, toEl);
508+ // optional
509+ onElUpdated(fromEl);
510+
511+ if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
512+ return;
513+ }
514+ }
515+
516+ if (fromEl.nodeName !== 'TEXTAREA') {
517+ morphChildren(fromEl, toEl);
518+ } else {
519+ specialElHandlers.TEXTAREA(fromEl, toEl);
520+ }
521+ }
522+
523+ function morphChildren(fromEl, toEl) {
524+ var skipFrom = skipFromChildren(fromEl, toEl);
525+ var curToNodeChild = toEl.firstChild;
526+ var curFromNodeChild = fromEl.firstChild;
527+ var curToNodeKey;
528+ var curFromNodeKey;
529+
530+ var fromNextSibling;
531+ var toNextSibling;
532+ var matchingFromEl;
533+
534+ // walk the children
535+ outer: while (curToNodeChild) {
536+ toNextSibling = curToNodeChild.nextSibling;
537+ curToNodeKey = getNodeKey(curToNodeChild);
538+
539+ // walk the fromNode children all the way through
540+ while (!skipFrom && curFromNodeChild) {
541+ fromNextSibling = curFromNodeChild.nextSibling;
542+
543+ if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
544+ curToNodeChild = toNextSibling;
545+ curFromNodeChild = fromNextSibling;
546+ continue outer;
547+ }
548+
549+ curFromNodeKey = getNodeKey(curFromNodeChild);
550+
551+ var curFromNodeType = curFromNodeChild.nodeType;
552+
553+ // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
554+ var isCompatible = undefined;
555+
556+ if (curFromNodeType === curToNodeChild.nodeType) {
557+ if (curFromNodeType === ELEMENT_NODE) {
558+ // Both nodes being compared are Element nodes
559+
560+ if (curToNodeKey) {
561+ // The target node has a key so we want to match it up with the correct element
562+ // in the original DOM tree
563+ if (curToNodeKey !== curFromNodeKey) {
564+ // The current element in the original DOM tree does not have a matching key so
565+ // let's check our lookup to see if there is a matching element in the original
566+ // DOM tree
567+ if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
568+ if (fromNextSibling === matchingFromEl) {
569+ // Special case for single element removals. To avoid removing the original
570+ // DOM node out of the tree (since that can break CSS transitions, etc.),
571+ // we will instead discard the current node and wait until the next
572+ // iteration to properly match up the keyed target element with its matching
573+ // element in the original tree
574+ isCompatible = false;
575+ } else {
576+ // We found a matching keyed element somewhere in the original DOM tree.
577+ // Let's move the original DOM node into the current position and morph
578+ // it.
579+
580+ // NOTE: We use insertBefore instead of replaceChild because we want to go through
581+ // the `removeNode()` function for the node that is being discarded so that
582+ // all lifecycle hooks are correctly invoked
583+ fromEl.insertBefore(matchingFromEl, curFromNodeChild);
584+
585+ // fromNextSibling = curFromNodeChild.nextSibling;
586+
587+ if (curFromNodeKey) {
588+ // Since the node is keyed it might be matched up later so we defer
589+ // the actual removal to later
590+ addKeyedRemoval(curFromNodeKey);
591+ } else {
592+ // NOTE: we skip nested keyed nodes from being removed since there is
593+ // still a chance they will be matched up later
594+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
595+ }
596+
597+ curFromNodeChild = matchingFromEl;
598+ curFromNodeKey = getNodeKey(curFromNodeChild);
599+ }
600+ } else {
601+ // The nodes are not compatible since the "to" node has a key and there
602+ // is no matching keyed node in the source tree
603+ isCompatible = false;
604+ }
605+ }
606+ } else if (curFromNodeKey) {
607+ // The original has a key
608+ isCompatible = false;
609+ }
610+
611+ isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
612+ if (isCompatible) {
613+ // We found compatible DOM elements so transform
614+ // the current "from" node to match the current
615+ // target DOM node.
616+ // MORPH
617+ morphEl(curFromNodeChild, curToNodeChild);
618+ }
619+
620+ } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
621+ // Both nodes being compared are Text or Comment nodes
622+ isCompatible = true;
623+ // Simply update nodeValue on the original node to
624+ // change the text value
625+ if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
626+ curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
627+ }
628+
629+ }
630+ }
631+
632+ if (isCompatible) {
633+ // Advance both the "to" child and the "from" child since we found a match
634+ // Nothing else to do as we already recursively called morphChildren above
635+ curToNodeChild = toNextSibling;
636+ curFromNodeChild = fromNextSibling;
637+ continue outer;
638+ }
639+
640+ // No compatible match so remove the old node from the DOM and continue trying to find a
641+ // match in the original DOM. However, we only do this if the from node is not keyed
642+ // since it is possible that a keyed node might match up with a node somewhere else in the
643+ // target tree and we don't want to discard it just yet since it still might find a
644+ // home in the final DOM tree. After everything is done we will remove any keyed nodes
645+ // that didn't find a home
646+ if (curFromNodeKey) {
647+ // Since the node is keyed it might be matched up later so we defer
648+ // the actual removal to later
649+ addKeyedRemoval(curFromNodeKey);
650+ } else {
651+ // NOTE: we skip nested keyed nodes from being removed since there is
652+ // still a chance they will be matched up later
653+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
654+ }
655+
656+ curFromNodeChild = fromNextSibling;
657+ } // END: while(curFromNodeChild) {}
658+
659+ // If we got this far then we did not find a candidate match for
660+ // our "to node" and we exhausted all of the children "from"
661+ // nodes. Therefore, we will just append the current "to" node
662+ // to the end
663+ if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
664+ // MORPH
665+ if(!skipFrom){ addChild(fromEl, matchingFromEl); }
666+ morphEl(matchingFromEl, curToNodeChild);
667+ } else {
668+ var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
669+ if (onBeforeNodeAddedResult !== false) {
670+ if (onBeforeNodeAddedResult) {
671+ curToNodeChild = onBeforeNodeAddedResult;
672+ }
673+
674+ if (curToNodeChild.actualize) {
675+ curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
676+ }
677+ addChild(fromEl, curToNodeChild);
678+ handleNodeAdded(curToNodeChild);
679+ }
680+ }
681+
682+ curToNodeChild = toNextSibling;
683+ curFromNodeChild = fromNextSibling;
684+ }
685+
686+ cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
687+
688+ var specialElHandler = specialElHandlers[fromEl.nodeName];
689+ if (specialElHandler) {
690+ specialElHandler(fromEl, toEl);
691+ }
692+ } // END: morphChildren(...)
693+
694+ var morphedNode = fromNode;
695+ var morphedNodeType = morphedNode.nodeType;
696+ var toNodeType = toNode.nodeType;
697+
698+ if (!childrenOnly) {
699+ // Handle the case where we are given two DOM nodes that are not
700+ // compatible (e.g. <div> --> <span> or <div> --> TEXT)
701+ if (morphedNodeType === ELEMENT_NODE) {
702+ if (toNodeType === ELEMENT_NODE) {
703+ if (!compareNodeNames(fromNode, toNode)) {
704+ onNodeDiscarded(fromNode);
705+ morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
706+ }
707+ } else {
708+ // Going from an element node to a text node
709+ morphedNode = toNode;
710+ }
711+ } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
712+ if (toNodeType === morphedNodeType) {
713+ if (morphedNode.nodeValue !== toNode.nodeValue) {
714+ morphedNode.nodeValue = toNode.nodeValue;
715+ }
716+
717+ return morphedNode;
718+ } else {
719+ // Text node to something else
720+ morphedNode = toNode;
721+ }
722+ }
723+ }
724+
725+ if (morphedNode === toNode) {
726+ // The "to node" was not compatible with the "from node" so we had to
727+ // toss out the "from node" and use the "to node"
728+ onNodeDiscarded(fromNode);
729+ } else {
730+ if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
731+ return;
732+ }
733+
734+ morphEl(morphedNode, toNode, childrenOnly);
735+
736+ // We now need to loop over any keyed nodes that might need to be
737+ // removed. We only do the removal if we know that the keyed node
738+ // never found a match. When a keyed node is matched up we remove
739+ // it out of fromNodesLookup and we use fromNodesLookup to determine
740+ // if a keyed node has been matched up or not
741+ if (keyedRemovalList) {
742+ for (var i=0, len=keyedRemovalList.length; i<len; i++) {
743+ var elToRemove = fromNodesLookup[keyedRemovalList[i]];
744+ if (elToRemove) {
745+ removeNode(elToRemove, elToRemove.parentNode, false);
746+ }
747+ }
748+ }
749+ }
750+
751+ if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
752+ if (morphedNode.actualize) {
753+ morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
754+ }
755+ // If we had to swap out the from node with a new node because the old
756+ // node was not compatible with the target node then we need to
757+ // replace the old DOM node in the original DOM tree. This is only
758+ // possible if the original DOM node was part of a DOM tree which
759+ // we know is the case if it has a parent node.
760+ fromNode.parentNode.replaceChild(morphedNode, fromNode);
761+ }
762+
763+ return morphedNode;
764+ };
765+}
766+
767+var morphdom = morphdomFactory(morphAttrs);
768+
769+export default morphdom;
public/scripts/extensions/quick-reply/lib/morphdom.LICENSE.txt+21 -0
@@ -0,0 +1,21 @@
1+The MIT License (MIT)
2+
3+Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com)
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in
13+all copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+THE SOFTWARE.
21 \ No newline at end of file
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+9 -0
@@ -83,6 +83,15 @@ export class AutoExecuteHandler {
8383 await this.performAutoExecute(qrList);
8484 }
8585
86+ async handleNewChat() {
87+ if (!this.checkExecute()) return;
88+ const qrList = [
89+ ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat)).flat(),
90+ ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat))?.flat() ?? []),
91+ ];
92+ await this.performAutoExecute(qrList);
93+ }
94+
8695 /**
8796 * @param {any[]} entries Set of activated entries
8897 */
public/scripts/extensions/quick-reply/src/QuickReply.js+1285 -87
@@ -1,10 +1,17 @@
11import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js';
22import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
33import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
4+import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js';
5+import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
6+import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js';
7+import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
8+import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js';
9+import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
410import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
511import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
612import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
713import { log, quickReplyApi, warn } from '../index.js';
14+import morphdom from '../lib/morphdom-esm.js';
815import { QuickReplyContextLink } from './QuickReplyContextLink.js';
916import { QuickReplySet } from './QuickReplySet.js';
1017import { ContextMenu } from './ui/ctx/ContextMenu.js';
@@ -21,48 +28,62 @@ export class QuickReply {
2128
2229
2330
2431 /**@type {Numbernumber}*/ id;
2532 /**@type {Stringstring}*/ label = ''icon;
2633 /**@type {Stringstring}*/ titlelabel = '';
2734 /**@type {Stringboolean}*/ messageshowLabel = ''false;
35+ /**@type {string}*/ title = '';
36+ /**@type {string}*/ message = '';
2837
2938 /**@type {QuickReplyContextLink[]}*/ contextList;
3039
3140 /**@type {Booleanboolean}*/ preventAutoExecute = true;
3241 /**@type {Booleanboolean}*/ isHidden = false;
3342 /**@type {Booleanboolean}*/ executeOnStartup = false;
3443 /**@type {Booleanboolean}*/ executeOnUser = false;
3544 /**@type {Booleanboolean}*/ executeOnAi = false;
3645 /**@type {Booleanboolean}*/ executeOnChatChange = false;
3746 /**@type {Booleanboolean}*/ executeOnGroupMemberDraft = false;
3847 /**@type {Stringboolean}*/ automationIdexecuteOnNewChat = ''false;
48+ /**@type {string}*/ automationId = '';
3949
4050 /**@type {Functionfunction}*/ onExecute;
41- /**@type {Function}*/ onDelete;
51+ /**@type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>}*/ onDebug;
4252 /**@type {Functionfunction}*/ onUpdateonDelete;
53+ /**@type {function}*/ onUpdate;
54+ /**@type {function}*/ onInsertBefore;
55+ /**@type {function}*/ onTransfer;
4356
4457
4558 /**@type {HTMLElement}*/ dom;
59+ /**@type {HTMLElement}*/ domIcon;
4660 /**@type {HTMLElement}*/ domLabel;
4761 /**@type {HTMLElement}*/ settingsDom;
62+ /**@type {HTMLElement}*/ settingsDomIcon;
4863 /**@type {HTMLInputElement}*/ settingsDomLabel;
4964 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;
5065
5166 /**@type {Popup}*/ editorPopup;
67+ /**@type {HTMLElement}*/ editorDom;
5268
69+ /**@type {HTMLTextAreaElement}*/ editorMessage;
70+ /**@type {HTMLTextAreaElement}*/ editorMessageLabel;
71+ /**@type {HTMLElement}*/ editorSyntax;
5372 /**@type {HTMLElement}*/ editorExecuteBtn;
5473 /**@type {HTMLElement}*/ editorExecuteBtnPause;
5574 /**@type {HTMLElement}*/ editorExecuteBtnStop;
5675 /**@type {HTMLElement}*/ editorExecuteProgress;
5776 /**@type {HTMLElement}*/ editorExecuteErrors;
5877 /**@type {HTMLElement}*/ editorExecuteResult;
5978 /**@type {HTMLInputElementHTMLElement}*/ editorExecuteHideeditorDebugState;
6079 /**@type {Promise}*/ editorExecutePromise;
80+ /**@type {boolean}*/ isExecuting;
6181 /**@type {SlashCommandAbortController}*/ abortController;
82+ /**@type {SlashCommandDebugController}*/ debugController;
6283
6384
6485 get hasContext() {
6586 return this.contextList && this.contextList.filter(it => it.set).length > 0;
6687 }
6788
6889
@@ -75,6 +96,14 @@ export class QuickReply {
7596 updateRender() {
7697 if (!this.dom) return;
7798 this.dom.title = this.title || this.message;
99+ if (this.icon) {
100+ this.domIcon.classList.remove('qr--hidden');
101+ if (this.showLabel) this.domLabel.classList.remove('qr--hidden');
102+ else this.domLabel.classList.add('qr--hidden');
103+ } else {
104+ this.domIcon.classList.add('qr--hidden');
105+ this.domLabel.classList.remove('qr--hidden');
106+ }
78107 this.domLabel.textContent = this.label;
79108 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');
80109 }
@@ -105,9 +134,18 @@ export class QuickReply {
105134 }
106135 this.execute();
107136 });
137+ const icon = document.createElement('div'); {
138+ this.domIcon = icon;
139+ icon.classList.add('qr--button-icon');
140+ icon.classList.add('fa-solid');
141+ if (!this.icon) icon.classList.add('qr--hidden');
142+ else icon.classList.add(this.icon);
143+ root.append(icon);
144+ }
108145 const lbl = document.createElement('div'); {
109146 this.domLabel = lbl;
110147 lbl.classList.add('qr--button-label');
148+ if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
111149 lbl.textContent = this.label;
112150 root.append(lbl);
113151 }
@@ -138,36 +176,115 @@ export class QuickReply {
138176 item.classList.add('qr--set-item');
139177 item.setAttribute('data-order', String(idx));
140178 item.setAttribute('data-id', String(this.id));
141179 const dragadder = document.createElement('div'); {
142180 dragadder.classList.add('dragqr-handle-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');
148186 lblContainer addNew.classList.add('qr--set-itemLabelContainermenu_button');
149- const lbl = document.createElement('input'); {
187+ addNew.classList.add('menu_button_icon');
150- this.settingsDomLabel = lbl;
188+ addNew.classList.add('fa-solid');
151189 lbl addNew.classList.add('qr--setfa-itemLabelplus');
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);
156272 }
157273 item.append(lblContaineritemContent);
158274 }
159275 const optContainer = document.createElement('div'); {
160276 optContainer.classList.add('qr--set-optionsContainer');
161277 const opt = document.createElement('div'); {
162278 opt.classList.add('qr--action');
163279 opt.classList.add('menu_button');
280+ opt.classList.add('fa-fw');
164281 opt.classList.add('fa-solid');
165282 opt.textContent = '⁝';
166283 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';
167284 opt.addEventListener('click', ()=>this.showEditor());
168285 optContainer.append(opt);
169286 }
170287 itemitemContent.append(optContainer);
171288 }
172289 const mes = document.createElement('textarea'); {
173290 this.settingsDomMessage = mes;
@@ -176,22 +293,89 @@ export class QuickReply {
176293 mes.value = this.message;
177294 //HACK need to use jQuery to catch the triggered event from the expanded editor
178295 $(mes).on('input', ()=>this.updateMessage(mes.value));
179296 itemitemContent.append(mes);
180297 }
181298 const actions = document.createElement('div'); {
182299 actions.classList.add('qr--actions');
300+ const move = document.createElement('div'); {
301+ move.classList.add('qr--action');
302+ move.classList.add('menu_button');
303+ move.classList.add('fa-fw');
304+ move.classList.add('fa-solid');
305+ move.classList.add('fa-truck-arrow-right');
306+ move.title = 'Move quick reply to other set';
307+ move.addEventListener('click', ()=>this.onTransfer(this));
308+ actions.append(move);
309+ }
310+ const copy = document.createElement('div'); {
311+ copy.classList.add('qr--action');
312+ copy.classList.add('menu_button');
313+ copy.classList.add('fa-fw');
314+ copy.classList.add('fa-solid');
315+ copy.classList.add('fa-copy');
316+ copy.title = 'Copy quick reply to clipboard';
317+ copy.addEventListener('click', async()=>{
318+ await navigator.clipboard.writeText(JSON.stringify(this));
319+ copy.classList.add('qr--success');
320+ await delay(3010);
321+ copy.classList.remove('qr--success');
322+ });
323+ actions.append(copy);
324+ }
325+ const cut = document.createElement('div'); {
326+ cut.classList.add('qr--action');
327+ cut.classList.add('menu_button');
328+ cut.classList.add('fa-fw');
329+ cut.classList.add('fa-solid');
330+ cut.classList.add('fa-cut');
331+ cut.title = 'Cut quick reply to clipboard (copy and remove)';
332+ cut.addEventListener('click', async()=>{
333+ await navigator.clipboard.writeText(JSON.stringify(this));
334+ this.delete();
335+ });
336+ actions.append(cut);
337+ }
338+ const exp = document.createElement('div'); {
339+ exp.classList.add('qr--action');
340+ exp.classList.add('menu_button');
341+ exp.classList.add('fa-fw');
342+ exp.classList.add('fa-solid');
343+ exp.classList.add('fa-file-export');
344+ exp.title = 'Export quick reply as file';
345+ exp.addEventListener('click', ()=>{
346+ const blob = new Blob([JSON.stringify(this)], { type:'text' });
347+ const url = URL.createObjectURL(blob);
348+ const a = document.createElement('a'); {
349+ a.href = url;
350+ a.download = `${this.label}.qr.json`;
351+ a.click();
352+ }
353+ });
354+ actions.append(exp);
355+ }
183356 const del = document.createElement('div'); {
184357 del.classList.add('qr--action');
185358 del.classList.add('menu_button');
186359 del.classList.add('menu_button_iconfa-fw');
187360 del.classList.add('fa-solid');
188361 del.classList.add('fa-trash-can');
189362 del.classList.add('redWarningBG');
190363 del.title = 'Remove quickQuick replyReply\n---\nShit+Click to skip confirmation';
191364 del.addEventListener('click', async(evt)=>this.delete());{
365+ if (!evt.shiftKey) {
366+ const result = await Popup.show.confirm(
367+ 'Remove Quick Reply',
368+ 'Are you sure you want to remove this Quick Reply?',
369+ );
370+ if (result != POPUP_RESULT.AFFIRMATIVE) {
371+ return;
372+ }
373+ }
374+ this.delete();
375+ });
192376 actions.append(del);
193377 }
194378 itemitemContent.append(actions);
195379 }
196380 }
197381 }
@@ -208,22 +392,156 @@ export class QuickReply {
208392 /**@type {HTMLElement} */
209393 // @ts-ignore
210394 const dom = this.template.cloneNode(true);
395+ this.editorDom = dom;
211396 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
212397 const popupResult = this.editorPopup.show();
213398
214399 // basics
400+ /**@type {HTMLElement}*/
401+ const icon = dom.querySelector('#qr--modal-icon');
402+ if (this.icon) {
403+ icon.classList.add('fa-solid');
404+ icon.classList.add(this.icon);
405+ }
406+ else {
407+ icon.textContent = '…';
408+ }
409+ icon.addEventListener('click', async()=>{
410+ let value = await showFontAwesomePicker();
411+ if (value === null) return;
412+ if (this.icon) icon.classList.remove(this.icon);
413+ if (value == '') {
414+ icon.classList.remove('fa-solid');
415+ icon.textContent = '…';
416+ } else {
417+ icon.textContent = '';
418+ icon.classList.add('fa-solid');
419+ icon.classList.add(value);
420+ }
421+ this.updateIcon(value);
422+ });
423+ /**@type {HTMLInputElement}*/
424+ const showLabel = dom.querySelector('#qr--modal-showLabel');
425+ showLabel.checked = this.showLabel;
426+ showLabel.addEventListener('click', ()=>{
427+ this.updateShowLabel(showLabel.checked);
428+ });
215429 /**@type {HTMLInputElement}*/
216430 const label = dom.querySelector('#qr--modal-label');
217431 label.value = this.label;
218432 label.addEventListener('input', ()=>{
219433 this.updateLabel(label.value);
220434 });
435+ let switcherList;
436+ dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{
437+ if (switcherList) {
438+ switcherList.remove();
439+ switcherList = null;
440+ return;
441+ }
442+ const list = document.createElement('ul'); {
443+ switcherList = list;
444+ list.classList.add('qr--modal-switcherList');
445+ const makeList = (qrs)=>{
446+ const setItem = document.createElement('li'); {
447+ setItem.classList.add('qr--modal-switcherItem');
448+ setItem.addEventListener('click', ()=>{
449+ list.innerHTML = '';
450+ for (const qrs of quickReplyApi.listSets()) {
451+ const item = document.createElement('li'); {
452+ item.classList.add('qr--modal-switcherItem');
453+ item.addEventListener('click', ()=>{
454+ list.innerHTML = '';
455+ makeList(quickReplyApi.getSetByName(qrs));
456+ });
457+ const lbl = document.createElement('div'); {
458+ lbl.classList.add('qr--label');
459+ lbl.textContent = qrs;
460+ item.append(lbl);
461+ }
462+ list.append(item);
463+ }
464+ }
465+ });
466+ const lbl = document.createElement('div'); {
467+ lbl.classList.add('qr--label');
468+ const icon = document.createElement('i'); {
469+ icon.classList.add('fa-solid');
470+ icon.classList.add('fa-arrow-alt-circle-right');
471+ icon.classList.add('menu_button');
472+ lbl.append(icon);
473+ }
474+ const text = document.createElement('span'); {
475+ text.textContent = 'Switch QR Sets...';
476+ lbl.append(text);
477+ }
478+ setItem.append(lbl);
479+ }
480+ list.append(setItem);
481+ }
482+ const addItem = document.createElement('li'); {
483+ addItem.classList.add('qr--modal-switcherItem');
484+ addItem.addEventListener('click', ()=>{
485+ const qr = quickReplyApi.getSetByQr(this).addQuickReply();
486+ this.editorPopup.completeAffirmative();
487+ qr.showEditor();
488+ });
489+ const lbl = document.createElement('div'); {
490+ lbl.classList.add('qr--label');
491+ const icon = document.createElement('i'); {
492+ icon.classList.add('fa-solid');
493+ icon.classList.add('fa-plus');
494+ icon.classList.add('menu_button');
495+ lbl.append(icon);
496+ }
497+ const text = document.createElement('span'); {
498+ text.textContent = 'Add QR';
499+ lbl.append(text);
500+ }
501+ addItem.append(lbl);
502+ }
503+ list.append(addItem);
504+ }
505+ for (const qr of qrs.qrList.toSorted((a,b)=>a.label.toLowerCase().localeCompare(b.label.toLowerCase()))) {
506+ const item = document.createElement('li'); {
507+ item.classList.add('qr--modal-switcherItem');
508+ if (qr == this) item.classList.add('qr--current');
509+ else item.addEventListener('click', ()=>{
510+ this.editorPopup.completeAffirmative();
511+ qr.showEditor();
512+ });
513+ const lbl = document.createElement('div'); {
514+ lbl.classList.add('qr--label');
515+ lbl.textContent = qr.label;
516+ item.append(lbl);
517+ }
518+ const id = document.createElement('div'); {
519+ id.classList.add('qr--id');
520+ id.textContent = qr.id.toString();
521+ item.append(id);
522+ }
523+ const mes = document.createElement('div'); {
524+ mes.classList.add('qr--message');
525+ mes.textContent = qr.message;
526+ item.append(mes);
527+ }
528+ list.append(item);
529+ }
530+ }
531+ };
532+ makeList(quickReplyApi.getSetByQr(this));
533+ }
534+ label.parentElement.append(list);
535+ });
221536 /**@type {HTMLInputElement}*/
222537 const title = dom.querySelector('#qr--modal-title');
223538 title.value = this.title;
224539 title.addEventListener('input', () => {
225540 this.updateTitle(title.value);
226541 });
542+ /**@type {HTMLElement}*/
543+ const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
544+ this.editorSyntax = messageSyntaxInner;
227545 /**@type {HTMLInputElement}*/
228546 const wrap = dom.querySelector('#qr--modal-wrap');
229547 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');
@@ -235,9 +553,15 @@ export class QuickReply {
235553 if (wrap.checked) {
236554 message.style.whiteSpace = 'pre-wrap';
237555 messageSyntaxInner.style.whiteSpace = 'pre-wrap';
556+ if (this.clone) {
557+ this.clone.style.whiteSpace = 'pre-wrap';
558+ }
238559 } else {
239560 message.style.whiteSpace = 'pre';
240561 messageSyntaxInner.style.whiteSpace = 'pre';
562+ if (this.clone) {
563+ this.clone.style.whiteSpace = 'pre';
564+ }
241565 }
242566 updateScrollDebounced();
243567 };
@@ -261,11 +585,8 @@ export class QuickReply {
261585 });
262586 };
263587 const updateScrollDebounced = updateScroll;
264- const updateSyntax = ()=>{
265- messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
266- };
267588 const updateSyntaxEnabled = ()=>{
268- if (JSON.parse(localStorage.getItem('qr--syntax'))) {
589+ if (syntax.checked) {
269590 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
270591 } else {
271592 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
@@ -296,48 +617,108 @@ export class QuickReply {
296617 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
297618 updateSyntaxEnabled();
298619 });
620+ if (navigator.keyboard) {
621+ navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash'));
622+ } else {
623+ dom.querySelector('#qr--modal-commentKey').closest('small').remove();
624+ }
625+ this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]');
299626 /**@type {HTMLTextAreaElement}*/
300627 const message = dom.querySelector('#qr--modal-message');
628+ this.editorMessage = message;
301629 message.value = this.message;
630+ const updateMessageDebounced = debounce((value)=>this.updateMessage(value), 10);
302631 message.addEventListener('input', () => {
303632 updateSyntaxupdateMessageDebounced(message.value);
304- this.updateMessage(message.value);
305633 updateScrollDebounced();
306- });
634+ }, { passive:true });
307- setSlashCommandAutoComplete(message, true);
635+ const getLineStart = ()=>{
308- //TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
636+ const start = message.selectionStart;
637+ const end = message.selectionEnd;
638+ let lineStart;
639+ if (start == 0 || message.value[start - 1] == '\n') {
640+ // cursor is already at beginning of line
641+ // -> keep start
642+ lineStart = start;
643+ } else {
644+ // cursor is at end of line or somewhere in the line
645+ // -> find last newline before cursor and start after that
646+ lineStart = message.value.lastIndexOf('\n', start - 1) + 1;
647+ }
648+ return lineStart;
649+ };
309650 message.addEventListener('keydown', async(evt) => {
651+ if (this.isExecuting) return;
310652 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
653+ // increase indent
311654 evt.preventDefault();
312655 const start = message.selectionStart;
313656 const end = message.selectionEnd;
314657 if (end - start > 0 && message.value.substring(start, end).includes('\n')) {
315- const lineStart = message.value.lastIndexOf('\n', start);
658+ evt.stopImmediatePropagation();
316- const count = message.value.substring(lineStart, end).split('\n').length - 1;
659+ evt.stopPropagation();
317- message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;
660+ const lineStart = getLineStart();
318661 message.selectionStart = start + 1lineStart;
319- message.selectionEnd = end + count;
662+ const affectedLines = message.value.substring(lineStart, end).split('\n');
320- updateSyntax();
663+ // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
321- } else {
664+ document.execCommand('insertText', false, `\t${affectedLines.join('\n\t')}`);
322- message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
323665 message.selectionStart = start + 1;
324666 message.selectionEnd = end + 1affectedLines.length;
325- updateSyntax();
667+ message.dispatchEvent(new Event('input', { bubbles:true }));
668+ } else if (!(ac.isReplaceable && ac.isActive)) {
669+ evt.stopImmediatePropagation();
670+ evt.stopPropagation();
671+ // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
672+ document.execCommand('insertText', false, '\t');
673+ message.dispatchEvent(new Event('input', { bubbles:true }));
326674 }
327675 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
676+ // decrease indent
328677 evt.preventDefault();
678+ evt.stopImmediatePropagation();
679+ evt.stopPropagation();
329680 const start = message.selectionStart;
330681 const end = message.selectionEnd;
331682 const lineStart = message.value.lastIndexOfgetLineStart('\n', start);
332- const count = message.value.substring(lineStart, end).split('\n\t').length - 1;
683+ message.selectionStart = lineStart;
333- message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;
684+ const affectedLines = message.value.substring(lineStart, end).split('\n');
334- message.selectionStart = start - 1;
685+ const newText = affectedLines.map(it=>it.replace(/^\t/, '')).join('\n');
335- message.selectionEnd = end - count;
686+ const delta = affectedLines.join('\n').length - newText.length;
336- updateSyntax();
687+ // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
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+ }
337716 } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
338- evt.stopPropagation();
339- evt.preventDefault();
340717 if (executeShortcut.checked) {
718+ // execute QR
719+ evt.stopImmediatePropagation();
720+ evt.stopPropagation();
721+ evt.preventDefault();
341722 const selectionStart = message.selectionStart;
342723 const selectionEnd = message.selectionEnd;
343724 message.blur();
@@ -348,17 +729,175 @@ export class QuickReply {
348729 message.selectionEnd = selectionEnd;
349730 }
350731 }
732+ } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
733+ // toggle breakpoint
734+ evt.stopImmediatePropagation();
735+ evt.stopPropagation();
736+ evt.preventDefault();
737+ preBreakPointStart = message.selectionStart;
738+ preBreakPointEnd = message.selectionEnd;
739+ toggleBreakpoint();
740+ } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
741+ // toggle block comment
742+ // (evt.code will use the same physical key on the keyboard across different keyboard layouts)
743+ evt.stopImmediatePropagation();
744+ evt.stopPropagation();
745+ evt.preventDefault();
746+ // check if we are inside a comment -> uncomment
747+ const parser = new SlashCommandParser();
748+ parser.parse(message.value, false);
749+ const start = message.selectionStart;
750+ const end = message.selectionEnd;
751+ const comment = parser.commandIndex.findLast(it=>it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end));
752+ if (comment) {
753+ // uncomment
754+ let content = message.value.slice(comment.start + 1, comment.end - 1);
755+ let len = content.length;
756+ content = content.replace(/^ /, '');
757+ const offsetStart = len - content.length;
758+ len = content.length;
759+ content = content.replace(/ $/, '');
760+ const offsetEnd = len - content.length;
761+ message.selectionStart = comment.start - 1;
762+ message.selectionEnd = comment.end + 1;
763+ // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
764+ document.execCommand('insertText', false, content);
765+ message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0);
766+ message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0);
767+ } else {
768+ // comment
769+ const lineStart = getLineStart();
770+ const lineEnd = message.value.indexOf('\n', end);
771+ message.selectionStart = lineStart;
772+ message.selectionEnd = lineEnd;
773+ // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
774+ document.execCommand('insertText', false, `/* ${message.value.slice(lineStart, lineEnd)} *|`);
775+ message.selectionStart = start + 3;
776+ message.selectionEnd = end + 3;
777+ }
778+ message.dispatchEvent(new Event('input', { bubbles:true }));
351779 }
352780 });
781+ const ac = await setSlashCommandAutoComplete(message, true);
353782 message.addEventListener('wheel', (evt)=>{
354783 updateScrollDebounced(evt);
355784 });
356785 message.addEventListener('scroll', (evt)=>{
357786 updateScrollDebounced();
358787 });
788+ let preBreakPointStart;
789+ let preBreakPointEnd;
790+ /**
791+ * @param {SlashCommandBreakPoint} bp
792+ */
793+ const removeBreakpoint = (bp)=>{
794+ // start at -1 because "/" is not included in start-end
795+ let start = bp.start - 1;
796+ // step left until forward slash "/"
797+ while (message.value[start] != '/') start--;
798+ // step left while whitespace (except newline) before start
799+ while (/[^\S\n]/.test(message.value[start - 1])) start--;
800+ // if newline before indent, include the newline for removal
801+ if (message.value[start - 1] == '\n') start--;
802+ let end = bp.end;
803+ // step right while whitespace
804+ while (/\s/.test(message.value[end])) end++;
805+ // if pipe after whitepace, include pipe for removal
806+ if (message.value[end] == '|') end++;
807+ message.selectionStart = start;
808+ message.selectionEnd = end;
809+ // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
810+ document.execCommand('insertText', false, '');
811+ message.dispatchEvent(new Event('input', { bubbles:true }));
812+ let postStart = preBreakPointStart;
813+ let postEnd = preBreakPointEnd;
814+ // set caret back to where it was
815+ if (preBreakPointStart <= start) {
816+ // selection start was before breakpoint: do nothing
817+ } else if (preBreakPointStart > start && preBreakPointEnd < end) {
818+ // selection start was inside breakpoint: move to index before breakpoint
819+ postStart = start;
820+ } else if (preBreakPointStart >= end) {
821+ // selection start was behind breakpoint: move back by length of removed string
822+ postStart = preBreakPointStart - (end - start);
823+ }
824+ if (preBreakPointEnd <= start) {
825+ // do nothing
826+ } else if (preBreakPointEnd > start && preBreakPointEnd < end) {
827+ // selection end was inside breakpoint: move to index before breakpoint
828+ postEnd = start;
829+ } else if (preBreakPointEnd >= end) {
830+ // selection end was behind breakpoint: move back by length of removed string
831+ postEnd = preBreakPointEnd - (end - start);
832+ }
833+ return { start:postStart, end:postEnd };
834+ };
835+ /**
836+ * @param {SlashCommandExecutor} cmd
837+ */
838+ const addBreakpoint = (cmd)=>{
839+ // start at -1 because "/" is not included in start-end
840+ let start = cmd.start - 1;
841+ let indent = '';
842+ // step left until forward slash "/"
843+ while (message.value[start] != '/') start--;
844+ // step left while whitespace (except newline) before start, collect the whitespace to help build indentation
845+ while (/[^\S\n]/.test(message.value[start - 1])) {
846+ start--;
847+ indent += message.value[start];
848+ }
849+ // if newline before indent, include the newline
850+ if (message.value[start - 1] == '\n') {
851+ start--;
852+ indent = `\n${indent}`;
853+ }
854+ const breakpointText = `${indent}/breakpoint |`;
855+ message.selectionStart = start;
856+ message.selectionEnd = start;
857+ // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
858+ document.execCommand('insertText', false, breakpointText);
859+ message.dispatchEvent(new Event('input', { bubbles:true }));
860+ return breakpointText.length;
861+ };
862+ const toggleBreakpoint = ()=>{
863+ const idx = message.selectionStart;
864+ let postStart = preBreakPointStart;
865+ let postEnd = preBreakPointEnd;
866+ const parser = new SlashCommandParser();
867+ parser.parse(message.value, false);
868+ const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx);
869+ if (cmdIdx > -1) {
870+ const cmd = parser.commandIndex[cmdIdx];
871+ if (cmd instanceof SlashCommandBreakPoint) {
872+ const bp = cmd;
873+ const { start, end } = removeBreakpoint(bp);
874+ postStart = start;
875+ postEnd = end;
876+ } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) {
877+ const bp = parser.commandIndex[cmdIdx - 1];
878+ const { start, end } = removeBreakpoint(bp);
879+ postStart = start;
880+ postEnd = end;
881+ } else {
882+ const len = addBreakpoint(cmd);
883+ postStart += len;
884+ postEnd += len;
885+ }
886+ message.selectionStart = postStart;
887+ message.selectionEnd = postEnd;
888+ }
889+ };
890+ message.addEventListener('pointerdown', (evt)=>{
891+ if (!evt.ctrlKey || !evt.altKey) return;
892+ preBreakPointStart = message.selectionStart;
893+ preBreakPointEnd = message.selectionEnd;
894+ });
895+ message.addEventListener('pointerup', async(evt)=>{
896+ if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return;
897+ toggleBreakpoint();
898+ });
359899 /** @type {any} */
360900 const resizeListener = debounce((evt) => {
361- updateSyntax();
362901 updateScrollDebounced(evt);
363902 if (document.activeElement == message) {
364903 message.blur();
@@ -367,10 +906,43 @@ export class QuickReply {
367906 });
368907 window.addEventListener('resize', resizeListener);
369908 updateSyntaxEnabled();
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());
370945 message.style.setProperty('text-shadow', 'none', 'important');
371- /**@type {HTMLElement}*/
372- const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
373- updateSyntax();
374946 updateWrap();
375947 updateTabSize();
376948
@@ -379,7 +951,7 @@ export class QuickReply {
379951 const tpl = dom.querySelector('#qr--ctxItem');
380952 const linkList = dom.querySelector('#qr--ctxEditor');
381953 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
382954 [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => {
383955 const opt = document.createElement('option'); {
384956 opt.value = qrs.name;
385957 opt.textContent = qrs.name;
@@ -388,7 +960,7 @@ export class QuickReply {
388960 }
389961 });
390962 };
391963 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Numbernumber}*/idx) => {
392964 /**@type {HTMLElement} */
393965 // @ts-ignore
394966 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
@@ -490,6 +1062,13 @@ export class QuickReply {
4901062 this.updateContext();
4911063 });
4921064 /**@type {HTMLInputElement}*/
1065+ const executeOnNewChat = dom.querySelector('#qr--executeOnNewChat');
1066+ executeOnNewChat.checked = this.executeOnNewChat;
1067+ executeOnNewChat.addEventListener('click', ()=>{
1068+ this.executeOnNewChat = executeOnNewChat.checked;
1069+ this.updateContext();
1070+ });
1071+ /**@type {HTMLInputElement}*/
4931072 const automationId = dom.querySelector('#qr--automationId');
4941073 automationId.value = this.automationId;
4951074 automationId.addEventListener('input', () => {
@@ -506,9 +1085,9 @@ export class QuickReply {
5061085 /**@type {HTMLElement}*/
5071086 const executeResult = dom.querySelector('#qr--modal-executeResult');
5081087 this.editorExecuteResult = executeResult;
5091088 /**@type {HTMLInputElementHTMLElement}*/
5101089 const executeHidedebugState = dom.querySelector('#qr--modal-executeHidedebugState');
5111090 this.editorExecuteHideeditorDebugState = executeHidedebugState;
5121091 /**@type {HTMLElement}*/
5131092 const executeBtn = dom.querySelector('#qr--modal-execute');
5141093 this.editorExecuteBtn = executeBtn;
@@ -536,6 +1115,76 @@ export class QuickReply {
5361115 this.abortController?.abort('Stop button clicked');
5371116 });
5381117
1118+ /**@type {HTMLTextAreaElement} */
1119+ const inputOg = document.querySelector('#send_textarea');
1120+ const inputMirror = dom.querySelector('#qr--modal-send_textarea');
1121+ inputMirror.value = inputOg.value;
1122+ const inputOgMo = new MutationObserver(muts=>{
1123+ if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) {
1124+ inputOg.removeEventListener('input', inputOgListener);
1125+ }
1126+ });
1127+ inputOgMo.observe(document.body, { childList:true });
1128+ const inputOgListener = ()=>{
1129+ inputMirror.value = inputOg.value;
1130+ };
1131+ inputOg.addEventListener('input', inputOgListener);
1132+ inputMirror.addEventListener('input', ()=>{
1133+ inputOg.value = inputMirror.value;
1134+ });
1135+
1136+ /**@type {HTMLElement}*/
1137+ const resumeBtn = dom.querySelector('#qr--modal-resume');
1138+ resumeBtn.addEventListener('click', ()=>{
1139+ this.debugController?.resume();
1140+ });
1141+ /**@type {HTMLElement}*/
1142+ const stepBtn = dom.querySelector('#qr--modal-step');
1143+ stepBtn.addEventListener('click', ()=>{
1144+ this.debugController?.step();
1145+ });
1146+ /**@type {HTMLElement}*/
1147+ const stepIntoBtn = dom.querySelector('#qr--modal-stepInto');
1148+ stepIntoBtn.addEventListener('click', ()=>{
1149+ this.debugController?.stepInto();
1150+ });
1151+ /**@type {HTMLElement}*/
1152+ const stepOutBtn = dom.querySelector('#qr--modal-stepOut');
1153+ stepOutBtn.addEventListener('click', ()=>{
1154+ this.debugController?.stepOut();
1155+ });
1156+ /**@type {HTMLElement}*/
1157+ const minimizeBtn = dom.querySelector('#qr--modal-minimize');
1158+ minimizeBtn.addEventListener('click', ()=>{
1159+ this.editorDom.classList.add('qr--minimized');
1160+ });
1161+ const maximizeBtn = dom.querySelector('#qr--modal-maximize');
1162+ maximizeBtn.addEventListener('click', ()=>{
1163+ this.editorDom.classList.remove('qr--minimized');
1164+ });
1165+ /**@type {boolean}*/
1166+ let isResizing = false;
1167+ let resizeStart;
1168+ let wStart;
1169+ /**@type {HTMLElement}*/
1170+ const resizeHandle = dom.querySelector('#qr--resizeHandle');
1171+ resizeHandle.addEventListener('pointerdown', (evt)=>{
1172+ if (isResizing) return;
1173+ isResizing = true;
1174+ evt.preventDefault();
1175+ resizeStart = evt.x;
1176+ wStart = dom.querySelector('#qr--qrOptions').offsetWidth;
1177+ const dragListener = debounce((evt)=>{
1178+ const w = wStart + resizeStart - evt.x;
1179+ dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`);
1180+ }, 5);
1181+ window.addEventListener('pointerup', ()=>{
1182+ window.removeEventListener('pointermove', dragListener);
1183+ isResizing = false;
1184+ }, { once:true });
1185+ window.addEventListener('pointermove', dragListener);
1186+ });
1187+
5391188 await popupResult;
5401189
5411190 window.removeEventListener('resize', resizeListener);
@@ -544,8 +1193,62 @@ export class QuickReply {
5441193 }
5451194 }
5461195
1196+ getEditorPosition(start, end, message = null) {
1197+ const inputRect = this.editorMessage.getBoundingClientRect();
1198+ const style = window.getComputedStyle(this.editorMessage);
1199+ if (!this.clone) {
1200+ this.clone = document.createElement('div');
1201+ for (const key of style) {
1202+ this.clone.style[key] = style[key];
1203+ }
1204+ this.clone.style.position = 'fixed';
1205+ this.clone.style.visibility = 'hidden';
1206+ const mo = new MutationObserver(muts=>{
1207+ if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) {
1208+ this.clone?.remove();
1209+ this.clone = null;
1210+ }
1211+ });
1212+ mo.observe(document.body, { childList:true });
1213+ }
1214+ document.body.append(this.clone);
1215+ this.clone.style.width = `${inputRect.width}px`;
1216+ this.clone.style.height = `${inputRect.height}px`;
1217+ this.clone.style.left = `${inputRect.left}px`;
1218+ this.clone.style.top = `${inputRect.top}px`;
1219+ this.clone.style.whiteSpace = style.whiteSpace;
1220+ this.clone.style.tabSize = style.tabSize;
1221+ const text = message ?? this.editorMessage.value;
1222+ const before = text.slice(0, start);
1223+ this.clone.textContent = before;
1224+ const locator = document.createElement('span');
1225+ locator.textContent = text.slice(start, end);
1226+ this.clone.append(locator);
1227+ this.clone.append(text.slice(end));
1228+ this.clone.scrollTop = this.editorSyntax.scrollTop;
1229+ this.clone.scrollLeft = this.editorSyntax.scrollLeft;
1230+ const locatorRect = locator.getBoundingClientRect();
1231+ const bodyRect = document.body.getBoundingClientRect();
1232+ const location = {
1233+ left: locatorRect.left - bodyRect.left,
1234+ right: locatorRect.right - bodyRect.left,
1235+ top: locatorRect.top - bodyRect.top,
1236+ bottom: locatorRect.bottom - bodyRect.top,
1237+ };
1238+ // this.clone.remove();
1239+ return location;
1240+ }
5471241 async executeFromEditor() {
5481242 if (this.editorExecutePromiseisExecuting) return;
1243+ this.editorPopup.onClosing = ()=>false;
1244+ const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/;
1245+ const oText = this.message;
1246+ this.isExecuting = true;
1247+ this.editorDom.classList.add('qr--isExecuting');
1248+ const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax');
1249+ if (noSyntax) {
1250+ this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
1251+ }
5491252 this.editorExecuteBtn.classList.add('qr--busy');
5501253 this.editorExecuteProgress.style.setProperty('--prog', '0');
5511254 this.editorExecuteErrors.classList.remove('qr--hasErrors');
@@ -556,12 +1259,451 @@ export class QuickReply {
5561259 this.editorExecuteProgress.classList.remove('qr--aborted');
5571260 this.editorExecuteErrors.innerHTML = '';
5581261 this.editorExecuteResult.innerHTML = '';
559- if (this.editorExecuteHide.checked) {
1262+ const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner');
560- this.editorPopup.dlg.classList.add('qr--hide');
1263+ const updateScroll = (evt) => {
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+ });
5621289 try {
5631290 this.editorExecutePromiseabortController = this.execute({},new trueSlashCommandAbortController();
564- const result = await this.editorExecutePromise;
1291+ this.debugController = new SlashCommandDebugController();
1292+ this.debugController.onBreakPoint = async(closure, executor)=>{
1293+ this.editorDom.classList.add('qr--isPaused');
1294+ syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
1295+ this.editorMessageLabel.innerHTML = '';
1296+ if (uuidCheck.test(closure.source)) {
1297+ const p0 = document.createElement('span'); {
1298+ p0.textContent = 'anonymous: ';
1299+ this.editorMessageLabel.append(p0);
1300+ }
1301+ const p1 = document.createElement('strong'); {
1302+ p1.textContent = executor.source.slice(0,5);
1303+ this.editorMessageLabel.append(p1);
1304+ }
1305+ const p2 = document.createElement('span'); {
1306+ p2.textContent = executor.source.slice(5, -5);
1307+ this.editorMessageLabel.append(p2);
1308+ }
1309+ const p3 = document.createElement('strong'); {
1310+ p3.textContent = executor.source.slice(-5);
1311+ this.editorMessageLabel.append(p3);
1312+ }
1313+ } else {
1314+ this.editorMessageLabel.textContent = executor.source;
1315+ }
1316+ const source = closure.source;
1317+ this.editorDebugState.innerHTML = '';
1318+ let ci = -1;
1319+ const varNames = [];
1320+ const macroNames = [];
1321+ /**
1322+ * @param {SlashCommandScope} scope
1323+ */
1324+ const buildVars = (scope, isCurrent = false)=>{
1325+ if (!isCurrent) {
1326+ ci--;
1327+ }
1328+ const c = this.debugController.stack.slice(ci)[0];
1329+ const wrap = document.createElement('div'); {
1330+ wrap.classList.add('qr--scope');
1331+ if (isCurrent) {
1332+ const executor = this.debugController.cmdStack.slice(-1)[0];
1333+ { // named args
1334+ const namedTitle = document.createElement('div'); {
1335+ namedTitle.classList.add('qr--title');
1336+ namedTitle.textContent = `Named Args - /${executor.name}`;
1337+ if (executor.command.name == 'run') {
1338+ namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1339+ }
1340+ wrap.append(namedTitle);
1341+ }
1342+ const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it=>it.name)]);
1343+ for (const key of keys) {
1344+ if (key[0] == '_') continue;
1345+ const item = document.createElement('div'); {
1346+ item.classList.add('qr--var');
1347+ const k = document.createElement('div'); {
1348+ k.classList.add('qr--key');
1349+ k.textContent = key;
1350+ item.append(k);
1351+ }
1352+ const vUnresolved = document.createElement('div'); {
1353+ vUnresolved.classList.add('qr--val');
1354+ vUnresolved.classList.add('qr--singleCol');
1355+ const val = executor.namedArgumentList.find(it=>it.name == key)?.value;
1356+ if (val instanceof SlashCommandClosure) {
1357+ vUnresolved.classList.add('qr--closure');
1358+ vUnresolved.title = val.rawText;
1359+ vUnresolved.textContent = val.toString();
1360+ } else if (val === undefined) {
1361+ vUnresolved.classList.add('qr--undefined');
1362+ vUnresolved.textContent = 'undefined';
1363+ } else {
1364+ let jsonVal;
1365+ try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1366+ if (jsonVal && typeof jsonVal == 'object') {
1367+ vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1368+ } else {
1369+ vUnresolved.textContent = val;
1370+ vUnresolved.classList.add('qr--simple');
1371+ }
1372+ }
1373+ item.append(vUnresolved);
1374+ }
1375+ const vResolved = document.createElement('div'); {
1376+ vResolved.classList.add('qr--val');
1377+ vResolved.classList.add('qr--singleCol');
1378+ if (this.debugController.namedArguments === undefined) {
1379+ vResolved.classList.add('qr--unresolved');
1380+ } else {
1381+ const val = this.debugController.namedArguments?.[key];
1382+ if (val instanceof SlashCommandClosure) {
1383+ vResolved.classList.add('qr--closure');
1384+ vResolved.title = val.rawText;
1385+ vResolved.textContent = val.toString();
1386+ } else if (val === undefined) {
1387+ vResolved.classList.add('qr--undefined');
1388+ vResolved.textContent = 'undefined';
1389+ } else {
1390+ let jsonVal;
1391+ try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1392+ if (jsonVal && typeof jsonVal == 'object') {
1393+ vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1394+ } else {
1395+ vResolved.textContent = val;
1396+ vResolved.classList.add('qr--simple');
1397+ }
1398+ }
1399+ }
1400+ item.append(vResolved);
1401+ }
1402+ wrap.append(item);
1403+ }
1404+ }
1405+ }
1406+ { // unnamed args
1407+ const unnamedTitle = document.createElement('div'); {
1408+ unnamedTitle.classList.add('qr--title');
1409+ unnamedTitle.textContent = `Unnamed Args - /${executor.name}`;
1410+ if (executor.command.name == 'run') {
1411+ unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1412+ }
1413+ wrap.append(unnamedTitle);
1414+ }
1415+ let i = 0;
1416+ let unnamed = this.debugController.unnamedArguments ?? [];
1417+ if (!Array.isArray(unnamed)) unnamed = [unnamed];
1418+ while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined);
1419+ unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]);
1420+ for (const arg of unnamed) {
1421+ i++;
1422+ const item = document.createElement('div'); {
1423+ item.classList.add('qr--var');
1424+ const k = document.createElement('div'); {
1425+ k.classList.add('qr--key');
1426+ k.textContent = i.toString();
1427+ item.append(k);
1428+ }
1429+ const vUnresolved = document.createElement('div'); {
1430+ vUnresolved.classList.add('qr--val');
1431+ vUnresolved.classList.add('qr--singleCol');
1432+ const val = arg[0]?.value;
1433+ if (val instanceof SlashCommandClosure) {
1434+ vUnresolved.classList.add('qr--closure');
1435+ vUnresolved.title = val.rawText;
1436+ vUnresolved.textContent = val.toString();
1437+ } else if (val === undefined) {
1438+ vUnresolved.classList.add('qr--undefined');
1439+ vUnresolved.textContent = 'undefined';
1440+ } else {
1441+ let jsonVal;
1442+ try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1443+ if (jsonVal && typeof jsonVal == 'object') {
1444+ vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1445+ } else {
1446+ vUnresolved.textContent = val;
1447+ vUnresolved.classList.add('qr--simple');
1448+ }
1449+ }
1450+ item.append(vUnresolved);
1451+ }
1452+ const vResolved = document.createElement('div'); {
1453+ vResolved.classList.add('qr--val');
1454+ vResolved.classList.add('qr--singleCol');
1455+ if (this.debugController.unnamedArguments === undefined) {
1456+ vResolved.classList.add('qr--unresolved');
1457+ } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) {
1458+ // do nothing
1459+ } else {
1460+ const val = arg[1];
1461+ if (val instanceof SlashCommandClosure) {
1462+ vResolved.classList.add('qr--closure');
1463+ vResolved.title = val.rawText;
1464+ vResolved.textContent = val.toString();
1465+ } else if (val === undefined) {
1466+ vResolved.classList.add('qr--undefined');
1467+ vResolved.textContent = 'undefined';
1468+ } else {
1469+ let jsonVal;
1470+ try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1471+ if (jsonVal && typeof jsonVal == 'object') {
1472+ vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1473+ } else {
1474+ vResolved.textContent = val;
1475+ vResolved.classList.add('qr--simple');
1476+ }
1477+ }
1478+ }
1479+ item.append(vResolved);
1480+ }
1481+ wrap.append(item);
1482+ }
1483+ }
1484+ }
1485+ }
1486+ // current scope
1487+ const title = document.createElement('div'); {
1488+ title.classList.add('qr--title');
1489+ title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope';
1490+ if (c.source == source) {
1491+ let hi;
1492+ title.addEventListener('pointerenter', ()=>{
1493+ const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText);
1494+ const layer = syntax.getBoundingClientRect();
1495+ hi = document.createElement('div');
1496+ hi.classList.add('qr--highlight-secondary');
1497+ hi.style.left = `${loc.left - layer.left}px`;
1498+ hi.style.width = `${loc.right - loc.left}px`;
1499+ hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1500+ hi.style.height = `${loc.bottom - loc.top}px`;
1501+ syntax.append(hi);
1502+ });
1503+ title.addEventListener('pointerleave', ()=>hi?.remove());
1504+ }
1505+ wrap.append(title);
1506+ }
1507+ for (const key of Object.keys(scope.variables)) {
1508+ const isHidden = varNames.includes(key);
1509+ if (!isHidden) varNames.push(key);
1510+ const item = document.createElement('div'); {
1511+ item.classList.add('qr--var');
1512+ if (isHidden) item.classList.add('qr--isHidden');
1513+ const k = document.createElement('div'); {
1514+ k.classList.add('qr--key');
1515+ k.textContent = key;
1516+ item.append(k);
1517+ }
1518+ const v = document.createElement('div'); {
1519+ v.classList.add('qr--val');
1520+ const val = scope.variables[key];
1521+ if (val instanceof SlashCommandClosure) {
1522+ v.classList.add('qr--closure');
1523+ v.title = val.rawText;
1524+ v.textContent = val.toString();
1525+ } else if (val === undefined) {
1526+ v.classList.add('qr--undefined');
1527+ v.textContent = 'undefined';
1528+ } else {
1529+ let jsonVal;
1530+ try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1531+ if (jsonVal && typeof jsonVal == 'object') {
1532+ v.textContent = JSON.stringify(jsonVal, null, 2);
1533+ } else {
1534+ v.textContent = val;
1535+ v.classList.add('qr--simple');
1536+ }
1537+ }
1538+ item.append(v);
1539+ }
1540+ wrap.append(item);
1541+ }
1542+ }
1543+ for (const key of Object.keys(scope.macros)) {
1544+ const isHidden = macroNames.includes(key);
1545+ if (!isHidden) macroNames.push(key);
1546+ const item = document.createElement('div'); {
1547+ item.classList.add('qr--macro');
1548+ if (isHidden) item.classList.add('qr--isHidden');
1549+ const k = document.createElement('div'); {
1550+ k.classList.add('qr--key');
1551+ k.textContent = key;
1552+ item.append(k);
1553+ }
1554+ const v = document.createElement('div'); {
1555+ v.classList.add('qr--val');
1556+ const val = scope.macros[key];
1557+ if (val instanceof SlashCommandClosure) {
1558+ v.classList.add('qr--closure');
1559+ v.title = val.rawText;
1560+ v.textContent = val.toString();
1561+ } else if (val === undefined) {
1562+ v.classList.add('qr--undefined');
1563+ v.textContent = 'undefined';
1564+ } else {
1565+ let jsonVal;
1566+ try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1567+ if (jsonVal && typeof jsonVal == 'object') {
1568+ v.textContent = JSON.stringify(jsonVal, null, 2);
1569+ } else {
1570+ v.textContent = val;
1571+ v.classList.add('qr--simple');
1572+ }
1573+ }
1574+ item.append(v);
1575+ }
1576+ wrap.append(item);
1577+ }
1578+ }
1579+ const pipeItem = document.createElement('div'); {
1580+ pipeItem.classList.add('qr--pipe');
1581+ const k = document.createElement('div'); {
1582+ k.classList.add('qr--key');
1583+ k.textContent = 'pipe';
1584+ pipeItem.append(k);
1585+ }
1586+ const v = document.createElement('div'); {
1587+ v.classList.add('qr--val');
1588+ const val = scope.pipe;
1589+ if (val instanceof SlashCommandClosure) {
1590+ v.classList.add('qr--closure');
1591+ v.title = val.rawText;
1592+ v.textContent = val.toString();
1593+ } else if (val === undefined) {
1594+ v.classList.add('qr--undefined');
1595+ v.textContent = 'undefined';
1596+ } else {
1597+ let jsonVal;
1598+ try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1599+ if (jsonVal && typeof jsonVal == 'object') {
1600+ v.textContent = JSON.stringify(jsonVal, null, 2);
1601+ } else {
1602+ v.textContent = val;
1603+ v.classList.add('qr--simple');
1604+ }
1605+ }
1606+ pipeItem.append(v);
1607+ }
1608+ wrap.append(pipeItem);
1609+ }
1610+ if (scope.parent) {
1611+ wrap.append(buildVars(scope.parent));
1612+ }
1613+ }
1614+ return wrap;
1615+ };
1616+ const buildStack = ()=>{
1617+ const wrap = document.createElement('div'); {
1618+ wrap.classList.add('qr--stack');
1619+ const title = document.createElement('div'); {
1620+ title.classList.add('qr--title');
1621+ title.textContent = 'Call Stack';
1622+ wrap.append(title);
1623+ }
1624+ let ei = -1;
1625+ for (const executor of this.debugController.cmdStack.toReversed()) {
1626+ ei++;
1627+ const c = this.debugController.stack.toReversed()[ei];
1628+ const item = document.createElement('div'); {
1629+ item.classList.add('qr--item');
1630+ if (executor.source == source) {
1631+ let hi;
1632+ item.addEventListener('pointerenter', ()=>{
1633+ const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText);
1634+ const layer = syntax.getBoundingClientRect();
1635+ hi = document.createElement('div');
1636+ hi.classList.add('qr--highlight-secondary');
1637+ hi.style.left = `${loc.left - layer.left}px`;
1638+ hi.style.width = `${loc.right - loc.left}px`;
1639+ hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1640+ hi.style.height = `${loc.bottom - loc.top}px`;
1641+ syntax.append(hi);
1642+ });
1643+ item.addEventListener('pointerleave', ()=>hi?.remove());
1644+ }
1645+ const cmd = document.createElement('div'); {
1646+ cmd.classList.add('qr--cmd');
1647+ cmd.textContent = `/${executor.name}`;
1648+ if (executor.command.name == 'run') {
1649+ cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1650+ }
1651+ item.append(cmd);
1652+ }
1653+ const src = document.createElement('div'); {
1654+ src.classList.add('qr--source');
1655+ const line = closure.fullText.slice(0, executor.start).split('\n').length;
1656+ if (uuidCheck.test(executor.source)) {
1657+ const p1 = document.createElement('span'); {
1658+ p1.classList.add('qr--fixed');
1659+ p1.textContent = executor.source.slice(0,5);
1660+ src.append(p1);
1661+ }
1662+ const p2 = document.createElement('span'); {
1663+ p2.classList.add('qr--truncated');
1664+ p2.textContent = '…';
1665+ src.append(p2);
1666+ }
1667+ const p3 = document.createElement('span'); {
1668+ p3.classList.add('qr--fixed');
1669+ p3.textContent = `${executor.source.slice(-5)}:${line}`;
1670+ src.append(p3);
1671+ }
1672+ src.title = `anonymous: ${executor.source}`;
1673+ } else {
1674+ src.textContent = `${executor.source}:${line}`;
1675+ }
1676+ item.append(src);
1677+ }
1678+ wrap.append(item);
1679+ }
1680+ }
1681+ }
1682+ return wrap;
1683+ };
1684+ this.editorDebugState.append(buildVars(closure.scope, true));
1685+ this.editorDebugState.append(buildStack());
1686+ this.editorDebugState.classList.add('qr--active');
1687+ const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText);
1688+ const layer = syntax.getBoundingClientRect();
1689+ const hi = document.createElement('div');
1690+ hi.classList.add('qr--highlight');
1691+ if (this.debugController.namedArguments === undefined) {
1692+ hi.classList.add('qr--unresolved');
1693+ }
1694+ hi.style.left = `${loc.left - layer.left}px`;
1695+ hi.style.width = `${loc.right - loc.left}px`;
1696+ hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1697+ hi.style.height = `${loc.bottom - loc.top}px`;
1698+ syntax.append(hi);
1699+ const isStepping = await this.debugController.awaitContinue();
1700+ hi.remove();
1701+ this.editorDebugState.textContent = '';
1702+ this.editorDebugState.classList.remove('qr--active');
1703+ this.editorDom.classList.remove('qr--isPaused');
1704+ return isStepping;
1705+ };
1706+ const result = await this.onDebug(this);
5651707 if (this.abortController?.signal?.aborted) {
5661708 this.editorExecuteProgress.classList.add('qr--aborted');
5671709 } else {
@@ -586,9 +1728,18 @@ export class QuickReply {
5861728 `;
5871729 }
5881730 }
1731+ if (noSyntax) {
1732+ this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
1733+ }
1734+ this.editorMessageLabel.innerHTML = '';
1735+ this.editorMessageLabel.textContent = 'Message / Command: ';
1736+ this.editorMessage.value = oText;
1737+ this.editorMessage.dispatchEvent(new Event('input', { bubbles:true }));
5891738 this.editorExecutePromise = null;
5901739 this.editorExecuteBtn.classList.remove('qr--busy');
5911740 this.editorPopup.dlgeditorDom.classList.remove('qr--hideisExecuting');
1741+ this.isExecuting = false;
1742+ this.editorPopup.onClosing = null;
5921743 }
5931744
5941745 updateEditorProgress(done, total) {
@@ -623,6 +1774,47 @@ export class QuickReply {
6231774 /**
6241775 * @param {string} value
6251776 */
1777+ updateIcon(value) {
1778+ if (this.onUpdate) {
1779+ if (value === null) return;
1780+ if (this.settingsDomIcon) {
1781+ if (this.icon != value) {
1782+ if (value == '') {
1783+ if (this.icon) {
1784+ this.settingsDomIcon.classList.remove(this.icon);
1785+ }
1786+ this.settingsDomIcon.textContent = '…';
1787+ this.settingsDomIcon.classList.remove('fa-solid');
1788+ } else {
1789+ if (this.icon) {
1790+ this.settingsDomIcon.classList.remove(this.icon);
1791+ } else {
1792+ this.settingsDomIcon.classList.add('fa-solid');
1793+ }
1794+ this.settingsDomIcon.classList.add(value);
1795+ }
1796+ }
1797+ }
1798+ this.icon = value;
1799+ this.updateRender();
1800+ this.onUpdate(this);
1801+ }
1802+ }
1803+
1804+ /**
1805+ * @param {boolean} value
1806+ */
1807+ updateShowLabel(value) {
1808+ if (this.onUpdate) {
1809+ this.showLabel = value;
1810+ this.updateRender();
1811+ this.onUpdate(this);
1812+ }
1813+ }
1814+
1815+ /**
1816+ * @param {string} value
1817+ */
6261818 updateLabel(value) {
6271819 if (this.onUpdate) {
6281820 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {
@@ -670,21 +1862,25 @@ export class QuickReply {
6701862 }
6711863
6721864
6731865 async execute(args = {}, isEditor = false, isRun = false, options = {}) {
6741866 if (this.message?.length > 0 && this.onExecute) {
6751867 const scope = new SlashCommandScope();
6761868 for (const key of Object.keys(args)) {
1869+ if (key[0] == '_') continue;
1870+ if (key == 'isAutoExecute') continue;
6771871 scope.setMacro(`arg::${key}`, args[key]);
6781872 }
1873+ scope.setMacro('arg::*', '');
6791874 if (isEditor) {
6801875 this.abortController = new SlashCommandAbortController();
6811876 }
6821877 return await this.onExecute(this, {
6831878 message: this.message,
6841879 isAutoExecute: args.isAutoExecute ?? false,
6851880 isEditor,
6861881 isRun,
6871882 scope,
1883+ executionOptions: options,
6881884 });
6891885 }
6901886 }
@@ -695,6 +1891,8 @@ export class QuickReply {
6951891 toJSON() {
6961892 return {
6971893 id: this.id,
1894+ icon: this.icon,
1895+ showLabel: this.showLabel,
6981896 label: this.label,
6991897 title: this.title,
7001898 message: this.message,
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+6 -1
@@ -60,7 +60,12 @@ export class QuickReplyConfig {
6060 /**@type {HTMLElement}*/
6161 this.setListDom = root.querySelector('.qr--setList');
6262 root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
63- this.addSet(QuickReplySet.list[0]);
63+ const newSet = QuickReplySet.list.find(qr=>!this.setList.find(qrl=>qrl.set == qr));
64+ if (newSet) {
65+ this.addSet(newSet);
66+ } else {
67+ toastr.warning('All existing QR Sets have already been added.');
68+ }
6469 });
6570 this.updateSetListDom();
6671 }
public/scripts/extensions/quick-reply/src/QuickReplySet.js+191 -20
@@ -1,7 +1,9 @@
11import { getRequestHeaders, substituteParams } from '../../../../script.js';
2+import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
23import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4+import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
35import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
46import { debounceAsync, log, warn } from '../index.js';
57import { QuickReply } from './QuickReply.js';
68
79export class QuickReplySet {
@@ -16,7 +18,7 @@ export class QuickReplySet {
1618 }
1719
1820 /**
1921 * @param {Stringstring} name - name of the QuickReplySet
2022 */
2123 static get(name) {
2224 return this.list.find(it=>it.name == name);
@@ -25,17 +27,19 @@ export class QuickReplySet {
2527
2628
2729
2830 /**@type {Stringstring}*/ name;
2931 /**@type {Booleanboolean}*/ disableSend = false;
3032 /**@type {Booleanboolean}*/ placeBeforeInput = false;
3133 /**@type {Booleanboolean}*/ injectInput = false;
34+ /**@type {string}*/ color = 'transparent';
35+ /**@type {boolean}*/ onlyBorderColor = false;
3236 /**@type {QuickReply[]}*/ qrList = [];
3337
3438 /**@type {Numbernumber}*/ idIndex = 0;
3539
3640 /**@type {Booleanboolean}*/ isDeleted = false;
3741
3842 /**@type {Functionfunction}*/ save;
3943
4044 /**@type {HTMLElement}*/ dom;
4145 /**@type {HTMLElement}*/ settingsDom;
@@ -64,6 +68,7 @@ export class QuickReplySet {
6468 const root = document.createElement('div'); {
6569 this.dom = root;
6670 root.classList.add('qr--buttons');
71+ this.updateColor();
6772 this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
6873 root.append(qr.render());
6974 });
@@ -78,6 +83,22 @@ export class QuickReplySet {
7883 this.dom.append(qr.render());
7984 });
8085 }
86+ updateColor() {
87+ if (!this.dom) return;
88+ if (this.color && this.color != 'transparent') {
89+ this.dom.style.setProperty('--qr--color', this.color);
90+ this.dom.classList.add('qr--color');
91+ if (this.onlyBorderColor) {
92+ this.dom.classList.add('qr--borderColor');
93+ } else {
94+ this.dom.classList.remove('qr--borderColor');
95+ }
96+ } else {
97+ this.dom.style.setProperty('--qr--color', 'transparent');
98+ this.dom.classList.remove('qr--color');
99+ this.dom.classList.remove('qr--borderColor');
100+ }
101+ }
81102
82103
83104
@@ -93,6 +114,11 @@ export class QuickReplySet {
93114 }
94115 return this.settingsDom;
95116 }
117+ /**
118+ *
119+ * @param {QuickReply} qr
120+ * @param {number} idx
121+ */
96122 renderSettingsItem(qr, idx) {
97123 this.settingsDom.append(qr.renderSettings(idx));
98124 }
@@ -102,6 +128,18 @@ export class QuickReplySet {
102128
103129 /**
104130 *
131+ * @param {QuickReply} qr
132+ */
133+ async debug(qr) {
134+ const parser = new SlashCommandParser();
135+ const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController);
136+ closure.source = `${this.name}.${qr.label}`;
137+ closure.onProgress = (done, total) => qr.updateEditorProgress(done, total);
138+ closure.scope.setMacro('arg::*', '');
139+ return (await closure.execute())?.pipe;
140+ }
141+ /**
142+ *
105143 * @param {QuickReply} qr The QR to execute.
106144 * @param {object} options
107145 * @param {string} [options.message] (null) altered message to be used
@@ -109,6 +147,7 @@ export class QuickReplySet {
109147 * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
110148 * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
111149 * @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
150+ * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options
112151 * @returns
113152 */
114153 async executeWithOptions(qr, options = {}) {
@@ -118,7 +157,9 @@ export class QuickReplySet {
118157 isEditor:false,
119158 isRun:false,
120159 scope:null,
160+ executionOptions:{},
121161 }, options);
162+ const execOptions = options.executionOptions;
122163 /**@type {HTMLTextAreaElement}*/
123164 const ta = document.querySelector('#send_textarea');
124165 const finalMessage = options.message ?? qr.message;
@@ -136,21 +177,24 @@ export class QuickReplySet {
136177 if (input[0] == '/' && !this.disableSend) {
137178 let result;
138179 if (options.isAutoExecute || options.isRun) {
139180 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
140181 handleParserErrors: true,
141182 scope: options.scope,
142- });
183+ source: `${this.name}.${qr.label}`,
184+ }));
143185 } else if (options.isEditor) {
144186 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
145187 handleParserErrors: false,
146188 scope: options.scope,
147189 abortController: qr.abortController,
190+ source: `${this.name}.${qr.label}`,
148191 onProgress: (done, total) => qr.updateEditorProgress(done, total),
149192 }));
150193 } else {
151194 result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, {
152195 scope: options.scope,
153- });
196+ source: `${this.name}.${qr.label}`,
197+ }));
154198 }
155199 return typeof result === 'object' ? result?.pipe : '';
156200 }
@@ -165,7 +209,7 @@ export class QuickReplySet {
165209 }
166210 /**
167211 * @param {QuickReply} qr
168212 * @param {Stringstring} [message] - optional altered message to be used
169213 * @param {SlashCommandScope} [scope] - optional scope to be used when running the command
170214 */
171215 async execute(qr, message = null, isAutoExecute = false, scope = null) {
@@ -179,10 +223,11 @@ export class QuickReplySet {
179223
180224
181225
182226 addQuickReply(data = {}) {
183227 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228+ data.id =
184229 this.idIndex = id + 1;
185230 const qr = QuickReply.from({ id }data);
186231 this.qrList.push(qr);
187232 this.hookQuickReply(qr);
188233 if (this.settingsDom) {
@@ -194,11 +239,131 @@ export class QuickReplySet {
194239 this.save();
195240 return qr;
196241 }
242+ addQuickReplyFromText(qrJson) {
243+ let data;
244+ if (qrJson) {
245+ try {
246+ data = JSON.parse(qrJson ?? '{}');
247+ delete data.id;
248+ } catch {
249+ // not JSON data
250+ }
251+ if (data) {
252+ // JSON data
253+ if (data.label === undefined || data.message === undefined) {
254+ // not a QR
255+ toastr.error('Not a QR.');
256+ return;
257+ }
258+ } else {
259+ // no JSON, use plaintext as QR message
260+ data = { message: qrJson };
261+ }
262+ } else {
263+ data = {};
264+ }
265+ const newQr = this.addQuickReply(data);
266+ return newQr;
267+ }
197268
269+ /**
270+ *
271+ * @param {QuickReply} qr
272+ */
198273 hookQuickReply(qr) {
274+ qr.onDebug = ()=>this.debug(qr);
199275 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
200276 qr.onDelete = ()=>this.removeQuickReply(qr);
201277 qr.onUpdate = ()=>this.save();
278+ qr.onInsertBefore = (qrJson)=>{
279+ this.addQuickReplyFromText(qrJson);
280+ const newQr = this.qrList.pop();
281+ this.qrList.splice(this.qrList.indexOf(qr), 0, newQr);
282+ if (qr.settingsDom) {
283+ qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom);
284+ }
285+ this.save();
286+ };
287+ qr.onTransfer = async()=>{
288+ /**@type {HTMLSelectElement} */
289+ let sel;
290+ let isCopy = false;
291+ const dom = document.createElement('div'); {
292+ dom.classList.add('qr--transferModal');
293+ const title = document.createElement('h3'); {
294+ title.textContent = 'Transfer Quick Reply';
295+ dom.append(title);
296+ }
297+ const subTitle = document.createElement('h4'); {
298+ const entryName = qr.label;
299+ const bookName = this.name;
300+ subTitle.textContent = `${bookName}: ${entryName}`;
301+ dom.append(subTitle);
302+ }
303+ sel = document.createElement('select'); {
304+ sel.classList.add('qr--transferSelect');
305+ sel.setAttribute('autofocus', '1');
306+ const noOpt = document.createElement('option'); {
307+ noOpt.value = '';
308+ noOpt.textContent = '-- Select QR Set --';
309+ sel.append(noOpt);
310+ }
311+ for (const qrs of QuickReplySet.list) {
312+ const opt = document.createElement('option'); {
313+ opt.value = qrs.name;
314+ opt.textContent = qrs.name;
315+ sel.append(opt);
316+ }
317+ }
318+ sel.addEventListener('keyup', (evt)=>{
319+ if (evt.key == 'Shift') {
320+ (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
321+ return;
322+ }
323+ });
324+ sel.addEventListener('keydown', (evt)=>{
325+ if (evt.key == 'Shift') {
326+ (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
327+ return;
328+ }
329+ if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') {
330+ evt.preventDefault();
331+ if (evt.shiftKey) isCopy = true;
332+ dlg.completeAffirmative();
333+ }
334+ });
335+ dom.append(sel);
336+ }
337+ const hintP = document.createElement('p'); {
338+ const hint = document.createElement('small'); {
339+ hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.';
340+ hintP.append(hint);
341+ }
342+ dom.append(hintP);
343+ }
344+ }
345+ const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' });
346+ const copyBtn = document.createElement('div'); {
347+ copyBtn.classList.add('qr--copy');
348+ copyBtn.classList.add('menu_button');
349+ copyBtn.textContent = 'Copy';
350+ copyBtn.addEventListener('click', ()=>{
351+ isCopy = true;
352+ dlg.completeAffirmative();
353+ });
354+ (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
355+ }
356+ const prom = dlg.show();
357+ sel.focus();
358+ await prom;
359+ if (dlg.result == POPUP_RESULT.AFFIRMATIVE) {
360+ const qrs = QuickReplySet.list.find(it=>it.name == sel.value);
361+ qrs.addQuickReply(qr.toJSON());
362+ if (!isCopy) {
363+ qr.delete();
364+ }
365+ }
366+ };
202367 }
203368
204369 removeQuickReply(qr) {
@@ -214,6 +379,8 @@ export class QuickReplySet {
214379 disableSend: this.disableSend,
215380 placeBeforeInput: this.placeBeforeInput,
216381 injectInput: this.injectInput,
382+ color: this.color,
383+ onlyBorderColor: this.onlyBorderColor,
217384 qrList: this.qrList,
218385 idIndex: this.idIndex,
219386 };
@@ -245,8 +412,12 @@ export class QuickReplySet {
245412 if (response.ok) {
246413 this.unrender();
247414 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+ }
250421 } else {
251422 warn(`Failed to delete Quick Reply Set: ${this.name}`);
252423 }
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js+1 -1
@@ -45,7 +45,7 @@ export class QuickReplySetLink {
4545 this.set = QuickReplySet.get(set.value);
4646 this.update();
4747 });
48- QuickReplySet.list.forEach(qrs=>{
48+ QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
4949 const opt = document.createElement('option'); {
5050 opt.value = qrs.name;
5151 opt.textContent = qrs.name;
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+2 -0
@@ -16,6 +16,7 @@ export class QuickReplySettings {
1616 /**@type {Boolean}*/ isEnabled = false;
1717 /**@type {Boolean}*/ isCombined = false;
1818 /**@type {Boolean}*/ isPopout = false;
19+ /**@type {Boolean}*/ showPopoutButton = true;
1920 /**@type {QuickReplyConfig}*/ config;
2021 /**@type {QuickReplyConfig}*/ _chatConfig;
2122 get chatConfig() {
@@ -79,6 +80,7 @@ export class QuickReplySettings {
7980 isEnabled: this.isEnabled,
8081 isCombined: this.isCombined,
8182 isPopout: this.isPopout,
83+ showPopoutButton: this.showPopoutButton,
8284 config: this.config,
8385 };
8486 }
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+259 -18
@@ -1,8 +1,12 @@
11import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
2+import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
23import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
4+import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
35import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';
6+import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
47import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';
58import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
9+import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
610import { isTrueBoolean } from '../../../utils.js';
711// eslint-disable-next-line no-unused-vars
812import { QuickReplyApi } from '../api/QuickReplyApi.js';
@@ -47,6 +51,13 @@ export class SlashCommandHandler {
4751 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);
4852 }) ?? [],
4953
54+ /** All QRs inside a set, utilizing the "set" named argument, returns the QR's ID */
55+ qrIds: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => {
56+ const icons = getExecutionIcons(qr);
57+ const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim();
58+ return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr, null, ()=>qr.id.toString(), true);
59+ }) ?? [],
60+
5061 /** All QRs as a set.name string, to be able to execute, for example via the /run command */
5162 qrExecutables: () => {
5263 const globalSetList = this.api.settings.config.setList;
@@ -63,7 +74,7 @@ export class SlashCommandHandler {
6374 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),
6475 ];
6576 },
6677 };
6778
6879 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;
6980
@@ -234,8 +245,20 @@ export class SlashCommandHandler {
234245 name: 'label',
235246 description: 'text on the button, e.g., label=MyButton',
236247 typeList: [ARGUMENT_TYPE.STRING],
237248 isRequired: truefalse,
238249 enumProvider: localEnumProviders.qrLabelsqrEntries,
250+ }),
251+ SlashCommandNamedArgument.fromProps({
252+ name: 'icon',
253+ description: 'icon to show on the button, e.g., icon=fa-pencil',
254+ typeList: [ARGUMENT_TYPE.STRING],
255+ isRequired: false,
256+ }),
257+ SlashCommandNamedArgument.fromProps({
258+ name: 'showlabel',
259+ description: 'whether to show the label even when an icon is assigned, e.g., icon=fa-pencil showlabel=true',
260+ typeList: [ARGUMENT_TYPE.BOOLEAN],
261+ isRequired: false,
239262 }),
240263 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
241264 new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
@@ -247,6 +270,13 @@ export class SlashCommandHandler {
247270 ];
248271 const qrUpdateArgs = [
249272 new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
273+ SlashCommandNamedArgument.fromProps({
274+ name: 'id',
275+ description: 'numeric ID of the QR, e.g., id=42',
276+ typeList: [ARGUMENT_TYPE.NUMBER],
277+ isRequired: false,
278+ enumProvider: localEnumProviders.qrIds,
279+ }),
250280 ];
251281
252282 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
@@ -272,13 +302,61 @@ export class SlashCommandHandler {
272302 </div>
273303 `,
274304 }));
305+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-get',
306+ callback: (args, _) => {
307+ return this.getQuickReply(args);
308+ },
309+ namedArgumentList: [
310+ SlashCommandNamedArgument.fromProps({
311+ name: 'set',
312+ description: 'name of the QR set, e.g., set=PresetName1',
313+ typeList: [ARGUMENT_TYPE.STRING],
314+ isRequired: true,
315+ enumProvider: localEnumProviders.qrSets,
316+ }),
317+ SlashCommandNamedArgument.fromProps({
318+ name: 'label',
319+ description: 'text on the button, e.g., label=MyButton',
320+ typeList: [ARGUMENT_TYPE.STRING],
321+ isRequired: false,
322+ enumProvider: localEnumProviders.qrEntries,
323+ }),
324+ SlashCommandNamedArgument.fromProps({
325+ name: 'id',
326+ description: 'numeric ID of the QR, e.g., id=42',
327+ typeList: [ARGUMENT_TYPE.NUMBER],
328+ isRequired: false,
329+ enumProvider: localEnumProviders.qrIds,
330+ }),
331+ ],
332+ returns: 'a dictionary with all the QR\'s properties',
333+ helpString: `
334+ <div>Get a Quick Reply's properties.</div>
335+ <div>
336+ <strong>Examples:</strong>
337+ <ul>
338+ <li>
339+ <pre><code>/qr-get set=MyPreset label=MyButton | /echo</code></pre>
340+ <pre><code>/qr-get set=MyPreset id=42 | /echo</code></pre>
341+ </li>
342+ </ul>
343+ </div>
344+ `,
345+ }));
275346 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
276347 callback: (args, message) => {
277348 this.updateQuickReply(args, message);
278349 return '';
279350 },
280351 returns: 'updated quick reply',
281352 namedArgumentList: [...qrUpdateArgs, ...qrArgs],.map(it=>{
353+ if (it.name == 'label') {
354+ const clone = SlashCommandNamedArgument.fromProps(it);
355+ clone.isRequired = false;
356+ return clone;
357+ }
358+ return it;
359+ })],
282360 unnamedArgumentList: [
283361 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),
284362 ],
@@ -315,6 +393,12 @@ export class SlashCommandHandler {
315393 typeList: [ARGUMENT_TYPE.STRING],
316394 enumProvider: localEnumProviders.qrEntries,
317395 }),
396+ SlashCommandNamedArgument.fromProps({
397+ name: 'id',
398+ description: 'numeric ID of the QR, e.g., id=42',
399+ typeList: [ARGUMENT_TYPE.NUMBER],
400+ enumProvider: localEnumProviders.qrIds,
401+ }),
318402 ],
319403 unnamedArgumentList: [
320404 SlashCommandArgument.fromProps({
@@ -344,6 +428,12 @@ export class SlashCommandHandler {
344428 typeList: [ARGUMENT_TYPE.STRING],
345429 enumProvider: localEnumProviders.qrEntries,
346430 }),
431+ SlashCommandNamedArgument.fromProps({
432+ name: 'id',
433+ description: 'numeric ID of the QR, e.g., id=42',
434+ typeList: [ARGUMENT_TYPE.NUMBER],
435+ enumProvider: localEnumProviders.qrIds,
436+ }),
347437 new SlashCommandNamedArgument(
348438 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
349439 ),
@@ -389,6 +479,12 @@ export class SlashCommandHandler {
389479 typeList: [ARGUMENT_TYPE.STRING],
390480 enumProvider: localEnumProviders.qrEntries,
391481 }),
482+ SlashCommandNamedArgument.fromProps({
483+ name: 'id',
484+ description: 'numeric ID of the QR, e.g., id=42',
485+ typeList: [ARGUMENT_TYPE.NUMBER],
486+ enumProvider: localEnumProviders.qrIds,
487+ }),
392488 ],
393489 unnamedArgumentList: [
394490 SlashCommandArgument.fromProps({
@@ -425,6 +521,12 @@ export class SlashCommandHandler {
425521 isRequired: true,
426522 enumProvider: localEnumProviders.qrSets,
427523 }),
524+ SlashCommandNamedArgument.fromProps({
525+ name: 'id',
526+ description: 'numeric ID of the QR, e.g., id=42',
527+ typeList: [ARGUMENT_TYPE.NUMBER],
528+ enumProvider: localEnumProviders.qrIds,
529+ }),
428530 ],
429531 unnamedArgumentList: [
430532 SlashCommandArgument.fromProps({
@@ -454,8 +556,8 @@ export class SlashCommandHandler {
454556 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
455557 ];
456558 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
457559 callback: async (args, name) => {
458560 await this.createSet(name, args);
459561 return '';
460562 },
461563 aliases: ['qr-presetadd'],
@@ -485,8 +587,8 @@ export class SlashCommandHandler {
485587 }));
486588
487589 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
488590 callback: async (args, name) => {
489591 await this.updateSet(name, args);
490592 return '';
491593 },
492594 aliases: ['qr-presetupdate'],
@@ -510,8 +612,8 @@ export class SlashCommandHandler {
510612 `,
511613 }));
512614 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
513615 callback: async (_, name) => {
514616 await this.deleteSet(name);
515617 return '';
516618 },
517619 aliases: ['qr-presetdelete'],
@@ -533,6 +635,134 @@ export class SlashCommandHandler {
533635 </div>
534636 `,
535637 }));
638+
639+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-arg',
640+ callback: ({ _scope }, [key, value]) => {
641+ _scope.setMacro(`arg::${key}`, value, key.includes('*'));
642+ return '';
643+ },
644+ unnamedArgumentList: [
645+ SlashCommandArgument.fromProps({ description: 'argument name',
646+ typeList: ARGUMENT_TYPE.STRING,
647+ isRequired: true,
648+ }),
649+ SlashCommandArgument.fromProps({ description: 'argument value',
650+ typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
651+ isRequired: true,
652+ }),
653+ ],
654+ splitUnnamedArgument: true,
655+ splitUnnamedArgumentCount: 2,
656+ helpString: `
657+ <div>
658+ Set a fallback value for a Quick Reply argument.
659+ </div>
660+ <div>
661+ <strong>Example:</strong>
662+ <pre><code>/qr-arg x foo |\n/echo {{arg::x}}</code></pre>
663+ </div>
664+ `,
665+ }));
666+
667+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'import',
668+ /**
669+ *
670+ * @param {{_scope:SlashCommandScope, _abortController:SlashCommandAbortController, _debugController:SlashCommandDebugController, from:string}} args
671+ * @param {string} value
672+ */
673+ callback: (args, value) => {
674+ if (!args.from) throw new Error('/import requires from= to be set.');
675+ if (!value) throw new Error('/import requires the unnamed argument to be set.');
676+ let qr = [...this.api.listGlobalSets(), ...this.api.listChatSets()]
677+ .map(it=>this.api.getSetByName(it)?.qrList ?? [])
678+ .flat()
679+ .find(it=>it.label == args.from)
680+ ;
681+ if (!qr) {
682+ let [setName, ...qrNameParts] = args.from.split('.');
683+ let qrName = qrNameParts.join('.');
684+ let qrs = QuickReplySet.get(setName);
685+ if (qrs) {
686+ qr = qrs.qrList.find(it=>it.label == qrName);
687+ }
688+ }
689+ if (qr) {
690+ const parser = new SlashCommandParser();
691+ const closure = parser.parse(qr.message, true, [], args._abortController, args._debugController);
692+ if (args._debugController) {
693+ closure.source = args.from;
694+ }
695+ const testCandidates = (executor)=>{
696+ return (
697+ executor.namedArgumentList.find(arg=>arg.name == 'key')
698+ && executor.unnamedArgumentList.length > 0
699+ && executor.unnamedArgumentList[0].value instanceof SlashCommandClosure
700+ ) || (
701+ !executor.namedArgumentList.find(arg=>arg.name == 'key')
702+ && executor.unnamedArgumentList.length > 1
703+ && executor.unnamedArgumentList[1].value instanceof SlashCommandClosure
704+ );
705+ };
706+ const candidates = closure.executorList
707+ .filter(executor=>['let', 'var'].includes(executor.command.name))
708+ .filter(testCandidates)
709+ .map(executor=>({
710+ key: executor.namedArgumentList.find(arg=>arg.name == 'key')?.value ?? executor.unnamedArgumentList[0].value,
711+ value: executor.unnamedArgumentList[executor.namedArgumentList.find(arg=>arg.name == 'key') ? 0 : 1].value,
712+ }))
713+ ;
714+ for (let i = 0; i < value.length; i++) {
715+ const srcName = value[i];
716+ let dstName = srcName;
717+ if (i + 2 < value.length && value[i + 1] == 'as') {
718+ dstName = value[i + 2];
719+ i += 2;
720+ }
721+ const pick = candidates.find(it=>it.key == srcName);
722+ if (!pick) throw new Error(`No scoped closure named "${srcName}" found in "${args.from}"`);
723+ if (args._scope.existsVariableInScope(dstName)) {
724+ args._scope.setVariable(dstName, pick.value);
725+ } else {
726+ args._scope.letVariable(dstName, pick.value);
727+ }
728+ }
729+ } else {
730+ throw new Error(`No Quick Reply found for "${name}".`);
731+ }
732+ return '';
733+ },
734+ namedArgumentList: [
735+ SlashCommandNamedArgument.fromProps({ name: 'from',
736+ description: 'Quick Reply to import from (QRSet.QRLabel)',
737+ typeList: ARGUMENT_TYPE.STRING,
738+ isRequired: true,
739+ }),
740+ ],
741+ unnamedArgumentList: [
742+ SlashCommandArgument.fromProps({ description: 'what to import (x or x as y)',
743+ acceptsMultiple: true,
744+ typeList: ARGUMENT_TYPE.STRING,
745+ isRequired: true,
746+ }),
747+ ],
748+ splitUnnamedArgument: true,
749+ helpString: `
750+ <div>
751+ Import one or more closures from another Quick Reply.
752+ </div>
753+ <div>
754+ Only imports closures that are directly assigned a scoped variable via <code>/let</code> or <code>/var</code>.
755+ </div>
756+ <div>
757+ <strong>Examples:</strong>
758+ <ul>
759+ <li><pre><code>/import from=LibraryQrSet.FooBar foo |\n/:foo</code></pre></li>
760+ <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo\n\tbar\n|\n/:foo |\n/:bar</code></pre></li>
761+ <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo as x\n\tbar as y\n|\n/:x |\n/:y</code></pre></li>
762+ </ul>
763+ </div>
764+ `,
765+ }));
536766 }
537767
538768
@@ -618,6 +848,8 @@ export class SlashCommandHandler {
618848 args.set ?? '',
619849 args.label ?? '',
620850 {
851+ icon: args.icon,
852+ showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
621853 message: message ?? '',
622854 title: args.title,
623855 isHidden: isTrueBoolean(args.hidden),
@@ -633,12 +865,21 @@ export class SlashCommandHandler {
633865 toastr.error(ex.message);
634866 }
635867 }
868+ getQuickReply(args) {
869+ try {
870+ return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
871+ } catch (ex) {
872+ toastr.error(ex.message);
873+ }
874+ }
636875 updateQuickReply(args, message) {
637876 try {
638877 this.api.updateQuickReply(
639878 args.set ?? '',
640- args.label ?? '',
879+ args.id !== undefined ? Number(args.id) : (args.label ?? ''),
641880 {
881+ icon: args.icon,
882+ showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
642883 newLabel: args.newlabel,
643884 message: (message ?? '').trim().length > 0 ? message : undefined,
644885 title: args.title,
@@ -657,7 +898,7 @@ export class SlashCommandHandler {
657898 }
658899 deleteQuickReply(args, label) {
659900 try {
660901 this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label));
661902 } catch (ex) {
662903 toastr.error(ex.message);
663904 }
@@ -692,9 +933,9 @@ export class SlashCommandHandler {
692933 }
693934
694935
695936 async createSet(name, args) {
696937 try {
697938 await this.api.createSet(
698939 args.name ?? name ?? '',
699940 {
700941 disableSend: isTrueBoolean(args.nosend),
@@ -706,9 +947,9 @@ export class SlashCommandHandler {
706947 toastr.error(ex.message);
707948 }
708949 }
709950 async updateSet(name, args) {
710951 try {
711952 await this.api.updateSet(
712953 args.name ?? name ?? '',
713954 {
714955 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,
@@ -720,9 +961,9 @@ export class SlashCommandHandler {
720961 toastr.error(ex.message);
721962 }
722963 }
723964 async deleteSet(name) {
724965 try {
725966 await this.api.deleteSet(name ?? '');
726967 } catch (ex) {
727968 toastr.error(ex.message);
728969 }
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js+14 -11
@@ -69,17 +69,20 @@ export class ButtonUi {
6969 root.id = 'qr--bar';
7070 root.classList.add('flex-container');
7171 root.classList.add('flexGap5');
72- const popout = document.createElement('div'); {
72+ if (this.settings.showPopoutButton) {
73- popout.id = 'qr--popoutTrigger';
73+ root.classList.add('popoutVisible');
7474 const popout.classList = document.addcreateElement('menu_buttondiv'); {
75- popout.classList.add('fa-solid');
75+ popout.id = 'qr--popoutTrigger';
7676 popout.classList.add('fa-window-restoremenu_button');
77- popout.addEventListener('click', ()=>{
77+ popout.classList.add('fa-solid');
78- this.settings.isPopout = true;
78+ popout.classList.add('fa-window-restore');
79- this.refresh();
79+ popout.addEventListener('click', ()=>{
8080 this.settings.save()isPopout = true;
81- });
81+ this.refresh();
82- root.append(popout);
82+ this.settings.save();
83+ });
84+ root.append(popout);
85+ }
8386 }
8487 if (this.settings.isCombined) {
8588 const buttons = document.createElement('div'); {
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+64 -3
@@ -14,6 +14,7 @@ export class SettingsUi {
1414
1515 /**@type {HTMLInputElement}*/ isEnabled;
1616 /**@type {HTMLInputElement}*/ isCombined;
17+ /**@type {HTMLInputElement}*/ showPopoutButton;
1718
1819 /**@type {HTMLElement}*/ globalSetList;
1920
@@ -23,6 +24,8 @@ export class SettingsUi {
2324 /**@type {HTMLInputElement}*/ disableSend;
2425 /**@type {HTMLInputElement}*/ placeBeforeInput;
2526 /**@type {HTMLInputElement}*/ injectInput;
27+ /**@type {HTMLInputElement}*/ color;
28+ /**@type {HTMLInputElement}*/ onlyBorderColor;
2629 /**@type {HTMLSelectElement}*/ currentSet;
2730
2831
@@ -77,6 +80,10 @@ export class SettingsUi {
7780 this.isCombined = this.dom.querySelector('#qr--isCombined');
7881 this.isCombined.checked = this.settings.isCombined;
7982 this.isCombined.addEventListener('click', ()=>this.onIsCombined());
83+
84+ this.showPopoutButton = this.dom.querySelector('#qr--showPopoutButton');
85+ this.showPopoutButton.checked = this.settings.showPopoutButton;
86+ this.showPopoutButton.addEventListener('click', ()=>this.onShowPopoutButton());
8087 }
8188
8289 prepareGlobalSetList() {
@@ -117,10 +124,29 @@ export class SettingsUi {
117124 this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
118125 this.currentQrSet.addQuickReply();
119126 });
127+ this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{
128+ const text = await navigator.clipboard.readText();
129+ this.currentQrSet.addQuickReplyFromText(text);
130+ });
131+ this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{
132+ const inp = document.createElement('input'); {
133+ inp.type = 'file';
134+ inp.accept = '.json';
135+ inp.addEventListener('change', async()=>{
136+ if (inp.files.length > 0) {
137+ for (const file of inp.files) {
138+ const text = await file.text();
139+ this.currentQrSet.addQuickReply(JSON.parse(text));
140+ }
141+ }
142+ });
143+ inp.click();
144+ }
145+ });
120146 this.qrList = this.dom.querySelector('#qr--set-qrList');
121147 this.currentSet = this.dom.querySelector('#qr--set');
122148 this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
123- QuickReplySet.list.forEach(qrs=>{
149+ QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
124150 const opt = document.createElement('option'); {
125151 opt.value = qrs.name;
126152 opt.textContent = qrs.name;
@@ -145,6 +171,34 @@ export class SettingsUi {
145171 qrs.injectInput = this.injectInput.checked;
146172 qrs.save();
147173 });
174+ let initialColorChange = true;
175+ this.color = this.dom.querySelector('#qr--color');
176+ this.color.color = this.currentQrSet?.color ?? 'transparent';
177+ this.color.addEventListener('change', (evt)=>{
178+ if (!this.dom.closest('body')) return;
179+ const qrs = this.currentQrSet;
180+ if (initialColorChange) {
181+ initialColorChange = false;
182+ this.color.color = qrs.color;
183+ return;
184+ }
185+ qrs.color = evt.detail.rgb;
186+ qrs.save();
187+ this.currentQrSet.updateColor();
188+ });
189+ this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
190+ const qrs = this.currentQrSet;
191+ this.color.color = 'transparent';
192+ qrs.save();
193+ this.currentQrSet.updateColor();
194+ });
195+ this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor');
196+ this.onlyBorderColor.addEventListener('click', ()=>{
197+ const qrs = this.currentQrSet;
198+ qrs.onlyBorderColor = this.onlyBorderColor.checked;
199+ qrs.save();
200+ this.currentQrSet.updateColor();
201+ });
148202 this.onQrSetChange();
149203 }
150204 onQrSetChange() {
@@ -152,6 +206,8 @@ export class SettingsUi {
152206 this.disableSend.checked = this.currentQrSet.disableSend;
153207 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
154208 this.injectInput.checked = this.currentQrSet.injectInput;
209+ this.color.color = this.currentQrSet.color ?? 'transparent';
210+ this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
155211 this.qrList.innerHTML = '';
156212 const qrsDom = this.currentQrSet.renderSettings();
157213 this.qrList.append(qrsDom);
@@ -184,6 +240,11 @@ export class SettingsUi {
184240 this.settings.save();
185241 }
186242
243+ async onShowPopoutButton() {
244+ this.settings.showPopoutButton = this.showPopoutButton.checked;
245+ this.settings.save();
246+ }
247+
187248 async onGlobalSetListSort() {
188249 this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{
189250 const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
@@ -265,7 +326,7 @@ export class SettingsUi {
265326 const qrs = new QuickReplySet();
266327 qrs.name = name;
267328 qrs.addQuickReply();
268329 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
269330 if (idx > -1) {
270331 QuickReplySet.list.splice(idx, 0, qrs);
271332 } else {
@@ -321,7 +382,7 @@ export class SettingsUi {
321382 this.prepareChatSetList();
322383 }
323384 } else {
324385 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
325386 if (idx > -1) {
326387 QuickReplySet.list.splice(idx, 0, qrs);
327388 } else {
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js+5 -0
@@ -33,11 +33,14 @@ export class ContextMenu {
3333 */
3434 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
3535 const tree = {
36+ icon: qr.icon,
37+ showLabel: qr.showLabel,
3638 label: qr.label,
3739 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
3840 children: [],
3941 };
4042 qr.contextList.forEach((cl) => {
43+ if (!cl.set) return;
4144 if (!hierarchy.includes(cl.set)) {
4245 const nextHierarchy = [...hierarchy, cl.set];
4346 const nextLabelHierarchy = [...labelHierarchy, tree.label];
@@ -45,6 +48,8 @@ export class ContextMenu {
4548 cl.set.qrList.forEach(subQr => {
4649 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
4750 tree.children.push(new MenuItem(
51+ subTree.icon,
52+ subTree.showLabel,
4853 subTree.label,
4954 subTree.message,
5055 (evt) => {
public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js+1 -1
@@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js';
22
33export class MenuHeader extends MenuItem {
44 constructor(/**@type {String}*/label) {
55 super(null, null, label, null, null);
66 }
77
88
public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js+34 -7
@@ -1,21 +1,34 @@
11import { SubMenu } from './SubMenu.js';
22
33export class MenuItem {
44 /**@type {Stringstring}*/ labelicon;
55 /**@type {Objectboolean}*/ valueshowLabel;
66 /**@type {Functionstring}*/ callbacklabel;
7+ /**@type {object}*/ value;
8+ /**@type {function}*/ callback;
79 /**@type {MenuItem[]}*/ childList = [];
810 /**@type {SubMenu}*/ subMenu;
911 /**@type {Booleanboolean}*/ isForceExpanded = false;
1012
1113 /**@type {HTMLElement}*/ root;
1214
1315 /**@type {Functionfunction}*/ onExpand;
1416
1517
1618
1719
18- constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) {
20+ /**
21+ *
22+ * @param {string} icon
23+ * @param {boolean} showLabel
24+ * @param {string} label
25+ * @param {object} value
26+ * @param {function} callback
27+ * @param {MenuItem[]} children
28+ */
29+ constructor(icon, showLabel, label, value, callback, children = []) {
30+ this.icon = icon;
31+ this.showLabel = showLabel;
1932 this.label = label;
2033 this.value = value;
2134 this.callback = callback;
@@ -33,7 +46,21 @@ export class MenuItem {
3346 if (this.callback) {
3447 item.addEventListener('click', (evt) => this.callback(evt, this));
3548 }
36- item.append(this.label);
49+ const icon = document.createElement('div'); {
50+ this.domIcon = icon;
51+ icon.classList.add('qr--button-icon');
52+ icon.classList.add('fa-solid');
53+ if (!this.icon) icon.classList.add('qr--hidden');
54+ else icon.classList.add(this.icon);
55+ item.append(icon);
56+ }
57+ const lbl = document.createElement('div'); {
58+ this.domLabel = lbl;
59+ lbl.classList.add('qr--button-label');
60+ if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
61+ lbl.textContent = this.label;
62+ item.append(lbl);
63+ }
3764 if (this.childList.length > 0) {
3865 item.classList.add('ctx-has-children');
3966 const sub = new SubMenu(this.childList);
public/scripts/extensions/quick-reply/style.css+604 -20
@@ -1,3 +1,20 @@
1+@keyframes qr--success {
2+ 0%,
3+ 100% {
4+ color: var(--SmartThemeBodyColor);
5+ }
6+ 25%,
7+ 75% {
8+ color: #51a351;
9+ }
10+}
11+.qr--success {
12+ animation-name: qr--success;
13+ animation-duration: 3s;
14+ animation-timing-function: linear;
15+ animation-delay: 0s;
16+ animation-iteration-count: 1;
17+}
118#qr--bar {
219 outline: none;
320 margin: 0;
@@ -10,7 +27,6 @@
1027 max-width: 100%;
1128 overflow-x: auto;
1229 order: 1;
13- padding-right: 2.5em;
1430 position: relative;
1531}
1632#qr--bar > #qr--popoutTrigger {
@@ -18,6 +34,9 @@
1834 right: 0.25em;
1935 top: 0;
2036}
37+#qr--bar.popoutVisible {
38+ padding-right: 2.5em;
39+}
2140#qr--popout {
2241 display: flex;
2342 flex-direction: column;
@@ -41,6 +60,7 @@
4160}
4261#qr--bar > .qr--buttons,
4362#qr--popout > .qr--body > .qr--buttons {
63+ --qr--color: transparent;
4464 margin: 0;
4565 padding: 0;
4666 display: flex;
@@ -49,10 +69,44 @@
4969 gap: 5px;
5070 width: 100%;
5171}
72+#qr--bar > .qr--buttons.qr--color,
73+#qr--popout > .qr--body > .qr--buttons.qr--color {
74+ background-color: var(--qr--color);
75+}
76+#qr--bar > .qr--buttons.qr--borderColor,
77+#qr--popout > .qr--body > .qr--buttons.qr--borderColor {
78+ background-color: transparent;
79+ border-left: 5px solid var(--qr--color);
80+ border-right: 5px solid var(--qr--color);
81+}
82+#qr--bar > .qr--buttons:has(.qr--buttons.qr--color),
83+#qr--popout > .qr--body > .qr--buttons:has(.qr--buttons.qr--color) {
84+ margin: 5px;
85+}
5286#qr--bar > .qr--buttons > .qr--buttons,
5387#qr--popout > .qr--body > .qr--buttons > .qr--buttons {
5488 display: contents;
5589}
90+#qr--bar > .qr--buttons > .qr--buttons.qr--color .qr--button:before,
91+#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color .qr--button:before {
92+ content: '';
93+ background-color: var(--qr--color);
94+ position: absolute;
95+ inset: -5px;
96+ z-index: -1;
97+}
98+#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before,
99+#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before {
100+ display: none;
101+}
102+#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
103+#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
104+#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after,
105+#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after {
106+ content: '';
107+ width: 5px;
108+ background-color: var(--qr--color);
109+}
56110#qr--bar > .qr--buttons .qr--button,
57111#qr--popout > .qr--body > .qr--buttons .qr--button {
58112 color: var(--SmartThemeBodyColor);
@@ -66,11 +120,19 @@
66120 align-items: center;
67121 justify-content: center;
68122 text-align: center;
123+ position: relative;
69124}
70125#qr--bar > .qr--buttons .qr--button:hover,
71126#qr--popout > .qr--body > .qr--buttons .qr--button:hover {
72127 opacitybackground-color: 1#4d4d4d;
73- filter: brightness(1.2);
128+}
129+#qr--bar > .qr--buttons .qr--button .qr--hidden,
130+#qr--popout > .qr--body > .qr--buttons .qr--button .qr--hidden {
131+ display: none;
132+}
133+#qr--bar > .qr--buttons .qr--button .qr--button-icon,
134+#qr--popout > .qr--body > .qr--buttons .qr--button .qr--button-icon {
135+ margin: 0 0.5em;
74136}
75137#qr--bar > .qr--buttons .qr--button > .qr--button-expander,
76138#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {
@@ -170,36 +232,80 @@
170232#qr--settings #qr--set-qrList .qr--set-qrListContents {
171233 padding: 0 0.5em;
172234}
173235#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder {
236+ display: flex;
237+ align-items: center;
238+ opacity: 0;
239+ transition: 100ms;
240+ margin: -2px 0 -11px 0;
241+ position: relative;
242+}
243+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions {
244+ display: flex;
245+ gap: 0.25em;
246+ flex: 0 0 auto;
247+}
248+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions .qr--action {
249+ margin: 0;
250+}
251+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:before,
252+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:after {
253+ content: "";
254+ display: block;
255+ flex: 1 1 auto;
256+ border: 1px solid;
257+ margin: 0 1em;
258+ height: 0;
259+}
260+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:hover,
261+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:focus-within {
262+ opacity: 1;
263+}
264+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content {
174265 display: flex;
175266 flex-direction: row;
176267 gap: 0.5em;
177268 align-items: baseline;
178269 padding: 0.25em 0;
179270}
180271#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(12) {
181272 flex: 0 0 auto;
182273}
183274#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
184275 flex: 1 1 25%;
185276}
186277#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) {
187278 flex: 0 0 auto;
188279}
189280#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) {
190281 flex: 1 1 75%;
191282}
192283#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) {
193284 flex: 0 01 auto;
285+ display: flex;
286+ gap: 0.25em;
287+ justify-content: flex-end;
288+ flex-wrap: wrap;
194289}
195290#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle {
196291 padding: 0.75em;
197292}
198293#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,itemLabelContainer {
199-#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {
294+ display: flex;
295+ align-items: center;
296+ gap: 0.5em;
297+}
298+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) {
299+ display: none;
300+}
301+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemLabel {
302+ min-width: 24px;
303+}
304+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,
305+#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--action {
200306 margin: 0;
201307}
202308#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage {
203309 font-size: smaller;
204310}
205311#qr--settings .qr--set-qrListActions {
@@ -212,6 +318,7 @@
212318#qr--qrOptions {
213319 display: flex;
214320 flex-direction: column;
321+ padding-right: 1px;
215322}
216323#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
217324 display: flex;
@@ -219,6 +326,12 @@
219326 gap: 0.5em;
220327 align-items: baseline;
221328}
329+#qr--qrOptions > #qr--autoExec .checkbox_label {
330+ text-wrap: nowrap;
331+}
332+#qr--qrOptions > #qr--autoExec .checkbox_label .fa-fw {
333+ margin-right: 2px;
334+}
222335@media screen and (max-width: 750px) {
223336 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
224337 flex-direction: column;
@@ -238,6 +351,72 @@
238351.popup:has(#qr--modalEditor) {
239352 aspect-ratio: unset;
240353}
354+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {
355+ min-width: unset;
356+ min-height: unset;
357+ height: auto !important;
358+ width: min-content !important;
359+ position: absolute;
360+ right: 1em;
361+ top: 1em;
362+ left: unset;
363+ bottom: unset;
364+ margin: unset;
365+ padding: 0;
366+}
367+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized)::backdrop {
368+ backdrop-filter: unset;
369+ background-color: transparent;
370+}
371+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-body {
372+ flex: 0 0 auto;
373+ height: min-content;
374+ width: min-content;
375+}
376+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content {
377+ flex: 0 0 auto;
378+ margin-top: 0;
379+}
380+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor {
381+ max-height: 50vh;
382+}
383+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--main,
384+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--resizeHandle,
385+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > h3,
386+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeButtons,
387+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeProgress {
388+ display: none;
389+}
390+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--qrOptions {
391+ width: auto;
392+}
393+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
394+ display: flex;
395+}
396+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
397+ display: none;
398+}
399+.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugState {
400+ padding-top: 0;
401+}
402+.popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls {
403+ display: none;
404+}
405+.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight {
406+ position: absolute;
407+ z-index: 50000;
408+ pointer-events: none;
409+ background-color: rgba(47, 150, 180, 0.5);
410+}
411+.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight.qr--unresolved {
412+ background-color: rgba(255, 255, 0, 0.5);
413+}
414+.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary {
415+ position: absolute;
416+ z-index: 50000;
417+ pointer-events: none;
418+ border: 3px solid red;
419+}
241420.popup:has(#qr--modalEditor) .popup-content {
242421 display: flex;
243422 flex-direction: column;
@@ -249,6 +428,67 @@
249428 gap: 1em;
250429 overflow: hidden;
251430}
431+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > h3:first-child,
432+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--labels,
433+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
434+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > h3:first-child,
435+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > #qr--ctxEditor,
436+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions,
437+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3,
438+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3 + div {
439+ display: none;
440+}
441+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
442+ visibility: hidden;
443+}
444+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons {
445+ display: flex;
446+}
447+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
448+ cursor: not-allowed;
449+ opacity: 0.5;
450+ pointer-events: none;
451+ transition: 200ms;
452+ border-color: transparent;
453+}
454+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
455+ cursor: pointer;
456+ opacity: 1;
457+ pointer-events: all;
458+}
459+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume {
460+ animation-name: qr--debugPulse;
461+ animation-duration: 1500ms;
462+ animation-timing-function: ease-in-out;
463+ animation-delay: 0s;
464+ animation-iteration-count: infinite;
465+}
466+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume {
467+ border-color: #51a351;
468+}
469+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-step {
470+ border-color: var(--SmartThemeQuoteColor);
471+}
472+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepInto {
473+ border-color: var(--SmartThemeQuoteColor);
474+}
475+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepOut {
476+ border-color: var(--SmartThemeQuoteColor);
477+}
478+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle {
479+ width: 6px;
480+ background-color: var(--SmartThemeBorderColor);
481+ border: 2px solid var(--SmartThemeBlurTintColor);
482+ transition: border-color 200ms, background-color 200ms;
483+ cursor: w-resize;
484+}
485+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle:hover {
486+ background-color: var(--SmartThemeQuoteColor);
487+ border-color: var(--SmartThemeQuoteColor);
488+}
489+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions {
490+ width: var(--width, auto);
491+}
252492.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
253493 flex: 1 1 auto;
254494 display: flex;
@@ -260,21 +500,115 @@
260500 display: flex;
261501 flex-direction: row;
262502 gap: 0.5em;
503+ padding: 1px;
263504}
264505.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {,
506+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label {
265507 flex: 1 1 1px;
266508 display: flex;
267509 flex-direction: column;
510+ position: relative;
268511}
269512.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {fit,
513+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label.qr--fit {
514+ flex: 0 0 auto;
515+ justify-content: center;
516+}
517+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup,
518+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup {
519+ display: flex;
520+ align-items: baseline;
521+ gap: 0.5em;
522+}
523+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup input,
524+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup input {
270525 flex: 1 1 auto;
271526}
272527.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {labelText,
528+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelText {
273529 flex: 1 1 auto;
274530}
275531.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {.qr--labelHint,
532+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelHint {
533+ flex: 1 1 auto;
534+}
535+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label input,
536+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label input {
276537 flex: 0 0 auto;
277538}
539+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList,
540+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList {
541+ background-color: var(--stcdx--bgColor);
542+ border: 1px solid var(--SmartThemeBorderColor);
543+ backdrop-filter: blur(var(--SmartThemeBlurStrength));
544+ border-radius: 10px;
545+ font-size: smaller;
546+ position: absolute;
547+ top: 100%;
548+ left: 0;
549+ right: 0;
550+ overflow: auto;
551+ margin: 0;
552+ padding: 0.5em;
553+ max-height: 50vh;
554+ list-style: none;
555+ z-index: 40000;
556+ max-width: 100%;
557+}
558+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem,
559+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem {
560+ display: flex;
561+ gap: 1em;
562+ text-align: left;
563+ opacity: 0.75;
564+ transition: 200ms;
565+ cursor: pointer;
566+}
567+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem:hover,
568+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem:hover {
569+ opacity: 1;
570+}
571+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current,
572+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current {
573+ opacity: 1;
574+}
575+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
576+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
577+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id,
578+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id {
579+ font-weight: bold;
580+}
581+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label,
582+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label {
583+ white-space: nowrap;
584+}
585+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label .menu_button,
586+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label .menu_button {
587+ display: inline-block;
588+ height: min-content;
589+ width: min-content;
590+ margin: 0 0.5em 0 0;
591+}
592+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id,
593+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id {
594+ opacity: 0.5;
595+}
596+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:before,
597+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:before {
598+ content: "[";
599+}
600+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:after,
601+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:after {
602+ content: "]";
603+}
604+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--message,
605+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--message {
606+ height: 1lh;
607+ overflow: hidden;
608+ text-overflow: ellipsis;
609+ white-space: nowrap;
610+ opacity: 0.5;
611+}
278612.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
279613 flex: 1 1 auto;
280614 display: flex;
@@ -283,8 +617,9 @@
283617}
284618.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
285619 display: flex;
620+ flex-wrap: wrap;
286621 flex-direction: row;
287622 column-gap: 1em;
288623 color: var(--grey70);
289624 font-size: smaller;
290625 align-items: baseline;
@@ -308,6 +643,11 @@
308643 background-color: var(--ac-style-color-background);
309644 color: var(--ac-style-color-text);
310645}
646+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar,
647+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar-thumb {
648+ visibility: visible;
649+ cursor: unset;
650+}
311651.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
312652 color: unset;
313653 background-color: rgba(108 171 251 / 0.25);
@@ -357,11 +697,15 @@
357697 font-family: var(--monoFontFamily);
358698 padding: 0.75em;
359699 margin: 0;
360- border: none;
361700 resize: none;
362701 line-height: 1.2;
363702 border: 1px solid var(--SmartThemeBorderColor);
364703 border-radius: 5px;
704+ position: relative;
705+}
706+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-icon {
707+ height: 100%;
708+ aspect-ratio: 1 / 1;
365709}
366710.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {
367711 display: flex;
@@ -410,6 +754,46 @@
410754.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
411755 border-color: #d78872;
412756}
757+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons {
758+ display: none;
759+ gap: 1em;
760+}
761+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton {
762+ aspect-ratio: 1.25 / 1;
763+ width: 2.25em;
764+ position: relative;
765+}
766+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid) {
767+ border-width: 1px;
768+ border-style: solid;
769+}
770+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid):after {
771+ content: '';
772+ position: absolute;
773+ inset: 3px;
774+ background-color: var(--SmartThemeBodyColor);
775+ mask-size: contain;
776+ mask-position: center;
777+ mask-repeat: no-repeat;
778+}
779+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-resume:after {
780+ mask-image: url('/img/step-resume.svg');
781+}
782+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-step:after {
783+ mask-image: url('/img/step-over.svg');
784+}
785+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepInto:after {
786+ mask-image: url('/img/step-into.svg');
787+}
788+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepOut:after {
789+ mask-image: url('/img/step-out.svg');
790+}
791+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
792+ display: none;
793+}
794+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-send_textarea {
795+ flex: 0 0 auto;
796+}
413797.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {
414798 --prog: 0;
415799 --progColor: #92befc;
@@ -417,6 +801,7 @@
417801 --progSuccessColor: #51a351;
418802 --progErrorColor: #bd362f;
419803 --progAbortedColor: #d78872;
804+ flex: 0 0 auto;
420805 height: 0.5em;
421806 background-color: var(--black50a);
422807 position: relative;
@@ -469,6 +854,7 @@
469854 overflow: auto;
470855 min-width: 100%;
471856 width: 0;
857+ white-space: pre-wrap;
472858}
473859.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
474860 display: block;
@@ -476,6 +862,150 @@
476862.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {
477863 content: 'Result: ';
478864}
865+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState {
866+ display: none;
867+ text-align: left;
868+ font-size: smaller;
869+ font-family: var(--monoFontFamily);
870+ color: white;
871+ padding: 0.5em 0;
872+ overflow: auto;
873+ min-width: 100%;
874+ width: 0;
875+ white-space: pre-wrap;
876+}
877+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState.qr--active {
878+ display: block;
879+}
880+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope {
881+ display: grid;
882+ grid-template-columns: 0fr 1fr 1fr;
883+ column-gap: 0em;
884+}
885+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title {
886+ grid-column: 1 / 4;
887+ font-weight: bold;
888+ font-family: var(--mainFontFamily);
889+ background-color: var(--black50a);
890+ padding: 0.25em;
891+ margin-top: 0.5em;
892+}
893+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var,
894+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro,
895+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe {
896+ display: contents;
897+}
898+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--key,
899+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--key,
900+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--key,
901+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val,
902+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val,
903+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val {
904+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
905+}
906+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:nth-child(2n),
907+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:nth-child(2n),
908+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:nth-child(2n) {
909+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
910+}
911+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:hover,
912+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:hover,
913+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:hover {
914+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
915+}
916+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:nth-child(2n),
917+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:nth-child(2n),
918+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:nth-child(2n) {
919+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
920+}
921+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:hover,
922+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:hover,
923+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:hover {
924+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
925+}
926+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--key,
927+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key,
928+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key,
929+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--val,
930+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--val,
931+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--val {
932+ opacity: 0.5;
933+}
934+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val,
935+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val,
936+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val {
937+ grid-column: 2 / 4;
938+}
939+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--singleCol,
940+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--singleCol,
941+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--singleCol {
942+ grid-column: unset;
943+}
944+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:before,
945+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:before,
946+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:before,
947+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:after,
948+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:after,
949+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:after {
950+ content: '"';
951+ color: var(--SmartThemeQuoteColor);
952+}
953+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--unresolved:after,
954+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--unresolved:after,
955+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--unresolved:after {
956+ content: '-UNRESOLVED-';
957+ font-style: italic;
958+ color: var(--SmartThemeQuoteColor);
959+}
960+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key {
961+ margin-left: 0.5em;
962+ padding-right: 1em;
963+}
964+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key:after {
965+ content: ": ";
966+}
967+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:before,
968+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:before {
969+ content: "{{";
970+}
971+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:after,
972+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:after {
973+ content: "}}: ";
974+}
975+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope {
976+ display: contents;
977+}
978+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--key,
979+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val {
980+ opacity: 0.5;
981+}
982+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack {
983+ display: grid;
984+ grid-template-columns: 1fr 0fr;
985+}
986+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title {
987+ grid-column: 1 / 3;
988+ font-weight: bold;
989+ font-family: var(--mainFontFamily);
990+ background-color: var(--black50a);
991+ padding: 0.25em;
992+ margin-top: 1em;
993+}
994+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item {
995+ display: contents;
996+}
997+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--name,
998+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--source {
999+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
1000+}
1001+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--name {
1002+ margin-left: 0.5em;
1003+}
1004+.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source {
1005+ opacity: 0.5;
1006+ text-align: right;
1007+ white-space: nowrap;
1008+}
4791009@keyframes qr--progressPulse {
4801010 0%,
4811011 100% {
@@ -485,9 +1015,63 @@
4851015 background-color: var(--progFlashColor);
4861016 }
4871017}
1018+@keyframes qr--debugPulse {
1019+ 0%,
1020+ 100% {
1021+ border-color: #51a351;
1022+ }
1023+ 50% {
1024+ border-color: #92befc;
1025+ }
1026+}
4881027.popup.qr--hide {
4891028 opacity: 0 !important;
4901029}
4911030.popup.qr--hide::backdrop {
4921031 opacity: 0 !important;
4931032}
1033+.popup.qr--hide::backdrop {
1034+ opacity: 0 !important;
1035+}
1036+.popup:has(.qr--transferModal) .popup-button-ok {
1037+ display: flex;
1038+ align-items: center;
1039+ flex-direction: column;
1040+ white-space: pre;
1041+ font-weight: normal;
1042+ box-shadow: 0 0 0;
1043+ transition: 200ms;
1044+}
1045+.popup:has(.qr--transferModal) .popup-button-ok:after {
1046+ content: 'Transfer';
1047+ height: 0;
1048+ overflow: hidden;
1049+ font-weight: bold;
1050+}
1051+.popup:has(.qr--transferModal) .qr--copy {
1052+ display: flex;
1053+ align-items: center;
1054+ flex-direction: column;
1055+ white-space: pre;
1056+ font-weight: normal;
1057+ box-shadow: 0 0 0;
1058+ transition: 200ms;
1059+}
1060+.popup:has(.qr--transferModal) .qr--copy:after {
1061+ content: 'Copy';
1062+ height: 0;
1063+ overflow: hidden;
1064+ font-weight: bold;
1065+}
1066+.popup:has(.qr--transferModal):has(.qr--transferSelect:focus) .popup-button-ok {
1067+ font-weight: bold;
1068+ box-shadow: 0 0 10px;
1069+}
1070+.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .popup-button-ok {
1071+ font-weight: normal;
1072+ box-shadow: 0 0 0;
1073+}
1074+.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .qr--copy {
1075+ font-weight: bold;
1076+ box-shadow: 0 0 10px;
1077+}
public/scripts/extensions/quick-reply/style.less+666 -126
@@ -1,3 +1,18 @@
1+@keyframes qr--success {
2+ 0%, 100% {
3+ color: var(--SmartThemeBodyColor);
4+ }
5+ 25%, 75% {
6+ color: rgb(81, 163, 81);
7+ }
8+}
9+&.qr--success {
10+ animation-name: qr--success;
11+ animation-duration: 3s;
12+ animation-timing-function: linear;
13+ animation-delay: 0s;
14+ animation-iteration-count: 1;
15+}
116#qr--bar {
217 outline: none;
318 margin: 0;
@@ -10,7 +25,6 @@
1025 max-width: 100%;
1126 overflow-x: auto;
1227 order: 1;
13- padding-right: 2.5em;
1428 position: relative;
1529
1630 >#qr--popoutTrigger {
@@ -19,6 +33,9 @@
1933 top: 0;
2034 }
2135}
36+#qr--bar.popoutVisible {
37+ padding-right: 2.5em;
38+}
2239
2340#qr--popout {
2441 display: flex;
@@ -50,6 +67,18 @@
5067#qr--bar,
5168#qr--popout>.qr--body {
5269 >.qr--buttons {
70+ --qr--color: transparent;
71+ &.qr--color {
72+ background-color: var(--qr--color);
73+ }
74+ &.qr--borderColor {
75+ background-color: transparent;
76+ border-left: 5px solid var(--qr--color);
77+ border-right: 5px solid var(--qr--color);
78+ }
79+ &:has(.qr--buttons.qr--color) {
80+ margin: 5px;
81+ }
5382 margin: 0;
5483 padding: 0;
5584 display: flex;
@@ -60,6 +89,25 @@
6089
6190 >.qr--buttons {
6291 display: contents;
92+ &.qr--color {
93+ .qr--button:before {
94+ content: '';
95+ background-color: var(--qr--color);
96+ position: absolute;
97+ inset: -5px;
98+ z-index: -1;
99+ }
100+ &.qr--borderColor {
101+ .qr--button:before {
102+ display: none;
103+ }
104+ &:before, &:after {
105+ content: '';
106+ width: 5px;
107+ background-color: var(--qr--color);
108+ }
109+ }
110+ }
63111 }
64112
65113 .qr--button {
@@ -75,10 +123,17 @@
75123 align-items: center;
76124 justify-content: center;
77125 text-align: center;
126+ position: relative;
78127
79128 &:hover {
80- opacity: 1;
129+ background-color: rgb(30% 30% 30%);
81- filter: brightness(1.2);
130+ }
131+
132+ .qr--hidden {
133+ display: none;
134+ }
135+ .qr--button-icon {
136+ margin: 0 0.5em;
82137 }
83138
84139 >.qr--button-expander {
@@ -211,14 +266,41 @@
211266 .qr--set-qrListContents> {
212267 padding: 0 0.5em;
213268
214269 >.qr--set-item .qr--set-itemAdder {
270+ display: flex;
271+ align-items: center;
272+ opacity: 0;
273+ transition: 100ms;
274+ margin: -2px 0 -11px 0;
275+ position: relative;
276+ .qr--actions {
277+ display: flex;
278+ gap: 0.25em;
279+ flex: 0 0 auto;
280+ .qr--action {
281+ margin: 0;
282+ }
283+ }
284+ &:before, &:after {
285+ content: "";
286+ display: block;
287+ flex: 1 1 auto;
288+ border: 1px solid;
289+ margin: 0 1em;
290+ height: 0;
291+ }
292+ &:hover, &:focus-within {
293+ opacity: 1;
294+ }
295+ }
296+ >.qr--set-item .qr--content {
215297 display: flex;
216298 flex-direction: row;
217299 gap: 0.5em;
218300 align-items: baseline;
219301 padding: 0.25em 0;
220302
221303 > :nth-child(12) {
222304 flex: 0 0 auto;
223305 }
224306
@@ -235,13 +317,29 @@
235317 }
236318
237319 > :nth-child(5) {
238320 flex: 0 01 auto;
321+ display: flex;
322+ gap: 0.25em;
323+ justify-content: flex-end;
324+ flex-wrap: wrap;
239325 }
240326
241327 >.drag-handle {
242328 padding: 0.75em;
243329 }
244330
331+ .qr--set-itemLabelContainer {
332+ display: flex;
333+ align-items: center;
334+ gap: 0.5em;
335+ .qr--set-itemIcon:not(.fa-solid) {
336+ display: none;
337+ }
338+ .qr--set-itemLabel {
339+ min-width: 24px;
340+ }
341+ }
342+
245343 .qr--set-itemLabel,
246344 .qr--action {
247345 margin: 0;
@@ -251,6 +349,8 @@
251349 font-size: smaller;
252350 }
253351 }
352+
353+
254354 }
255355 }
256356
@@ -270,6 +370,7 @@
270370#qr--qrOptions {
271371 display: flex;
272372 flex-direction: column;
373+ padding-right: 1px;
273374
274375 >#qr--ctxEditor {
275376 .qr--ctxItem {
@@ -279,6 +380,15 @@
279380 align-items: baseline;
280381 }
281382 }
383+ >#qr--autoExec {
384+ .checkbox_label {
385+ text-wrap: nowrap;
386+
387+ .fa-fw {
388+ margin-right: 2px;
389+ }
390+ }
391+ }
282392}
283393
284394
@@ -306,6 +416,78 @@
306416.popup:has(#qr--modalEditor) {
307417 aspect-ratio: unset;
308418
419+ &:has(.qr--isExecuting.qr--minimized) {
420+ min-width: unset;
421+ min-height: unset;
422+ height: auto !important;
423+ width: min-content !important;
424+ position: absolute;
425+ right: 1em;
426+ top: 1em;
427+ left: unset;
428+ bottom: unset;
429+ margin: unset;
430+ padding: 0;
431+ &::backdrop {
432+ backdrop-filter: unset;
433+ background-color: transparent;
434+ }
435+ .popup-body {
436+ flex: 0 0 auto;
437+ height: min-content;
438+ width: min-content;
439+ }
440+ .popup-content {
441+ flex: 0 0 auto;
442+ margin-top: 0;
443+
444+ > #qr--modalEditor {
445+ max-height: 50vh;
446+ > #qr--main,
447+ > #qr--resizeHandle,
448+ > #qr--qrOptions > h3,
449+ > #qr--qrOptions > #qr--modal-executeButtons,
450+ > #qr--qrOptions > #qr--modal-executeProgress
451+ {
452+ display: none;
453+ }
454+ #qr--qrOptions {
455+ width: auto;
456+ }
457+ #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
458+ display: flex;
459+ }
460+ #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
461+ display: none;
462+ }
463+ #qr--modal-debugState {
464+ padding-top: 0;
465+ }
466+ }
467+ }
468+ }
469+ &:has(.qr--isExecuting) {
470+ .popup-controls {
471+ display: none;
472+ }
473+
474+ .qr--highlight {
475+ position: absolute;
476+ z-index: 50000;
477+ pointer-events: none;
478+ background-color: rgb(47 150 180 / 0.5);
479+ &.qr--unresolved {
480+ background-color: rgb(255 255 0 / 0.5);
481+ }
482+ }
483+ .qr--highlight-secondary {
484+ position: absolute;
485+ z-index: 50000;
486+ pointer-events: none;
487+ border: 3px solid red;
488+ }
489+ }
490+
309491 .popup-content {
310492 display: flex;
311493 flex-direction: column;
@@ -317,140 +499,262 @@
317499 gap: 1em;
318500 overflow: hidden;
319501
320502 ># &.qr--mainisExecuting {
321- flex: 1 1 auto;
503+ #qr--main > h3:first-child,
322- display: flex;
504+ #qr--main > .qr--labels,
323- flex-direction: column;
505+ #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
324- overflow: hidden;
506+ #qr--qrOptions > h3:first-child,
325-
507+ #qr--qrOptions > #qr--ctxEditor,
326- >.qr--labels {
508+ #qr--qrOptions > .qr--ctxEditorActions,
327- flex: 0 0 auto;
509+ #qr--qrOptions > .qr--ctxEditorActions + h3,
328- display: flex;
510+ #qr--qrOptions > .qr--ctxEditorActions + h3 + div
329- flex-direction: row;
511+ {
330- gap: 0.5em;
512+ display: none;
331-
513+ }
332- >label {
514+ #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
333- flex: 1 1 1px;
515+ visibility: hidden;
334- display: flex;
516+ }
335- flex-direction: column;
517+ #qr--modal-debugButtons {
336-
518+ display: flex;
337- >.qr--labelText {
519+ .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
338- flex: 1 1 auto;
520+ cursor: not-allowed;
521+ opacity: 0.5;
522+ pointer-events: none;
523+ transition: 200ms;
524+ border-color: transparent;
525+ }
526+ }
527+ &.qr--isPaused #qr--modal-debugButtons {
528+ .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
529+ cursor: pointer;
530+ opacity: 1;
531+ pointer-events: all;
532+ &#qr--modal-resume {
533+ animation-name: qr--debugPulse;
534+ animation-duration: 1500ms;
535+ animation-timing-function: ease-in-out;
536+ animation-delay: 0s;
537+ animation-iteration-count: infinite;
339538 }
340-
539+ &#qr--modal-resume {
341- >.qr--labelHint {
540+ border-color: rgb(81, 163, 81);
342- flex: 1 1 auto;
343541 }
344-
542+ &#qr--modal-step {
345- >input {
543+ border-color: var(--SmartThemeQuoteColor);
346- flex: 0 0 auto;
544+ }
545+ &#qr--modal-stepInto {
546+ border-color: var(--SmartThemeQuoteColor);
547+ }
548+ &#qr--modal-stepOut {
549+ border-color: var(--SmartThemeQuoteColor);
347550 }
348551 }
349552 }
350-
553+ #qr--resizeHandle {
351- >.qr--modal-messageContainer {
554+ width: 6px;
352- flex: 1 1 auto;
555+ background-color: var(--SmartThemeBorderColor);
353- display: flex;
556+ border: 2px solid var(--SmartThemeBlurTintColor);
354- flex-direction: column;
557+ transition: border-color 200ms, background-color 200ms;
355- overflow: hidden;
558+ cursor: w-resize;
356-
559+ &:hover {
357- >.qr--modal-editorSettings {
560+ background-color: var(--SmartThemeQuoteColor);
358- display: flex;
561+ border-color: var(--SmartThemeQuoteColor);
359- flex-direction: row;
562+ }
360- gap: 1em;
563+ }
361- color: var(--grey70);
564+ #qr--qrOptions {
362- font-size: smaller;
565+ width: var(--width, auto);
363- align-items: baseline;
566+ }
364-
567+ }
365- >.checkbox_label {
568+
366- white-space: nowrap;
569+ > #qr--main {
367-
570+ flex: 1 1 auto;
368- >input {
571+ display: flex;
369572 font flex-sizedirection: inheritcolumn;
370- }
573+ overflow: hidden;
574+ > .qr--labels {
575+ flex: 0 0 auto;
576+ display: flex;
577+ flex-direction: row;
578+ gap: 0.5em;
579+ padding: 1px;
580+ > label, > .label {
581+ flex: 1 1 1px;
582+ display: flex;
583+ flex-direction: column;
584+ position: relative;
585+ &.qr--fit {
586+ flex: 0 0 auto;
587+ justify-content: center;
371588 }
372- }
589+ .qr--inputGroup {
373-
590+ display: flex;
374- >#qr--modal-messageHolder {
591+ align-items: baseline;
375- flex: 1 1 auto;
592+ gap: 0.5em;
376- display: grid;
593+ input {
377- text-align: left;
594+ flex: 1 1 auto;
378- overflow: hidden;
379-
380- &.qr--noSyntax {
381- >#qr--modal-messageSyntax {
382- display: none;
383595 }
384-
596+ }
385597 ># .qr--modal-messagelabelText {
386- background-color: var(--ac-style-color-background);
598+ flex: 1 1 auto;
387- color: var(--ac-style-color-text);
599+ }
388-
600+ .qr--labelHint {
389- &::selection {
601+ flex: 1 1 auto;
390- color: unset;
602+ }
391- background-color: rgba(108 171 251 / 0.25);
603+ input {
392-
604+ flex: 0 0 auto;
393- @supports (color: rgb(from white r g b / 0.25)) {
605+ }
394- background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
606+ .qr--modal-switcherList {
607+ background-color: var(--stcdx--bgColor);
608+ border: 1px solid var(--SmartThemeBorderColor);
609+ backdrop-filter: blur(var(--SmartThemeBlurStrength));
610+ border-radius: 10px;
611+ font-size: smaller;
612+ position: absolute;
613+ top: 100%;
614+ left: 0;
615+ right: 0;
616+ overflow: auto;
617+ margin: 0;
618+ padding: 0.5em;
619+ max-height: 50vh;
620+ list-style: none;
621+ z-index: 40000;
622+ max-width: 100%;
623+ .qr--modal-switcherItem {
624+ display: flex;
625+ gap: 1em;
626+ text-align: left;
627+ opacity: 0.75;
628+ transition: 200ms;
629+ cursor: pointer;
630+ &:hover {
631+ opacity: 1;
632+ }
633+ &.qr--current {
634+ opacity: 1;
635+ .qr--label, .qr--id {
636+ font-weight: bold;
395637 }
396638 }
397639 }
398- }
640+ .qr--label {
399-
641+ white-space: nowrap;
400- >#qr--modal-messageSyntax {
642+ .menu_button {
401- grid-column: 1;
643+ display: inline-block;
402- grid-row: 1;
644+ height: min-content;
403- padding: 0;
645+ width: min-content;
404646 margin: 0 0.5em 0 0;
405- border: none;
647+ }
406- overflow: hidden;
407- min-width: 100%;
408- width: 0;
409-
410- >#qr--modal-messageSyntaxInner {
411- height: 100%;
412648 }
413- }
649+ .qr--id {
414-
650+ opacity: 0.5;
415- >#qr--modal-message {
651+ &:before { content: "["; }
416- background-color: transparent;
652+ &:after { content: "]"; }
417- color: transparent;
418- grid-column: 1;
419- grid-row: 1;
420- caret-color: var(--ac-style-color-text);
421- overflow: auto;
422-
423- &::-webkit-scrollbar,
424- &::-webkit-scrollbar-thumb {
425- visibility: hidden;
426- cursor: default;
427653 }
428-
654+ .qr--message {
429- &::selection {
655+ height: 1lh;
430656 coloroverflow: transparenthidden;
431- background-color: rgba(108 171 251 / 0.25);
657+ text-overflow: ellipsis;
432-
658+ white-space: nowrap;
433- @supports (color: rgb(from white r g b / 0.25)) {
659+ opacity: 0.5;
434- background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
435- }
436660 }
437661 }
438-
662+ }
439- #qr--modal-message,
663+ }
440664 # > .qr--modal-messageSyntaxInnermessageContainer {
441- font-family: var(--monoFontFamily);
665+ flex: 1 1 auto;
442- padding: 0.75em;
666+ display: flex;
443- margin: 0;
667+ flex-direction: column;
444668 border overflow: nonehidden;
445- resize: none;
669+ > .qr--modal-editorSettings {
446- line-height: 1.2;
670+ display: flex;
447- border: 1px solid var(--SmartThemeBorderColor);
671+ flex-wrap: wrap;
448672 border flex-radiusdirection: 5pxrow;
449- }
673+ column-gap: 1em;
450- }
674+ color: var(--grey70);
451- }
675+ font-size: smaller;
676+ align-items: baseline;
677+ > .checkbox_label {
678+ white-space: nowrap;
679+ > input {
680+ font-size: inherit;
681+ }
682+ }
683+ }
684+ > #qr--modal-messageHolder {
685+ flex: 1 1 auto;
686+ display: grid;
687+ text-align: left;
688+ overflow: hidden;
689+ &.qr--noSyntax {
690+ > #qr--modal-messageSyntax {
691+ display: none;
692+ }
693+ > #qr--modal-message {
694+ background-color: var(--ac-style-color-background);
695+ color: var(--ac-style-color-text);
696+ &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
697+ visibility: visible;
698+ cursor: unset;
699+ }
700+ &::selection {
701+ color: unset;
702+ background-color: rgba(108 171 251 / 0.25);
703+ @supports (color: rgb(from white r g b / 0.25)) {
704+ background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
705+ }
706+ }
707+ }
708+ }
709+ > #qr--modal-messageSyntax {
710+ grid-column: 1;
711+ grid-row: 1;
712+ padding: 0;
713+ margin: 0;
714+ border: none;
715+ overflow: hidden;
716+ min-width: 100%;
717+ width: 0;
718+ > #qr--modal-messageSyntaxInner {
719+ height: 100%;
720+ }
721+ }
722+ > #qr--modal-message {
723+ background-color: transparent;
724+ color: transparent;
725+ grid-column: 1;
726+ grid-row: 1;
727+ caret-color: var(--ac-style-color-text);
728+ overflow: auto;
729+ &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
730+ visibility: hidden;
731+ cursor: default;
732+ }
733+ &::selection {
734+ color: transparent;
735+ background-color: rgba(108 171 251 / 0.25);
736+ @supports (color: rgb(from white r g b / 0.25)) {
737+ background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
738+ }
739+ }
740+ }
741+ #qr--modal-message, #qr--modal-messageSyntaxInner {
742+ font-family: var(--monoFontFamily);
743+ padding: 0.75em;
744+ margin: 0;
745+ resize: none;
746+ line-height: 1.2;
747+ border: 1px solid var(--SmartThemeBorderColor);
748+ border-radius: 5px;
749+ position: relative;
750+ }
751+ }
752+ }
753+ }
754+ #qr--modal-icon {
755+ height: 100%;
756+ aspect-ratio: 1 / 1;
452757 }
453-
454758 #qr--modal-executeButtons {
455759 display: flex;
456760 gap: 1em;
@@ -510,6 +814,47 @@
510814 border-color: rgb(215, 136, 114);
511815 }
512816 }
817+ #qr--modal-debugButtons {
818+ display: none;
819+ gap: 1em;
820+ .qr--modal-debugButton {
821+ aspect-ratio: 1.25 / 1;
822+ width: 2.25em;
823+ position: relative;
824+ &:not(.fa-solid) {
825+ border-width: 1px;
826+ border-style: solid;
827+ &:after {
828+ content: '';
829+ position: absolute;
830+ inset: 3px;
831+ background-color: var(--SmartThemeBodyColor);
832+ mask-size: contain;
833+ mask-position: center;
834+ mask-repeat: no-repeat;
835+ }
836+ }
837+ &#qr--modal-resume:after {
838+ mask-image: url('/img/step-resume.svg');
839+ }
840+ &#qr--modal-step:after {
841+ mask-image: url('/img/step-over.svg');
842+ }
843+ &#qr--modal-stepInto:after {
844+ mask-image: url('/img/step-into.svg');
845+ }
846+ &#qr--modal-stepOut:after {
847+ mask-image: url('/img/step-out.svg');
848+ }
849+ &#qr--modal-maximize {
850+ display: none;
851+ }
852+ }
853+ }
854+
855+ #qr--modal-send_textarea {
856+ flex: 0 0 auto;
857+ }
513858
514859 #qr--modal-executeProgress {
515860 --prog: 0;
@@ -518,6 +863,7 @@
518863 --progSuccessColor: rgb(81, 163, 81);
519864 --progErrorColor: rgb(189, 54, 47);
520865 --progAbortedColor: rgb(215, 136, 114);
866+ flex: 0 0 auto;
521867 height: 0.5em;
522868 background-color: var(--black50a);
523869 position: relative;
@@ -588,6 +934,135 @@
588934 overflow: auto;
589935 min-width: 100%;
590936 width: 0;
937+ white-space: pre-wrap;
938+ }
939+ #qr--modal-debugState {
940+ display: none;
941+ &.qr--active {
942+ display: block;
943+ }
944+ text-align: left;
945+ font-size: smaller;
946+ font-family: var(--monoFontFamily);
947+ // background-color: rgb(146, 190, 252);
948+ color: white;
949+ padding: 0.5em 0;
950+ overflow: auto;
951+ min-width: 100%;
952+ width: 0;
953+ white-space: pre-wrap;
954+
955+ .qr--scope {
956+ display: grid;
957+ grid-template-columns: 0fr 1fr 1fr;
958+ column-gap: 0em;
959+ .qr--title {
960+ grid-column: 1 / 4;
961+ font-weight: bold;
962+ font-family: var(--mainFontFamily);
963+ background-color: var(--black50a);
964+ padding: 0.25em;
965+ margin-top: 0.5em;
966+ }
967+ .qr--var, .qr--macro, .qr--pipe {
968+ display: contents;
969+ &:nth-child(2n + 1) {
970+ .qr--key, .qr--val {
971+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
972+ }
973+ .qr--val {
974+ &:nth-child(2n) {
975+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
976+ }
977+ &:hover {
978+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
979+ }
980+ }
981+ }
982+ &:nth-child(2n) {
983+ .qr--val {
984+ &:nth-child(2n) {
985+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
986+ }
987+ &:hover {
988+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
989+ }
990+ }
991+ }
992+ &.qr--isHidden {
993+ .qr--key, .qr--val {
994+ opacity: 0.5;
995+ }
996+ }
997+ .qr--val {
998+ grid-column: 2 / 4;
999+ &.qr--singleCol {
1000+ grid-column: unset;
1001+ }
1002+ &.qr--simple {
1003+ &:before, &:after {
1004+ content: '"';
1005+ color: var(--SmartThemeQuoteColor);
1006+ }
1007+ }
1008+ &.qr--unresolved {
1009+ &:after {
1010+ content: '-UNRESOLVED-';
1011+ font-style: italic;
1012+ color: var(--SmartThemeQuoteColor);
1013+ }
1014+ }
1015+ }
1016+ }
1017+ .qr--key {
1018+ margin-left: 0.5em;
1019+ padding-right: 1em;
1020+ &:after { content: ": "; }
1021+ }
1022+ .qr--pipe, .qr--macro {
1023+ > .qr--key {
1024+ &:before { content: "{{"; }
1025+ &:after { content: "}}: "; }
1026+ }
1027+ }
1028+ .qr--scope {
1029+ display: contents;
1030+ .qr--pipe {
1031+ .qr--key, .qr--val {
1032+ opacity: 0.5;
1033+ }
1034+ }
1035+ }
1036+ }
1037+
1038+ .qr--stack {
1039+ display: grid;
1040+ grid-template-columns: 1fr 0fr;
1041+ .qr--title {
1042+ grid-column: 1 / 3;
1043+ font-weight: bold;
1044+ font-family: var(--mainFontFamily);
1045+ background-color: var(--black50a);
1046+ padding: 0.25em;
1047+ margin-top: 1em;
1048+ }
1049+ .qr--item {
1050+ display: contents;
1051+ &:nth-child(2n + 1) {
1052+ .qr--name, .qr--source {
1053+ background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
1054+ }
1055+ }
1056+ .qr--name {
1057+ margin-left: 0.5em;
1058+ }
1059+ .qr--source {
1060+ opacity: 0.5;
1061+ text-align: right;
1062+ white-space: nowrap;
1063+ }
1064+ }
1065+ }
5911066 }
5921067 }
5931068 }
@@ -605,10 +1080,75 @@
6051080 }
6061081}
6071082
1083+@keyframes qr--debugPulse {
1084+ 0%,
1085+ 100% {
1086+ border-color: rgb(81, 163, 81);
1087+ }
1088+
1089+ 50% {
1090+ border-color: rgb(146, 190, 252);
1091+ }
1092+}
1093+
6081094.popup.qr--hide {
6091095 opacity: 0 !important;
1096+ &::backdrop {
1097+ opacity: 0 !important;
1098+ }
6101099}
6111100
6121101.popup.qr--hide::backdrop {
6131102 opacity: 0 !important;
6141103}
1104+
1105+
1106+
1107+.popup:has(.qr--transferModal) {
1108+ .popup-button-ok {
1109+ &:after {
1110+ content: 'Transfer';
1111+ height: 0;
1112+ overflow: hidden;
1113+ font-weight: bold;
1114+ }
1115+ display: flex;
1116+ align-items: center;
1117+ flex-direction: column;
1118+ white-space: pre;
1119+ font-weight: normal;
1120+ box-shadow: 0 0 0;
1121+ transition: 200ms;
1122+ }
1123+ .qr--copy {
1124+ &:after {
1125+ content: 'Copy';
1126+ height: 0;
1127+ overflow: hidden;
1128+ font-weight: bold;
1129+ }
1130+ display: flex;
1131+ align-items: center;
1132+ flex-direction: column;
1133+ white-space: pre;
1134+ font-weight: normal;
1135+ box-shadow: 0 0 0;
1136+ transition: 200ms;
1137+ }
1138+ &:has(.qr--transferSelect:focus) {
1139+ .popup-button-ok {
1140+ font-weight: bold;
1141+ box-shadow: 0 0 10px;
1142+ }
1143+ &.qr--isCopy {
1144+ .popup-button-ok {
1145+ font-weight: normal;
1146+ box-shadow: 0 0 0;
1147+ }
1148+ .qr--copy {
1149+ font-weight: bold;
1150+ box-shadow: 0 0 10px;
1151+ }
1152+ }
1153+ }
1154+}
public/scripts/extensions/regex/editor.html+15 -22
@@ -54,12 +54,7 @@
5454 <small data-i18n="Replace With">Replace With</small>
5555 </label>
5656 <div>
57- <textarea
57+ <textarea class="regex_replace_string text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_replace_string_placeholder" placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups." rows="2"></textarea>
58- class="regex_replace_string text_pole wide100p textarea_compact"
59- data-i18n="[placeholder]ext_regex_replace_string_placeholder"
60- placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
61- rows="2"
62- ></textarea>
6358 </div>
6459 </div>
6560 <div class="flex1">
@@ -67,11 +62,7 @@
6762 <small data-i18n="Trim Out">Trim Out</small>
6863 </label>
6964 <div>
70- <textarea
65+ <textarea class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder" placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter." rows="3"></textarea>
71- class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder"
72- placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter."
73- rows="3"
74- ></textarea>
7566 </div>
7667 </div>
7768 </div>
@@ -126,17 +117,6 @@
126117 <input type="checkbox" name="disabled" />
127118 <span data-i18n="Disabled">Disabled</span>
128119 </label>
129- <label class="checkbox flex-container" title="Chat history won't change, only the message rendered in the UI.">
130- <input type="checkbox" name="only_format_display" />
131- <span data-i18n="Only Format Display">Only Format Display</span>
132- </label>
133- <label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_prompt_desc" title="Chat history won't change, only the prompt as the request is sent (on generation).">
134- <input type="checkbox" name="only_format_prompt"/>
135- <span>
136- <span data-i18n="Only Format Prompt (?)">Only Format Prompt</span>
137- <span class="fa-solid fa-circle-question note-link-span"></span>
138- </span>
139- </label>
140120 <label class="checkbox flex-container">
141121 <input type="checkbox" name="run_on_edit" />
142122 <span data-i18n="Run On Edit">Run On Edit</span>
@@ -148,6 +128,19 @@
148128 <span class="fa-solid fa-circle-question note-link-span"></span>
149129 </span>
150130 </label>
131+ <span>
132+ <small data-i18n="ext_regex_other_options" data-i18n="Ephemerality">Ephemerality</small>
133+ <span class="fa-solid fa-circle-question note-link-span" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
134+ </span>
135+ <label class="checkbox flex-container" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
136+ <input type="checkbox" name="only_format_display" />
137+ <span data-i18n="Only Format Display">Alter Chat Display</span>
138+ </label>
139+ <label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_prompt_desc" title="Chat history file contents won't change, but regex will be applied to the outgoing prompt before it is sent to the LLM.">
140+ <input type="checkbox" name="only_format_prompt" />
141+ <span data-i18n="Only Format Prompt (?)">Alter Outgoing Prompt</span>
142+ </label>
143+
151144 </div>
152145 </div>
153146 </div>
public/scripts/extensions/shared.js+89 -2
@@ -1,5 +1,5 @@
11import { getRequestHeaders } from '../../script.js';
22import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
33import { oai_settings } from '../openai.js';
44import { SECRET_KEYS, secret_state } from '../secrets.js';
55import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@@ -136,8 +136,12 @@ function throwIfInvalidModel(useReverseProxy) {
136136 throw new Error('Anthropic (Claude) API key is not set.');
137137 }
138138
139+ if (extension_settings.caption.multimodal_api === 'zerooneai' && !secret_state[SECRET_KEYS.ZEROONEAI]) {
140+ throw new Error('01.AI API key is not set.');
141+ }
142+
139143 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
140144 throw new Error('MakerSuiteGoogle AI Studio API key is not set.');
141145 }
142146
143147 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
@@ -172,3 +176,86 @@ function throwIfInvalidModel(useReverseProxy) {
172176 throw new Error('Custom API URL is not set.');
173177 }
174178}
179+
180+/**
181+ * Check if the WebLLM extension is installed and supported.
182+ * @returns {boolean} Whether the extension is installed and supported
183+ */
184+export function isWebLlmSupported() {
185+ if (!('gpu' in navigator)) {
186+ const warningKey = 'webllm_browser_warning_shown';
187+ if (!sessionStorage.getItem(warningKey)) {
188+ toastr.error('Your browser does not support the WebGPU API. Please use a different browser.', 'WebLLM', {
189+ preventDuplicates: true,
190+ timeOut: 0,
191+ extendedTimeOut: 0,
192+ });
193+ sessionStorage.setItem(warningKey, '1');
194+ }
195+ return false;
196+ }
197+
198+ if (!('llm' in SillyTavern)) {
199+ const warningKey = 'webllm_extension_warning_shown';
200+ if (!sessionStorage.getItem(warningKey)) {
201+ toastr.error('WebLLM extension is not installed. Click here to install it.', 'WebLLM', {
202+ timeOut: 0,
203+ extendedTimeOut: 0,
204+ preventDuplicates: true,
205+ onclick: () => openThirdPartyExtensionMenu('https://github.com/SillyTavern/Extension-WebLLM'),
206+ });
207+ sessionStorage.setItem(warningKey, '1');
208+ }
209+ return false;
210+ }
211+
212+ return true;
213+}
214+
215+/**
216+ * Generates text in response to a chat prompt using WebLLM.
217+ * @param {any[]} messages Messages to use for generating
218+ * @param {object} params Additional parameters
219+ * @returns {Promise<string>} Generated response
220+ */
221+export async function generateWebLlmChatPrompt(messages, params = {}) {
222+ if (!isWebLlmSupported()) {
223+ throw new Error('WebLLM extension is not installed.');
224+ }
225+
226+ console.debug('WebLLM chat completion request:', messages, params);
227+ const engine = SillyTavern.llm;
228+ const response = await engine.generateChatPrompt(messages, params);
229+ console.debug('WebLLM chat completion response:', response);
230+ return response;
231+}
232+
233+/**
234+ * Counts the number of tokens in the provided text using WebLLM's default model.
235+ * @param {string} text Text to count tokens in
236+ * @returns {Promise<number>} Number of tokens in the text
237+ */
238+export async function countWebLlmTokens(text) {
239+ if (!isWebLlmSupported()) {
240+ throw new Error('WebLLM extension is not installed.');
241+ }
242+
243+ const engine = SillyTavern.llm;
244+ const response = await engine.countTokens(text);
245+ return response;
246+}
247+
248+/**
249+ * Gets the size of the context in the WebLLM's default model.
250+ * @returns {Promise<number>} Size of the context in the WebLLM model
251+ */
252+export async function getWebLlmContextSize() {
253+ if (!isWebLlmSupported()) {
254+ throw new Error('WebLLM extension is not installed.');
255+ }
256+
257+ const engine = SillyTavern.llm;
258+ await engine.loadModel();
259+ const model = await engine.getCurrentModelInfo();
260+ return model?.context_size;
261+}
public/scripts/extensions/stable-diffusion/button.html+6 -2
@@ -1,4 +1,8 @@
11<div id="sd_gen" class="list-group-item flex-container flexGap5">
22 <div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion" /></div>
33 <span>Generate Image</span>
4+</div>
5+<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
6+ <div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task" data-i18n="[title]Abort current image generation task"></div>
7+ <span>Stop Image Generation</span>
48</div>
public/scripts/extensions/stable-diffusion/index.js+297 -68
@@ -30,13 +30,14 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
3030import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
3131import { debounce_timeout } from '../../constants.js';
3232import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
3333import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
3434export { MODULE_NAME };
3535
3636const MODULE_NAME = 'sd';
3737const UPDATE_INTERVAL = 1000;
3838// This is a 1x1 transparent PNG
3939const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
40+const CUSTOM_STOP_EVENT = 'sd_stop_generation';
4041
4142const sources = {
4243 extras: 'extras',
@@ -50,6 +51,8 @@ const sources = {
5051 drawthings: 'drawthings',
5152 pollinations: 'pollinations',
5253 stability: 'stability',
54+ blockentropy: 'blockentropy',
55+ huggingface: 'huggingface',
5356};
5457
5558const initiators = {
@@ -57,6 +60,7 @@ const initiators = {
5760 action: 'action',
5861 interactive: 'interactive',
5962 wand: 'wand',
63+ swipe: 'swipe',
6064};
6165
6266const generationMode = {
@@ -451,6 +455,7 @@ async function loadSettings() {
451455 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
452456 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
453457 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
458+ $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
454459
455460 for (const style of extension_settings.sd.styles) {
456461 const option = document.createElement('option');
@@ -718,29 +723,29 @@ function onChatChanged() {
718723 adjustElementScrollHeight();
719724}
720725
721726async function adjustElementScrollHeight() {
722727 if (!$('.sd_settings').is(':visible')) {
723728 return;
724729 }
725730
726731 await resetScrollHeight($('#sd_prompt_prefix'));
727732 await resetScrollHeight($('#sd_negative_prompt'));
728733 await resetScrollHeight($('#sd_character_prompt'));
729734 await resetScrollHeight($('#sd_character_negative_prompt'));
730735}
731736
732737async function onCharacterPromptInput() {
733738 const key = getCharaFilename(this_chid);
734739 extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val();
735740 await resetScrollHeight($(this));
736741 saveSettingsDebounced();
737742 writePromptFieldsDebounced(this_chid);
738743}
739744
740745async function onCharacterNegativePromptInput() {
741746 const key = getCharaFilename(this_chid);
742747 extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val();
743748 await resetScrollHeight($(this));
744749 saveSettingsDebounced();
745750 writePromptFieldsDebounced(this_chid);
746751}
@@ -849,15 +854,15 @@ function onStepsInput() {
849854 saveSettingsDebounced();
850855}
851856
852857async function onPromptPrefixInput() {
853858 extension_settings.sd.prompt_prefix = $('#sd_prompt_prefix').val();
854859 await resetScrollHeight($(this));
855860 saveSettingsDebounced();
856861}
857862
858863async function onNegativePromptInput() {
859864 extension_settings.sd.negative_prompt = $('#sd_negative_prompt').val();
860865 await resetScrollHeight($(this));
861866 saveSettingsDebounced();
862867}
863868
@@ -1088,6 +1093,11 @@ function onComfyUrlInput() {
10881093 saveSettingsDebounced();
10891094}
10901095
1096+function onHFModelInput() {
1097+ extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();
1098+ saveSettingsDebounced();
1099+}
1100+
10911101function onComfyWorkflowChange() {
10921102 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();
10931103 saveSettingsDebounced();
@@ -1095,7 +1105,18 @@ function onComfyWorkflowChange() {
10951105
10961106async function onStabilityKeyClick() {
10971107 const popupText = 'Stability AI API Key:';
10981108 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT);, '', {
1109+ customButtons: [{
1110+ text: 'Remove Key',
1111+ appendAtEnd: true,
1112+ result: POPUP_RESULT.NEGATIVE,
1113+ action: async () => {
1114+ await writeSecret(SECRET_KEYS.STABILITY, '');
1115+ toastr.success('API Key removed');
1116+ await loadSettingOptions();
1117+ },
1118+ }],
1119+ });
10991120
11001121 if (!key) {
11011122 return;
@@ -1221,7 +1242,16 @@ async function onModelChange() {
12211242 extension_settings.sd.model = $('#sd_model').find(':selected').val();
12221243 saveSettingsDebounced();
12231244
1224- const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai, sources.pollinations, sources.stability];
1245+ const cloudSources = [
1246+ sources.horde,
1247+ sources.novel,
1248+ sources.openai,
1249+ sources.togetherai,
1250+ sources.pollinations,
1251+ sources.stability,
1252+ sources.blockentropy,
1253+ sources.huggingface,
1254+ ];
12251255
12261256 if (cloudSources.includes(extension_settings.sd.source)) {
12271257 return;
@@ -1433,6 +1463,12 @@ async function loadSamplers() {
14331463 case sources.stability:
14341464 samplers = ['N/A'];
14351465 break;
1466+ case sources.blockentropy:
1467+ samplers = ['N/A'];
1468+ break;
1469+ case sources.huggingface:
1470+ samplers = ['N/A'];
1471+ break;
14361472 }
14371473
14381474 for (const sampler of samplers) {
@@ -1619,6 +1655,12 @@ async function loadModels() {
16191655 case sources.stability:
16201656 models = await loadStabilityModels();
16211657 break;
1658+ case sources.blockentropy:
1659+ models = await loadBlockEntropyModels();
1660+ break;
1661+ case sources.huggingface:
1662+ models = [{ value: '', text: '<Enter Model ID above>' }];
1663+ break;
16221664 }
16231665
16241666 for (const model of models) {
@@ -1648,49 +1690,13 @@ async function loadStabilityModels() {
16481690async function loadPollinationsModels() {
16491691 return [
16501692 {
16511693 value: 'pixartflux',
16521694 text: 'PixArt-αlphaFLUX.1 [schnell]',
1653- },
1654- {
1655- value: 'playground',
1656- text: 'Playground v2',
1657- },
1658- {
1659- value: 'dalle3xl',
1660- text: 'DALL•E 3 XL',
1661- },
1662- {
1663- value: 'formulaxl',
1664- text: 'FormulaXL',
1665- },
1666- {
1667- value: 'dreamshaper',
1668- text: 'DreamShaper',
1669- },
1670- {
1671- value: 'deliberate',
1672- text: 'Deliberate',
1673- },
1674- {
1675- value: 'dpo',
1676- text: 'SDXL-DPO',
1677- },
1678- {
1679- value: 'swizz8',
1680- text: 'Swizz8',
1681- },
1682- {
1683- value: 'juggernaut',
1684- text: 'Juggernaut',
16851695 },
16861696 {
16871697 value: 'turbo',
16881698 text: 'SDXL Turbo',
16891699 },
1690- {
1691- value: 'realvis',
1692- text: 'Realistic Vision',
1693- },
16941700 ];
16951701}
16961702
@@ -1713,6 +1719,26 @@ async function loadTogetherAIModels() {
17131719 return [];
17141720}
17151721
1722+async function loadBlockEntropyModels() {
1723+ if (!secret_state[SECRET_KEYS.BLOCKENTROPY]) {
1724+ console.debug('Block Entropy API key is not set.');
1725+ return [];
1726+ }
1727+
1728+ const result = await fetch('/api/sd/blockentropy/models', {
1729+ method: 'POST',
1730+ headers: getRequestHeaders(),
1731+ });
1732+ console.log(result);
1733+ if (result.ok) {
1734+ const data = await result.json();
1735+ console.log(data);
1736+ return data;
1737+ }
1738+
1739+ return [];
1740+}
1741+
17161742async function loadHordeModels() {
17171743 const result = await fetch('/api/horde/sd-models', {
17181744 method: 'POST',
@@ -1979,6 +2005,12 @@ async function loadSchedulers() {
19792005 case sources.stability:
19802006 schedulers = ['N/A'];
19812007 break;
2008+ case sources.blockentropy:
2009+ schedulers = ['N/A'];
2010+ break;
2011+ case sources.huggingface:
2012+ schedulers = ['N/A'];
2013+ break;
19822014 }
19832015
19842016 for (const scheduler of schedulers) {
@@ -2055,6 +2087,12 @@ async function loadVaes() {
20552087 case sources.stability:
20562088 vaes = ['N/A'];
20572089 break;
2090+ case sources.blockentropy:
2091+ vaes = ['N/A'];
2092+ break;
2093+ case sources.huggingface:
2094+ vaes = ['N/A'];
2095+ break;
20582096 }
20592097
20602098 for (const vae of vaes) {
@@ -2266,9 +2304,9 @@ async function generatePicture(initiator, args, trigger, message, callback) {
22662304 const quietPrompt = getQuietPrompt(generationType, trigger);
22672305 const context = getContext();
22682306
2269- // if context.characterId is not null, then we get context.characters[context.characterId].avatar, else we get groupId and context.groups[groupId].id
2307+ const characterName = context.groupId
2270- // sadly, groups is not an array, but is a dict with keys being index numbers, so we have to filter it
2308+ ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
2271- const characterName = context.characterId ? context.characters[context.characterId].name : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString();
2309+ : context.characters[context.characterId]?.name;
22722310
22732311 if (generationType == generationMode.BACKGROUND) {
22742312 const callbackOriginal = callback;
@@ -2290,6 +2328,7 @@ async function generatePicture(initiator, args, trigger, message, callback) {
22902328
22912329 const dimensions = setTypeSpecificDimensions(generationType);
22922330 const abortController = new AbortController();
2331+ const stopButton = document.getElementById('sd_stop_gen');
22932332 let negativePromptPrefix = args?.negative || '';
22942333 let imagePath = '';
22952334
@@ -2300,9 +2339,8 @@ async function generatePicture(initiator, args, trigger, message, callback) {
23002339 const prompt = await getPrompt(generationType, message, trigger, quietPrompt, combineNegatives);
23012340 console.log('Processed image prompt:', prompt);
23022341
2303- eventSource.once(event_types.GENERATION_STOPPED, stopListener);
2342+ $(stopButton).show();
23042343 contexteventSource.deactivateSendButtonsonce(CUSTOM_STOP_EVENT, stopListener);
2305- hideSwipeButtons();
23062344
23072345 if (typeof args?._abortController?.addEventListener === 'function') {
23082346 args._abortController.addEventListener('abort', stopListener);
@@ -2311,13 +2349,13 @@ async function generatePicture(initiator, args, trigger, message, callback) {
23112349 imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal);
23122350 } catch (err) {
23132351 console.trace(err);
23142352 throw new Errortoastr.error('SD prompt text generation failed. Reason: ' + err, 'Image Generation');
2353+ throw new Error('SD prompt text generation failed. Reason: ' + err);
23152354 }
23162355 finally {
2356+ $(stopButton).hide();
23172357 restoreOriginalDimensions(dimensions);
23182358 eventSource.removeListener(event_types.GENERATION_STOPPEDCUSTOM_STOP_EVENT, stopListener);
2319- context.activateSendButtons();
2320- showSwipeButtons();
23212359 }
23222360
23232361 return imagePath;
@@ -2583,6 +2621,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
25832621 case sources.stability:
25842622 result = await generateStabilityImage(prefixedPrompt, negativePrompt, signal);
25852623 break;
2624+ case sources.blockentropy:
2625+ result = await generateBlockEntropyImage(prefixedPrompt, negativePrompt, signal);
2626+ break;
2627+ case sources.huggingface:
2628+ result = await generateHuggingFaceImage(prefixedPrompt, signal);
2629+ break;
25862630 }
25872631
25882632 if (!result.data) {
@@ -2638,6 +2682,40 @@ async function generateTogetherAIImage(prompt, negativePrompt, signal) {
26382682 }
26392683}
26402684
2685+async function generateBlockEntropyImage(prompt, negativePrompt, signal) {
2686+ const result = await fetch('/api/sd/blockentropy/generate', {
2687+ method: 'POST',
2688+ headers: getRequestHeaders(),
2689+ signal: signal,
2690+ body: JSON.stringify({
2691+ prompt: prompt,
2692+ negative_prompt: negativePrompt,
2693+ model: extension_settings.sd.model,
2694+ steps: extension_settings.sd.steps,
2695+ width: extension_settings.sd.width,
2696+ height: extension_settings.sd.height,
2697+ seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
2698+ }),
2699+ });
2700+
2701+ if (result.ok) {
2702+ const data = await result.json();
2703+
2704+ // Default format is 'jpg'
2705+ let format = 'jpg';
2706+
2707+ // Check if a format is specified in the result
2708+ if (data.format) {
2709+ format = data.format.toLowerCase();
2710+ }
2711+
2712+ return { format: format, data: data.images[0] };
2713+ } else {
2714+ const text = await result.text();
2715+ throw new Error(text);
2716+ }
2717+}
2718+
26412719/**
26422720 * Generates an image using the Pollinations API.
26432721 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3182,6 +3260,34 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
31823260 return { format: 'png', data: await promptResult.text() };
31833261}
31843262
3263+
3264+/**
3265+ * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected).
3266+ * @param {string} prompt - The main instruction used to guide the image generation.
3267+ * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3268+ * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3269+ */
3270+async function generateHuggingFaceImage(prompt, signal) {
3271+ const result = await fetch('/api/sd/huggingface/generate', {
3272+ method: 'POST',
3273+ headers: getRequestHeaders(),
3274+ signal: signal,
3275+ body: JSON.stringify({
3276+ model: extension_settings.sd.huggingface_model_id,
3277+ prompt: prompt,
3278+ }),
3279+ });
3280+
3281+ if (result.ok) {
3282+ const data = await result.json();
3283+ return { format: 'jpg', data: data.image };
3284+ } else {
3285+ const text = await result.text();
3286+ throw new Error(text);
3287+ }
3288+}
3289+
3290+
31853291async function onComfyOpenWorkflowEditorClick() {
31863292 let workflow = await (await fetch('/api/sd/comfy/workflow', {
31873293 method: 'POST',
@@ -3347,11 +3453,15 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
33473453 generationType: generationType,
33483454 negative: additionalNegativePrefix,
33493455 inline_image: false,
3456+ image_swipes: [image],
33503457 },
33513458 };
33523459 context.chat.push(message);
3460+ const messageId = context.chat.length - 1;
3461+ await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
33533462 context.addOneMessage(message);
3354- context.saveChat();
3463+ await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
3464+ await context.saveChat();
33553465}
33563466
33573467/**
@@ -3395,7 +3505,7 @@ async function addSDGenButtons() {
33953505 $(document).on('click touchend', function (e) {
33963506 const target = $(e.target);
33973507 if (target.is(dropdown) || target.closest(dropdown).length) return;
33983508 if ((target.is(button) &&|| !dropdowntarget.isclosest(':visible'button).length) && $('#send_but')!dropdown.is(':visible')) {
33993509 e.preventDefault();
34003510
34013511 dropdown.fadeIn(animation_duration);
@@ -3425,6 +3535,10 @@ async function addSDGenButtons() {
34253535 generatePicture(initiators.wand, {}, param);
34263536 }
34273537 });
3538+
3539+ const stopGenButton = $('#sd_stop_gen');
3540+ stopGenButton.hide();
3541+ stopGenButton.on('click', () => eventSource.emit(CUSTOM_STOP_EVENT));
34283542}
34293543
34303544function isValidState() {
@@ -3451,6 +3565,10 @@ function isValidState() {
34513565 return true;
34523566 case sources.stability:
34533567 return secret_state[SECRET_KEYS.STABILITY];
3568+ case sources.blockentropy:
3569+ return secret_state[SECRET_KEYS.BLOCKENTROPY];
3570+ case sources.huggingface:
3571+ return secret_state[SECRET_KEYS.HUGGINGFACE];
34543572 }
34553573}
34563574
@@ -3480,7 +3598,9 @@ async function sdMessageButton(e) {
34803598 const $mes = $icon.closest('.mes');
34813599 const message_id = $mes.attr('mesid');
34823600 const message = context.chat[message_id];
3483- const characterFileName = context.characterId ? context.characters[context.characterId].name : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString();
3601+ const characterFileName = context.groupId
3602+ ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
3603+ : context.characters[context.characterId]?.name;
34843604 const messageText = message?.mes;
34853605 const hasSavedImage = message?.extra?.image && message?.extra?.title;
34863606 const hasSavedNegative = message?.extra?.negative;
@@ -3524,10 +3644,23 @@ async function sdMessageButton(e) {
35243644
35253645 function saveGeneratedImage(prompt, image, generationType, negative) {
35263646 // Some message sources may not create the extra object
35273647 if (typeof message.extra !== 'object' || message.extra === null) {
35283648 message.extra = {};
35293649 }
35303650
3651+ // Add image to the swipe list if it's not already there
3652+ if (!Array.isArray(message.extra.image_swipes)) {
3653+ message.extra.image_swipes = [];
3654+ }
3655+
3656+ const swipes = message.extra.image_swipes;
3657+
3658+ if (message.extra.image && !swipes.includes(message.extra.image)) {
3659+ swipes.push(message.extra.image);
3660+ }
3661+
3662+ swipes.push(image);
3663+
35313664 // If already contains an image and it's not inline - leave it as is
35323665 message.extra.inline_image = message.extra.image && !message.extra.inline_image ? false : true;
35333666 message.extra.image = image;
@@ -3566,6 +3699,99 @@ async function writePromptFields(characterId) {
35663699 await writeExtensionField(characterId, 'sd_character_prompt', promptObject);
35673700}
35683701
3702+/**
3703+ * Switches an image to the next or previous one in the swipe list.
3704+ * @param {object} args Event arguments
3705+ * @param {any} args.message Message object
3706+ * @param {JQuery<HTMLElement>} args.element Message element
3707+ * @param {string} args.direction Swipe direction
3708+ * @returns {Promise<void>}
3709+ */
3710+async function onImageSwiped({ message, element, direction }) {
3711+ const context = getContext();
3712+ const animationClass = 'fa-fade';
3713+ const messageImg = element.find('.mes_img');
3714+
3715+ // Current image is already animating
3716+ if (messageImg.hasClass(animationClass)) {
3717+ return;
3718+ }
3719+
3720+ const swipes = message?.extra?.image_swipes;
3721+
3722+ if (!Array.isArray(swipes)) {
3723+ console.warn('No image swipes found in the message');
3724+ return;
3725+ }
3726+
3727+ const currentIndex = swipes.indexOf(message.extra.image);
3728+
3729+ if (currentIndex === -1) {
3730+ console.warn('Current image not found in the swipes');
3731+ return;
3732+ }
3733+
3734+ // Switch to previous image or wrap around if at the beginning
3735+ if (direction === 'left') {
3736+ const newIndex = currentIndex === 0 ? swipes.length - 1 : currentIndex - 1;
3737+ message.extra.image = swipes[newIndex];
3738+
3739+ // Update the image in the message
3740+ appendMediaToMessage(message, element, false);
3741+ }
3742+
3743+ // Switch to next image or generate a new one if at the end
3744+ if (direction === 'right') {
3745+ const newIndex = currentIndex === swipes.length - 1 ? swipes.length : currentIndex + 1;
3746+
3747+ if (newIndex === swipes.length) {
3748+ const abortController = new AbortController();
3749+ const swipeControls = element.find('.mes_img_swipes');
3750+ const stopButton = document.getElementById('sd_stop_gen');
3751+ const stopListener = () => abortController.abort('Aborted by user');
3752+ const generationType = message?.extra?.generationType ?? generationMode.FREE;
3753+ const dimensions = setTypeSpecificDimensions(generationType);
3754+ const originalSeed = extension_settings.sd.seed;
3755+ extension_settings.sd.seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
3756+ let imagePath = '';
3757+
3758+ try {
3759+ $(stopButton).show();
3760+ eventSource.once(CUSTOM_STOP_EVENT, stopListener);
3761+ const callback = () => { };
3762+ const hasNegative = message.extra.negative;
3763+ const prompt = await refinePrompt(message.extra.title, false, false);
3764+ const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, false, true) : '';
3765+ const characterName = context.groupId
3766+ ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
3767+ : context.characters[context.characterId]?.name;
3768+
3769+ messageImg.addClass(animationClass);
3770+ swipeControls.hide();
3771+ imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiators.swipe, abortController.signal);
3772+ } finally {
3773+ $(stopButton).hide();
3774+ messageImg.removeClass(animationClass);
3775+ swipeControls.show();
3776+ eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
3777+ restoreOriginalDimensions(dimensions);
3778+ extension_settings.sd.seed = originalSeed;
3779+ }
3780+
3781+ if (!imagePath) {
3782+ return;
3783+ }
3784+
3785+ swipes.push(imagePath);
3786+ }
3787+
3788+ message.extra.image = swipes[newIndex];
3789+ appendMediaToMessage(message, element, false);
3790+ }
3791+
3792+ await context.saveChat();
3793+}
3794+
35693795jQuery(async () => {
35703796 await addSDGenButtons();
35713797
@@ -3683,6 +3909,7 @@ jQuery(async () => {
36833909 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
36843910 $('#sd_stability_key').on('click', onStabilityKeyClick);
36853911 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
3912+ $('#sd_huggingface_model_id').on('input', onHFModelInput);
36863913
36873914 $('.sd_settings .inline-drawer-toggle').on('click', function () {
36883915 initScrollHeight($('#sd_prompt_prefix'));
@@ -3704,6 +3931,8 @@ jQuery(async () => {
37043931 }
37053932 });
37063933
3934+ eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped);
3935+
37073936 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
37083937
37093938 await loadSettings();
public/scripts/extensions/stable-diffusion/settings.html+10 -2
@@ -29,7 +29,8 @@
2929 </label>
3030 <label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">
3131 <input id="sd_expand" type="checkbox" />
3232 <span data-i18n="sd_expand_txt">Auto-enhanceextend prompts</span>
33+ <span class="right_menu_button fa-solid fa-triangle-exclamation" data-i18n="[title]sd_expand_warning" title="May produce unexpected results. Manual prompt editing is recommended."></span>
3334 </label>
3435 <label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
3536 <input id="sd_snap" type="checkbox" />
@@ -37,9 +38,11 @@
3738 </label>
3839 <label for="sd_source" data-i18n="Source">Source</label>
3940 <select id="sd_source">
41+ <option value="blockentropy">Block Entropy</option>
4042 <option value="comfy">ComfyUI</option>
4143 <option value="drawthings">DrawThings HTTP API</option>
4244 <option value="extras">Extras API (local / remote)</option>
45+ <option value="huggingface">HuggingFace Inference API (serverless)</option>
4346 <option value="novel">NovelAI Diffusion</option>
4447 <option value="openai">OpenAI (DALL-E)</option>
4548 <option value="pollinations">Pollinations</option>
@@ -81,6 +84,11 @@
8184 <!-- (Original Text)<b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine. -->
8285 <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_drawthings_auth_txt"> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i>
8386 </div>
87+ <div data-sd-source="huggingface">
88+ <i>Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.</i>
89+ <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
90+ <input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" />
91+ </div>
8492 <div data-sd-source="vlad">
8593 <label for="sd_vlad_url">SD.Next API URL</label>
8694 <div class="flex-container flexnowrap">
@@ -378,7 +386,7 @@
378386 </label>
379387 </div>
380388
381389 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,blockentropy" class="marginTop5">
382390 <label for="sd_seed">
383391 <span data-i18n="Seed">Seed</span>
384392 <small data-i18n="(-1 for random)">(-1 for random)</small>
public/scripts/extensions/token-counter/index.js+4 -3
@@ -59,8 +59,8 @@ async function doTokenCounter() {
5959 $('#tokenized_chunks_display').text('—');
6060 }
6161
6262 await resetScrollHeight($('#token_counter_textarea'));
6363 await resetScrollHeight($('#token_counter_ids'));
6464 }, debounce_timeout.relaxed);
6565 dialog.find('#token_counter_textarea').on('input', () => countDebounced());
6666
@@ -134,7 +134,8 @@ jQuery(() => {
134134 </div>`;
135135 $('#token_counter_wand_container').append(buttonHtml);
136136 $('#token_counter').on('click', doTokenCounter);
137137 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count',
138+ name: 'count',
138139 callback: async () => String(await doCount()),
139140 returns: 'number of tokens',
140141 helpString: 'Counts the number of tokens in the current chat.',
public/scripts/extensions/translate/buttons.html+3 -3
@@ -1,8 +1,8 @@
11<div id="translate_chat" class="list-group-item flex-container flexGap5">
22 <div class="fa-solid fa-language extensionsMenuExtensionButton" /></div>
33 <span data-i18n="ext_translate_btn_chat">Translate Chat</span>
44</div>
55<div id="translate_input_message" class="list-group-item flex-container flexGap5">
66 <div class="fa-solid fa-keyboard extensionsMenuExtensionButton" /></div>
77 <span data-i18n="ext_translate_btn_input">Translate Input</span>
8-</div>
8 \ No newline at end of file
8+</div>
public/scripts/extensions/translate/index.js+13 -2
@@ -10,7 +10,7 @@ import {
1010 updateMessageBlock,
1111} from '../../../script.js';
1212import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
1313import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
1414import { findSecret, secret_state, writeSecret } from '../../secrets.js';
1515import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1616import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@@ -621,7 +621,18 @@ jQuery(async () => {
621621 const secretKey = extension_settings.translate.provider + '_url';
622622 const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
623623
624624 const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl);,{
625+ customButtons: [{
626+ text: 'Remove URL',
627+ appendAtEnd: true,
628+ result: POPUP_RESULT.NEGATIVE,
629+ action: async () => {
630+ await writeSecret(secretKey, '');
631+ toastr.success('API URL removed');
632+ $('#translate_url_button').toggleClass('success', !!secret_state[secretKey]);
633+ },
634+ }],
635+ });
625636
626637 if (url == false || url == '') {
627638 return;
public/scripts/extensions/tts/azure.js+0 -0
@@ -1,5 +1,5 @@
11import { getRequestHeaders } from '../../../script.js';
public/scripts/extensions/tts/index.js+0 -0
public/scripts/extensions/tts/openai-compatible.js+0 -0
public/scripts/extensions/tts/system.js+0 -0
public/scripts/extensions/vectors/index.js+0 -0
public/scripts/extensions/vectors/settings.html+0 -0
public/scripts/instruct-mode.js+0 -0
public/scripts/openai.js+0 -0
public/scripts/popup.js+0 -0
public/scripts/power-user.js+0 -0
public/scripts/samplerSelect.js+0 -0
public/scripts/secrets.js+0 -0
public/scripts/showdown-underscore.js+0 -0
public/scripts/slash-commands.js+0 -0
public/scripts/slash-commands/SlashCommand.js+0 -0
public/scripts/slash-commands/SlashCommandArgument.js+0 -0
public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js+0 -0
public/scripts/slash-commands/SlashCommandBreak.js+0 -0
public/scripts/slash-commands/SlashCommandBreakController.js+0 -0
public/scripts/slash-commands/SlashCommandBreakPoint.js+0 -0
public/scripts/slash-commands/SlashCommandClosure.js+0 -0
public/scripts/slash-commands/SlashCommandClosureExecutor.js+0 -0
public/scripts/slash-commands/SlashCommandClosureResult.js+0 -0
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+0 -0
public/scripts/slash-commands/SlashCommandDebugController.js+0 -0
public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js+0 -0
public/scripts/slash-commands/SlashCommandEnumValue.js+0 -0
public/scripts/slash-commands/SlashCommandExecutionError.js+0 -0
public/scripts/slash-commands/SlashCommandExecutor.js+0 -0
public/scripts/slash-commands/SlashCommandParser.js+0 -0
public/scripts/slash-commands/SlashCommandScope.js+0 -0
public/scripts/tags.js+0 -0
public/scripts/templates/installExtension.html+0 -0
public/scripts/templates/worldInfoKeywordHeaders.html+0 -0
public/scripts/textgen-models.js+0 -0
public/scripts/textgen-settings.js+0 -0
public/scripts/tokenizers.js+0 -0
public/scripts/utils.js+0 -0
public/scripts/variables.js+0 -0
public/scripts/world-info.js+0 -0
public/style.css+0 -0
server.js+0 -0
src/additional-headers.js+0 -0
src/constants.js+0 -0
src/endpoints/anthropic.js+0 -0
src/endpoints/backends/chat-completions.js+0 -0
src/endpoints/backends/text-completions.js+0 -0
src/endpoints/content-manager.js+0 -0
src/endpoints/google.js+0 -0
src/endpoints/images.js+0 -0
src/endpoints/openai.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/tokenizers.js+0 -0
src/endpoints/translate.js+0 -0
src/prompt-converters.js+0 -0
src/tokenizers/gemma.model+0 -0

Binary file

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