Merge branch 'staging' into pr/Cohee1207/2711

64d3ed468081047806f93cbcea3281679733049e

RossAscends <124905043+RossAscends@users.noreply.github.com>

99 files changed, +2587 -710Showing whitespace changes
.eslintrc.js+1 -0
@@ -55,6 +55,7 @@ module.exports = {
5555 isProbablyReaderable: 'readonly',
5656 ePub: 'readonly',
5757 diff_match_patch: 'readonly',
58+ SillyTavern: 'readonly',
5859 },
5960 },
6061 ],
.github/readme.md+1 -2
@@ -246,7 +246,6 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
246246
247247##### Additional Settings
248248
249-- [TimeZone] - The timezone your instance should use. This is useful for making logs match your local time for easier troubleshooting. Use your TZ Identifier. (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
250249- [DockerNet] - The docker network that the container should be created with a connection to. If you don't know what it is, see the [official Docker documentation](https://docs.docker.com/reference/cli/docker/network/).
251250- [version] - On the right-hand side of this GitHub page, you'll see "Packages". Select the "sillytavern" package and you'll see the image versions. The image tag "latest" will keep you up-to-date with the current release. You can also utilize "staging" and "release" tags that point to the nightly images of the respective branches, but this may not be appropriate, if you are utilizing extensions that could be broken, and may need time to update.
252251
@@ -255,7 +254,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
2552541. Open your Command Line
2562552. Run the following command
257256
258257`docker create --name='sillytavern' --net='[DockerNet]' -e TZ="[TimeZone]" -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
259258
260259> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.
261260
CONTRIBUTING.md+5 -0
@@ -34,3 +34,8 @@
3434 - What did you do to achieve this?
3535 - How would a reviewer test the change?
36366. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer.
37+
38+## Further reading
39+
40+1. [How to write UI extensions](https://docs.sillytavern.app/for-contributors/writing-extensions/)
41+2. [How to write server plugins](https://docs.sillytavern.app/for-contributors/server-plugins)
Dockerfile+1 -1
@@ -1,4 +1,4 @@
11FROM node:lts-alpine3.1819
22
33# Arguments
44ARG APP_HOME=/home/node/app
default/config.yaml+42 -6
@@ -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)
@@ -67,9 +90,11 @@ whitelistImportDomains:
6790## headers:
6891## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
6992requestOverrides: []
7093# -- PLUGINEXTENSIONS CONFIGURATION --
7194# Enable UI extensions
7295enableExtensions: true
96+# Automatically update extensions when a release version changes
97+enableExtensionsAutoUpdate: true
7398# Extension settings
7499extras:
75100 # Disables automatic model download from HuggingFace
@@ -98,10 +123,21 @@ mistral:
98123 # Enables prefilling of the reply with the last assistant message in the prompt
99124 # CAUTION: The prefix is echoed into the completion. You may want to use regex to trim it out.
100125 enablePrefix: false
126+# -- OLLAMA API CONFIGURATION --
127+ollama:
128+ # Controls how long the model will stay loaded into memory following the request
129+ # * -1: Keep the model loaded indefinitely
130+ # * 0: Unload the model immediately after the request
131+ # * N (any positive number): Keep the model loaded for N seconds after the request.
132+ keepAlive: -1
133+# -- ANTHROPIC CLAUDE API CONFIGURATION --
134+claude:
135+ # Enables caching of the system prompt (if supported).
136+ # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
137+ # -- IMPORTANT! --
138+ # Use only when the prompt before the chat history is static and doesn't change between requests
139+ # (e.g {{random}} macro or lorebooks not as in-chat injections).
140+ # Otherwise, you'll just waste money on cache misses.
141+ enableSystemPromptCache: false
101142# -- SERVER PLUGIN CONFIGURATION --
102143enableServerPlugins: 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+1 -1
@@ -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+1 -0
@@ -142,6 +142,7 @@
142142 "timestamps_enabled": true,
143143 "timestamp_model_icon": true,
144144 "mesIDDisplay_enabled": false,
145+ "hideChatAvatars_enabled": false,
145146 "max_context_unlocked": false,
146147 "prefer_character_prompt": true,
147148 "prefer_character_jailbreak": true,
default/content/themes/Azure.json+1 -0
@@ -23,6 +23,7 @@
2323 "timestamps_enabled": true,
2424 "timestamp_model_icon": false,
2525 "mesIDDisplay_enabled": true,
26+ "hideChatAvatars_enabled": false,
2627 "message_token_count_enabled": false,
2728 "expand_message_actions": false,
2829 "enableZenSliders": false,
default/content/themes/Cappuccino.json+1 -0
@@ -23,6 +23,7 @@
2323 "timestamps_enabled": true,
2424 "timestamp_model_icon": true,
2525 "mesIDDisplay_enabled": true,
26+ "hideChatAvatars_enabled": false,
2627 "message_token_count_enabled": false,
2728 "expand_message_actions": false,
2829 "enableZenSliders": false,
default/content/themes/Dark Lite.json+1 -0
@@ -23,6 +23,7 @@
2323 "timestamps_enabled": true,
2424 "timestamp_model_icon": true,
2525 "mesIDDisplay_enabled": false,
26+ "hideChatAvatars_enabled": false,
2627 "message_token_count_enabled": false,
2728 "expand_message_actions": false,
2829 "enableZenSliders": "",
default/content/themes/Dark V 1.0.json+0 -0
default/content/user-default.png+0 -0

Binary file

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+58 -32
@@ -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 },
@@ -1391,12 +1394,11 @@
13911394 "license": "MIT"
13921395 },
13931396 "node_modules/axios": {
13941397 "version": "1.67.14",
13951398 "resolved": "https://registry.npmjs.org/axios/-/axios-1.67.14.tgz",
13961399 "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnSDukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/VG4I5yxig1pCEXE4gTWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
1397- "license": "MIT",
13981400 "dependencies": {
13991401 "follow-redirects": "^1.15.06",
14001402 "form-data": "^4.0.0",
14011403 "proxy-from-env": "^1.1.0"
14021404 }
@@ -1491,6 +1493,18 @@
14911493 "node": ">= 0.8"
14921494 }
14931495 },
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+ },
14941508 "node_modules/boolbase": {
14951509 "version": "1.0.0",
14961510 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -2456,17 +2470,17 @@
24562470 }
24572471 },
24582472 "node_modules/eslint": {
24592473 "version": "8.5557.0",
24602474 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.5557.0.tgz",
24612475 "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdtdZ6+rCntju0xEH7teIABPwXpahftIaTdAmexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
24622476 "dev": true,
24632477 "license": "MIT",
24642478 "dependencies": {
24652479 "@eslint-community/eslint-utils": "^4.2.0",
24662480 "@eslint-community/regexpp": "^4.6.1",
24672481 "@eslint/eslintrc": "^2.1.4",
24682482 "@eslint/js": "8.5557.0",
24692483 "@humanwhocodes/config-array": "^0.11.1314",
24702484 "@humanwhocodes/module-importer": "^1.0.1",
24712485 "@nodelib/fs.walk": "^1.2.8",
24722486 "@ungap/structured-clone": "^1.2.0",
@@ -3281,12 +3295,12 @@
32813295 }
32823296 },
32833297 "node_modules/iconv-lite": {
32843298 "version": "0.46.243",
32853299 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.46.243.tgz",
32863300 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
32873301 "license": "MIT",
32883302 "dependencies": {
32893303 "safer-buffer": ">= 2.1.2 < 3.0.0"
32903304 },
32913305 "engines": {
32923306 "node": ">=0.10.0"
@@ -4617,6 +4631,18 @@
46174631 "node": ">= 0.8"
46184632 }
46194633 },
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+ },
46204646 "node_modules/readable-stream": {
46214647 "version": "2.3.8",
46224648 "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/ai4.png+0 -0

Binary file

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/index.html+49 -10
@@ -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">
@@ -1823,10 +1823,16 @@
18231823 </div>
18241824 <div data-newbie-hidden class="range-block" data-source="claude">
18251825 <div class="wide100p">
1826+ <div class="flex-container alignItemsCenter">
18261827 <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
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>
1828+ <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_prefill" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
1829+ </div>
1830+ <textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1831+ <div class="flex-container alignItemsCenter">
18281832 <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
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>
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>
@@ -2899,10 +2908,13 @@
28992908 </div>
29002909 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
29012910 <select id="model_perplexity_select">
29022911 <optgroup label="Perplexity Sonar Models">
29032912 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
2904- <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
29052913 <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>
29062918 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
29072919 </optgroup>
29082920 <optgroup label="Open-Source Models">
@@ -2951,6 +2963,20 @@
29512963 </select>
29522964 </div>
29532965 </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>
29542980 <form id="custom_form" data-source="custom">
29552981 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
29562982 <div class="flex-container">
@@ -3485,6 +3511,7 @@
34853511 <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.">
34863512 <small>
34873513 <span data-i18n="Min Activations">Min Activations</span>
3514+ <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>
34883515 </small>
34893516 <input class="neo-range-slider" type="range" id="world_info_min_activations" name="world_info_min_activations" min="0" max="100" step="1">
34903517 <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">
@@ -3498,6 +3525,14 @@
34983525 <input class="neo-range-slider" type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">
34993526 <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">
35003527 </div>
3528+ <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">
3529+ <small>
3530+ <span data-i18n="Max Recursion Steps">Max Recursion Steps</span>
3531+ <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>
3532+ </small>
3533+ <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">
3534+ <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">
3535+ </div>
35013536
35023537 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink flexBasis48p">
35033538 <small data-i18n="Insertion Strategy">
@@ -3761,8 +3796,8 @@
37613796 <span data-i18n="Font Scale">Font Scale</span>
37623797 <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Font size" title="Font size"></div>
37633798 </small>
37643799 <input class="neo-range-slider" type="range" id="font_scale" name="font_scale" min="0.85" max="1.25" step="0.01">
37653800 <input class="neo-range-input" type="number" min="0.85" max="1.25" step="0.01" data-for="font_scale" id="font_scale_counter">
37663801 </div>
37673802
37683803 <div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
@@ -3961,6 +3996,10 @@
39613996 <input id="world_import_dialog" type="checkbox" />
39623997 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>
39633998 </label>
3999+ <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.">
4000+ <input id="enable_auto_select_input" type="checkbox" />
4001+ <small data-i18n="Auto-select Input Text">Auto-select Input Text</small>
4002+ </label>
39644003 <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">
39654004 <input id="restore_user_input" type="checkbox" />
39664005 <small data-i18n="Restore User Input">Restore User Input</small>
@@ -4565,7 +4604,7 @@
45654604 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>
45664605 <input type="hidden" id="fav_checkbox" name="fav" />
45674606 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
45684607 <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>
45694608 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
45704609 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
45714610 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
public/locales/ar-sa.json+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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+1 -1
@@ -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/pt-pt.json+1 -1
@@ -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+1 -1
@@ -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",
public/locales/uk-ua.json+1 -1
@@ -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+1 -1
@@ -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+10 -3
@@ -406,7 +406,7 @@
406406 "Alt Method": "备用方法",
407407 "AI21 API Key": "AI21 API 密钥",
408408 "AI21 Model": "AI21 模型",
409409 "MakerSuiteGoogle AI Studio API Key": "MakerSuiteGoogle AI Studio API 密钥",
410410 "Google Model": "Google 模型",
411411 "MistralAI API Key": "MistralAI API 密钥",
412412 "MistralAI Model": "MistralAI 模型",
@@ -707,10 +707,10 @@
707707 "Restore User Input": "恢复用户输入",
708708 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",
709709 "Movable UI Panels": "可移动 UI 面板",
710+ "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
710711 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",
711712 "MUI Preset": "可移动 UI 预设",
712713 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",
713- "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
714714 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",
715715 "Custom CSS": "自定义 CSS",
716716 "Expand the editor": "展开编辑器",
@@ -730,6 +730,8 @@
730730 "Press Send to continue": "按发送键以继续",
731731 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",
732732 "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": "快速“模仿”按钮",
733735 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",
734736 "Swipes": "刷新回复按钮",
735737 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",
@@ -1183,6 +1185,7 @@
11831185 "Pause script execution": "暂停执行脚本",
11841186 "Abort script execution": "中止执行脚本",
11851187 "Abort request": "中止请求",
1188+ "Ask AI to write your message for you": "让AI为您撰写消息",
11861189 "Continue the last message": "继续上一条消息",
11871190 "Send a message": "发送消息",
11881191 "Close chat": "关闭聊天",
@@ -1194,7 +1197,6 @@
11941197 "Manage chat files": "管理聊天文件",
11951198 "Delete messages": "删除消息",
11961199 "Regenerate": "重新生成",
1197- "Ask AI to write your message for you": "请求AI为您撰写消息",
11981200 "Impersonate": "AI 帮答",
11991201 "Continue": "继续",
12001202 "Bind user name to that avatar": "将用户名称绑定到该头像",
@@ -1429,6 +1431,7 @@
14291431 "ext_regex_export_script": "导出脚本",
14301432 "ext_regex_delete_script": "删除脚本",
14311433 "Trigger Stable Diffusion": "触发Stable Diffusion",
1434+ "Abort current image generation task": "中止当前图像生成",
14321435 "sd_Yourself": "你自己",
14331436 "sd_Your_Face": "你的脸",
14341437 "sd_Me": "我",
@@ -1582,6 +1585,10 @@
15821585 "Only used when Main API is selected.": "仅在选择主 API 时使用。",
15831586 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",
15841587 "View Stats": "查看统计数据",
1588+ "Title/Memo": "标题/备忘录",
1589+ "Status": "状态",
1590+ "Position": "位置",
1591+ "Trigger %": "触发率 %",
15851592 "Manager Users": "管理用户",
15861593 "New User": "新用户",
15871594 "Status:": "地位:",
public/locales/zh-tw.json+1 -1
@@ -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+133 -52
@@ -84,6 +84,7 @@ import {
8484 context_presets,
8585 resetMovableStyles,
8686 forceCharacterEditorTokenize,
87+ applyPowerUserSettings,
8788} from './scripts/power-user.js';
8889
8990import {
@@ -156,6 +157,7 @@ import {
156157 ensureImageFormatSupported,
157158 flashHighlight,
158159 isTrueBoolean,
160+ toggleDrawer,
159161} from './scripts/utils.js';
160162import { debounce_timeout } from './scripts/constants.js';
161163
@@ -224,7 +226,7 @@ import {
224226import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
225227import { hideLoader, showLoader } from './scripts/loader.js';
226228import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
227229import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels } from './scripts/textgen-models.js';
228230import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
229231import { initPresetManager } from './scripts/preset-manager.js';
230232import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros.js';
@@ -241,7 +243,7 @@ import { DragAndDropHandler } from './scripts/dragdrop.js';
241243import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
242244import { initDynamicStyles } from './scripts/dynamic-styles.js';
243245import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
244246import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
245247
246248//exporting functions and vars for mods
247249export {
@@ -414,6 +416,7 @@ export const event_types = {
414416 GENERATION_STOPPED: 'generation_stopped',
415417 GENERATION_ENDED: 'generation_ended',
416418 EXTENSIONS_FIRST_LOAD: 'extensions_first_load',
419+ EXTENSION_SETTINGS_LOADED: 'extension_settings_loaded',
417420 SETTINGS_LOADED: 'settings_loaded',
418421 SETTINGS_UPDATED: 'settings_updated',
419422 GROUP_UPDATED: 'group_updated',
@@ -424,6 +427,8 @@ export const event_types = {
424427 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
425428 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
426429 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',
430+ OAI_PRESET_EXPORT_READY: 'oai_preset_export_ready',
431+ OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready',
427432 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
428433 WORLDINFO_UPDATED: 'worldinfo_updated',
429434 CHARACTER_EDITED: 'character_edited',
@@ -456,6 +461,7 @@ export const event_types = {
456461 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
457462 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
458463 ONLINE_STATUS_CHANGED: 'online_status_changed',
464+ IMAGE_SWIPED: 'image_swiped',
459465};
460466
461467export const eventSource = new EventEmitter();
@@ -911,6 +917,7 @@ async function firstLoadInit() {
911917 await readSecretState();
912918 initLocales();
913919 initDefaultSlashCommands();
920+ initTextGenModels();
914921 await getSystemMessages();
915922 sendSystemMessage(system_message_types.WELCOME);
916923 await getSettings();
@@ -1874,7 +1881,12 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
18741881 }
18751882
18761883 if (Number(messageId) === 0 && !isSystem && !isUser) {
1884+ const mesBeforeReplace = mes;
1885+ const chatMessage = chat[messageId];
18771886 mes = substituteParams(mes, undefined, ch_name);
1887+ if (chatMessage && chatMessage.mes === mesBeforeReplace && chatMessage.extra?.display_text !== mesBeforeReplace) {
1888+ chatMessage.mes = mes;
1889+ }
18781890 }
18791891
18801892 mesForShowdownParse = mes;
@@ -2108,6 +2120,7 @@ export function updateMessageBlock(messageId, message) {
21082120export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
21092121 // Add image to message
21102122 if (mes.extra?.image) {
2123+ const container = messageElement.find('.mes_img_container');
21112124 const chatHeight = $('#chat').prop('scrollHeight');
21122125 const image = messageElement.find('.mes_img');
21132126 const text = messageElement.find('.mes_text');
@@ -2123,9 +2136,27 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
21232136 });
21242137 image.attr('src', mes.extra?.image);
21252138 image.attr('title', mes.extra?.title || mes.title || '');
21262139 messageElement.find('.mes_img_container')container.addClass('img_extra');
21272140 image.toggleClass('img_inline', isInline);
21282141 text.toggleClass('displayNone', !isInline);
2142+
2143+ const imageSwipes = mes.extra.image_swipes;
2144+ if (Array.isArray(imageSwipes) && imageSwipes.length > 0) {
2145+ container.addClass('img_swipes');
2146+ const counter = container.find('.mes_img_swipe_counter');
2147+ const currentImage = imageSwipes.indexOf(mes.extra.image) + 1;
2148+ counter.text(`${currentImage}/${imageSwipes.length}`);
2149+
2150+ const swipeLeft = container.find('.mes_img_swipe_left');
2151+ swipeLeft.off('click').on('click', function () {
2152+ eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'left' });
2153+ });
2154+
2155+ const swipeRight = container.find('.mes_img_swipe_right');
2156+ swipeRight.off('click').on('click', function () {
2157+ eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' });
2158+ });
2159+ }
21292160 }
21302161
21312162 // Add file to message
@@ -2492,8 +2523,8 @@ export function getStoppingStrings(isImpersonate, isContinue) {
24922523 result.push(charString);
24932524 }
24942525
2495- // Add other group members as the stopping strings
2526+ // 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)
24962527 if (selected_group && (name2 || isImpersonate)) {
24972528 const group = groups.find(x => x.id === selected_group);
24982529
24992530 if (group && Array.isArray(group.members)) {
@@ -2815,7 +2846,14 @@ function hideStopButton() {
28152846}
28162847
28172848class StreamingProcessor {
2818- constructor(type, force_name2, timeStarted, messageAlreadyGenerated) {
2849+ /**
2850+ * Creates a new streaming processor.
2851+ * @param {string} type Generation type
2852+ * @param {boolean} forceName2 If true, force the use of name2
2853+ * @param {Date} timeStarted Date when generation was started
2854+ * @param {string} continueMessage Previous message if the type is 'continue'
2855+ */
2856+ constructor(type, forceName2, timeStarted, continueMessage) {
28192857 this.result = '';
28202858 this.messageId = -1;
28212859 this.messageDom = null;
@@ -2825,14 +2863,14 @@ class StreamingProcessor {
28252863 /** @type {HTMLTextAreaElement} */
28262864 this.sendTextarea = document.querySelector('#send_textarea');
28272865 this.type = type;
28282866 this.force_name2 = force_name2forceName2;
28292867 this.isStopped = false;
28302868 this.isFinished = false;
28312869 this.generator = this.nullStreamingGeneration;
28322870 this.abortController = new AbortController();
28332871 this.firstMessageText = '...';
28342872 this.timeStarted = timeStarted;
2835- this.messageAlreadyGenerated = messageAlreadyGenerated;
2873+ this.continueMessage = type === 'continue' ? continueMessage : '';
28362874 this.swipes = [];
28372875 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
28382876 this.messageLogprobs = [];
@@ -2985,8 +3023,7 @@ class StreamingProcessor {
29853023 await eventSource.emit(event_types.IMPERSONATE_READY, text);
29863024 }
29873025
2988- const continueMsg = this.type === 'continue' ? this.messageAlreadyGenerated : undefined;
3026+ saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
2989- saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), continueMsg);
29903027 await saveChatConditional();
29913028 unblockGeneration();
29923029 generatedPromptCache = '';
@@ -3082,7 +3119,7 @@ class StreamingProcessor {
30823119 if (logprobs) {
30833120 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
30843121 }
30853122 await sw.tick(() => this.onProgressStreaming(this.messageId, this.messageAlreadyGeneratedcontinueMessage + text));
30863123 }
30873124 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
30883125 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
@@ -3275,8 +3312,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
32753312 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
32763313 const isImpersonate = type == 'impersonate';
32773314
3278- let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
3279-
32803315 if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
32813316 const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
32823317
@@ -3715,7 +3750,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37153750 let oaiMessageExamples = [];
37163751
37173752 if (main_api === 'openai') {
3718- message_already_generated = '';
37193753 oaiMessages = setOpenAIMessages(coreChat);
37203754 oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray);
37213755 }
@@ -3858,7 +3892,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
38583892 cyclePrompt += oai_settings.continue_postfix;
38593893 continue_mag += oai_settings.continue_postfix;
38603894 }
3861- message_already_generated = continue_mag;
38623895 }
38633896
38643897 const originalType = type;
@@ -3943,7 +3976,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
39433976
39443977 // Get instruct mode line
39453978 if (isInstruct && !isContinue) {
39463979 const name = (quiet_prompt && !quietToLoud && !isImpersonate) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
39473980 const isQuiet = quiet_prompt && type == 'quiet';
39483981 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);
39493982 }
@@ -4285,7 +4318,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
42854318 console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);
42864319
42874320 if (isStreamingEnabled() && type !== 'quiet') {
42884321 streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, message_already_generatedcontinue_mag);
42894322 if (isContinue) {
42904323 // Save reply does add cycle text to the prompt, so it's not needed here
42914324 streamingProcessor.firstMessageText = '';
@@ -5313,17 +5346,10 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
53135346 // Regex uses vars, so add before formatting
53145347 getMessage = getRegexedString(getMessage, isImpersonate ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT);
53155348
5316- if (!displayIncompleteSentences && power_user.trim_sentences) {
5317- getMessage = trimToEndSentence(getMessage, power_user.include_newline);
5318- }
5319-
53205349 if (power_user.collapse_newlines) {
53215350 getMessage = collapseNewlines(getMessage);
53225351 }
53235352
5324- if (power_user.trim_spaces) {
5325- getMessage = getMessage.trim();
5326- }
53275353 // trailing invisible whitespace before every newlines, on a multiline string
53285354 // "trailing whitespace on newlines \nevery line of the string \n?sample text" ->
53295355 // "trailing whitespace on newlines\nevery line of the string\nsample text"
@@ -5402,9 +5428,11 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
54025428 getMessage = fixMarkdown(getMessage, false);
54035429 }
54045430
54055431 const nameToTrim2 = isImpersonate ? name1 : name2;
5432+ ? (!power_user.allow_name1_display ? name1 : '')
5433+ : (!power_user.allow_name2_display ? name2 : '');
54065434
54075435 if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) {
54085436 getMessage = getMessage.replace(nameToTrim2 + ':', '');
54095437 getMessage = getMessage.trimStart();
54105438 }
@@ -5413,6 +5441,14 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
54135441 getMessage = getMessage.trim();
54145442 }
54155443
5444+ if (!displayIncompleteSentences && power_user.trim_sentences) {
5445+ getMessage = trimToEndSentence(getMessage, power_user.include_newline);
5446+ }
5447+
5448+ if (power_user.trim_spaces) {
5449+ getMessage = getMessage.trim();
5450+ }
5451+
54165452 return getMessage;
54175453}
54185454
@@ -6426,6 +6462,8 @@ export async function getSettings() {
64266462 // Load power user settings
64276463 await loadPowerUserSettings(settings, data);
64286464
6465+ applyPowerUserSettings();
6466+
64296467 // Load character tags
64306468 loadTagsSettings(settings);
64316469
@@ -6480,9 +6518,10 @@ export async function getSettings() {
64806518 selected_button = settings.selected_button;
64816519
64826520 if (data.enable_extensions) {
6521+ const enableAutoUpdate = Boolean(data.enable_extensions_auto_update);
64836522 const isVersionChanged = settings.currentVersion !== currentVersion;
64846523 await loadExtensionSettings(settings, isVersionChanged, enableAutoUpdate);
64856524 await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
64866525 }
64876526
64886527 firstRun = !!settings.firstRun;
@@ -8353,6 +8392,12 @@ const CONNECT_API_MAP = {
83538392 button: '#api_button_openai',
83548393 source: chat_completion_sources.OPENAI,
83558394 },
8395+ // Google alias
8396+ 'google': {
8397+ selected: 'openai',
8398+ button: '#api_button_openai',
8399+ source: chat_completion_sources.MAKERSUITE,
8400+ },
83568401 // OpenRouter special naming, to differentiate between chat comp and text comp
83578402 'openrouter': {
83588403 selected: 'openai',
@@ -8366,6 +8411,9 @@ const CONNECT_API_MAP = {
83668411 },
83678412};
83688413
8414+// Collect all unique API names in an array
8415+export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8416+
83698417// Fill connections map from textgen_types and chat_completion_sources
83708418for (const textGenType of Object.values(textgen_types)) {
83718419 if (CONNECT_API_MAP[textGenType]) continue;
@@ -8435,7 +8483,7 @@ async function disableInstructCallback() {
84358483/**
84368484 * @param {string} text API name
84378485 */
84388486async function connectAPISlash(_args, text) {
84398487 if (!text.trim()) {
84408488 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
84418489 if (config.selected !== main_api) continue;
@@ -8458,12 +8506,15 @@ async function connectAPISlash(_, text) {
84588506
84598507 return key;
84608508 }
8509+
8510+ console.error('FIXME: The current API is not in the API map');
8511+ return '';
84618512 }
84628513
84638514 const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
84648515 if (!apiConfig) {
84658516 toastr.error(`Error: ${text} is not a valid API`);
84668517 return '';
84678518 }
84688519
84698520 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
@@ -8483,14 +8534,18 @@ async function connectAPISlash(_, text) {
84838534 $(apiConfig.button).trigger('click');
84848535 }
84858536
8486- toastr.info(`API set to ${text}, trying to connect..`);
8537+ const quiet = isTrueBoolean(args?.quiet);
8538+ const toast = quiet ? jQuery() : toastr.info(`API set to ${text}, trying to connect..`);
84878539
84888540 try {
84898541 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
84908542 console.log('Connection successful');
84918543 } catch {
84928544 console.log('Could not connect after 510 seconds, skipping.');
84938545 }
8546+
8547+ toastr.clear(toast);
8548+ return text;
84948549}
84958550
84968551/**
@@ -8940,9 +8995,6 @@ jQuery(async function () {
89408995 return '';
89418996 }
89428997
8943- // Collect all unique API names in an array
8944- const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8945-
89468998 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89478999 name: 'dupe',
89489000 callback: duplicateCharacter,
@@ -8951,13 +9003,22 @@ jQuery(async function () {
89519003 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89529004 name: 'api',
89539005 callback: connectAPISlash,
9006+ returns: 'the current API',
9007+ namedArgumentList: [
9008+ SlashCommandNamedArgument.fromProps({
9009+ name: 'quiet',
9010+ description: 'Suppress the toast message on connection',
9011+ typeList: [ARGUMENT_TYPE.BOOLEAN],
9012+ defaultValue: 'false',
9013+ enumList: commonEnumProviders.boolean('trueFalse')(),
9014+ }),
9015+ ],
89549016 unnamedArgumentList: [
89559017 SlashCommandArgument.fromProps({
89569018 description: 'API to connect to',
89579019 typeList: [ARGUMENT_TYPE.STRING],
8958- isRequired: false,
89599020 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
89609021 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIsUNIQUE_APIS.findIndex(x => x === selected)),
89619022 selected[0].toUpperCase() ?? enumIcons.default)),
89629023 }),
89639024 ],
@@ -10615,15 +10676,31 @@ jQuery(async function () {
1061510676 }
1061610677 });
1061710678
1061810679 $(document).onaddEventListener('click', '#OpenAllWIEntries', function (e) {
10619- $('#world_popup_entries_list').children().find('.down').click();
10680+ if (!(e.target instanceof HTMLElement)) return;
10681+ if (e.target.matches('#OpenAllWIEntries')) {
10682+ document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10683+ toggleDrawer(drawer, true);
1062010684 });
10621- $(document).on('click', '#CloseAllWIEntries', function () {
10685+ } else if (e.target.matches('#CloseAllWIEntries')) {
10622- $('#world_popup_entries_list').children().find('.up').click();
10686+ document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10687+ toggleDrawer(drawer, false);
1062310688 });
10689+ }
10690+ });
10691+
1062410692 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
1062510693 /* $('#set_character_world').on('click', openCharacterWorldPopup); */
1062610694
10695+ $(document).on('focus', 'input.auto-select, textarea.auto-select', function () {
10696+ if (!power_user.enable_auto_select_input) return;
10697+ const control = $(this)[0];
10698+ if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {
10699+ control.select();
10700+ console.debug('Auto-selecting content of input control', control);
10701+ }
10702+ });
10703+
1062710704 $(document).keyup(function (e) {
1062810705 if (e.key === 'Escape') {
1062910706 const isEditVisible = $('#curEditTextarea').is(':visible');
@@ -10707,7 +10784,7 @@ jQuery(async function () {
1070710784 }
1070810785 } break;
1070910786 case 'import_tags': {
1071010787 await importTags(characters[this_chid], { forceShowimportSetting: truetag_import_setting.ASK });
1071110788 } break;
1071210789 /*case 'delete_button':
1071310790 popup_type = "del_ch";
@@ -10736,15 +10813,17 @@ jQuery(async function () {
1073610813 var isManualInput = false;
1073710814 var valueBeforeManualInput;
1073810815
1073910816 $(document).on('input', '.range-block-counter input, .neo-range-input').on('click', function () {
1074010817 valueBeforeManualInput = $(this).val();
1074110818 console.log(valueBeforeManualInput);
1074210819 });
10743- .on('change', function (e) {
10820+
10821+ $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {
1074410822 e.target.focus();
1074510823 e.target.dispatchEvent(new EventKeyboardEvent('keyup', { bubbles: true }));
1074610824 });
10747- .on('keydown', function (e) {
10825+
10826+ $(document).on('keydown', '.range-block-counter input, .neo-range-input', function (e) {
1074810827 const masterSelector = '#' + $(this).data('for');
1074910828 const masterElement = $(masterSelector);
1075010829 if (e.key === 'Enter') {
@@ -10766,14 +10845,16 @@ jQuery(async function () {
1076610845 }
1076710846 }
1076810847 }
1076910848 });
10770- .on('keyup', function () {
10849+
10850+ $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {
1077110851 valueBeforeManualInput = $(this).val();
1077210852 console.log(valueBeforeManualInput);
1077310853 isManualInput = true;
1077410854 });
10855+
1077510856 //trigger slider changes when user clicks away
1077610857 $(document).on('mouseup blur', '.range-block-counter input, .neo-range-input', function () {
1077710858 const masterSelector = '#' + $(this).data('for');
1077810859 const masterElement = $(masterSelector);
1077910860 let manualInput = Number($(this).val());
public/scripts/BulkEditOverlay.js+33 -3
@@ -18,7 +18,7 @@ import {
1818import { favsToHotswap } from './RossAscends-mods.js';
1919import { hideLoader, showLoader } from './loader.js';
2020import { convertCharacterToPersona } from './personas.js';
2121import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
2222
2323/**
2424 * Static object representing the actions of the
@@ -197,10 +197,10 @@ class BulkTagPopupHandler {
197197 #getHtml = () => {
198198 const characterData = JSON.stringify({ characterIds: this.characterIds });
199199 return `<div id="bulk_tag_shadow_popup">
200200 <div id="bulk_tag_popup" class="wider_dialogue_popup">
201201 <div id="bulk_tag_popup_holder">
202202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
203203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters. Import all or existing tags for all selected characters.</small>
204204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
205205 <br>
206206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
@@ -219,6 +219,12 @@ class BulkTagPopupHandler {
219219 <i class="fa-solid fa-trash-can margin-right-10px"></i>
220220 Mutual
221221 </div>
222+ <div id="bulk_tag_popup_import_all_tags" class="menu_button" title="Import all tags from selected characters" data-i18n="[title]Import all tags from selected characters">
223+ Import All
224+ </div>
225+ <div id="bulk_tag_popup_import_existing_tags" class="menu_button" title="Import existing tags from selected characters" data-i18n="[title]Import existing tags from selected characters">
226+ Import Existing
227+ </div>
222228 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
223229 </div>
224230 </div>
@@ -254,6 +260,30 @@ class BulkTagPopupHandler {
254260 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
255261 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
256262 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
263+ document.querySelector('#bulk_tag_popup_import_all_tags').addEventListener('click', this.importAllTags.bind(this));
264+ document.querySelector('#bulk_tag_popup_import_existing_tags').addEventListener('click', this.importExistingTags.bind(this));
265+ }
266+
267+ /**
268+ * Import existing tags for all selected characters
269+ */
270+ async importExistingTags() {
271+ for (const characterId of this.characterIds) {
272+ await importTags(characters[characterId], { importSetting: tag_import_setting.ONLY_EXISTING });
273+ }
274+
275+ $('#bulkTagList').empty();
276+ }
277+
278+ /**
279+ * Import all tags for all selected characters
280+ */
281+ async importAllTags() {
282+ for (const characterId of this.characterIds) {
283+ await importTags(characters[characterId], { importSetting: tag_import_setting.ALL });
284+ }
285+
286+ $('#bulkTagList').empty();
257287 }
258288
259289 /**
public/scripts/RossAscends-mods.js+6 -4
@@ -380,6 +380,7 @@ function RA_autoconnect(PrevApi) {
380380 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
381381 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
382382 || (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)
383384 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
384385 ) {
385386 $('#api_button_openai').trigger('click');
@@ -953,6 +954,11 @@ export function initRossMods() {
953954 * @param {KeyboardEvent} event
954955 */
955956 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+
956962 //Enter to send when send_textarea in focus
957963 if (document.activeElement == hotkeyTargets['send_textarea']) {
958964 const sendOnEnter = shouldSendOnEnter();
@@ -1106,10 +1112,6 @@ export function initRossMods() {
11061112 }
11071113
11081114 if (event.key == 'Escape') { //closes various panels
1109- // Do not close panels if we are currently inside a popup
1110- if (Popup.util.isPopupOpen())
1111- return;
1112-
11131115 //dont override Escape hotkey functions from script.js
11141116 //"close edit box" and "cancel stream generation".
11151117 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {
public/scripts/extensions.js+56 -28
@@ -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();
@@ -304,7 +307,7 @@ async function activateExtensions() {
304307
305308 if (!isDisabled) {
306309 const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]);
307310 await promise
308311 .then(() => activeExtensions.add(name))
309312 .catch(err => console.log('Could not activate extension: ' + name, err));
310313 promises.push(promise);
@@ -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.');
@@ -797,16 +822,17 @@ export async function installExtension(url) {
797822 const response = await request.json();
798823 toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful');
799824 console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
800825 await loadExtensionSettings({}, false, false);
801826 await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
802827}
803828
804829/**
805830 * Loads extension settings from the app settings.
806831 * @param {object} settings App Settings
807832 * @param {boolean} versionChanged Is this a version change?
833+ * @param {boolean} enableAutoUpdate Enable auto-update
808834 */
809835async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) {
810836 if (settings.extension_settings) {
811837 Object.assign(extension_settings, settings.extension_settings);
812838 }
@@ -817,11 +843,11 @@ async function loadExtensionSettings(settings, versionChanged) {
817843 $('#extensions_notify_updates').prop('checked', extension_settings.notifyUpdates);
818844
819845 // Activate offline extensions
820846 await eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD);
821847 extensionNames = await discoverExtensions();
822848 manifests = await getManifests(extensionNames);
823849
824850 if (versionChanged && enableAutoUpdate) {
825851 await autoUpdateExtensions(false);
826852 }
827853
@@ -989,6 +1015,28 @@ export async function writeExtensionField(characterId, key, value) {
9891015 }
9901016}
9911017
1018+/**
1019+ * Prompts the user to enter the Git URL of the extension to import.
1020+ * After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
1021+ * If the extension is imported successfully, a success message is displayed.
1022+ * If the extension import fails, an error message is displayed and the error is logged to the console.
1023+ * After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
1024+ * @param {string} [suggestUrl] Suggested URL to install
1025+ * @returns {Promise<void>}
1026+ */
1027+export async function openThirdPartyExtensionMenu(suggestUrl = '') {
1028+ const html = await renderTemplateAsync('installExtension');
1029+ const input = await callGenericPopup(html, POPUP_TYPE.INPUT, suggestUrl ?? '');
1030+
1031+ if (!input) {
1032+ console.debug('Extension install cancelled');
1033+ return;
1034+ }
1035+
1036+ const url = String(input).trim();
1037+ await installExtension(url);
1038+}
1039+
9921040jQuery(async function () {
9931041 await addExtensionsButtonAndMenu();
9941042 $('#extensionsMenuButton').css('display', 'flex');
@@ -1004,28 +1052,8 @@ jQuery(async function () {
10041052
10051053 /**
10061054 * 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.
10121055 *
10131056 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
10141057 */
10151058 $('#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- });
10311059});
public/scripts/extensions/caption/index.js+4 -0
@@ -169,7 +169,11 @@ async function sendCaptionedMessage(caption, image) {
169169 },
170170 };
171171 context.chat.push(message);
172+ const messageId = context.chat.length - 1;
173+ await eventSource.emit(event_types.MESSAGE_SENT, messageId);
172174 context.addOneMessage(message);
175+ await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
176+ await context.saveChat();
173177}
174178
175179/**
public/scripts/extensions/caption/settings.html+2 -1
@@ -20,7 +20,7 @@
2020 <option value="zerooneai">01.AI (Yi)</option>
2121 <option value="anthropic">Anthropic</option>
2222 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
2323 <option value="google">Google MakerSuiteAI Studio</option>
2424 <option value="koboldcpp">KoboldCpp</option>
2525 <option value="llamacpp">llama.cpp</option>
2626 <option value="ollama">Ollama</option>
@@ -38,6 +38,7 @@
3838 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
3939 <option data-type="openai" value="gpt-4o">gpt-4o</option>
4040 <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>
4142 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
4243 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
4344 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
public/scripts/extensions/memory/index.js+158 -29
@@ -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')) {
358395 return;
359396 }
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+6 -0
@@ -204,6 +204,7 @@ export class QuickReplyApi {
204204 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
205205 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
206206 * @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
207208 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
208209 * @returns {QuickReply} the new quick reply
209210 */
@@ -218,6 +219,7 @@ export class QuickReplyApi {
218219 executeOnAi,
219220 executeOnChatChange,
220221 executeOnGroupMemberDraft,
222+ executeOnNewChat,
221223 automationId,
222224 } = {}) {
223225 const set = this.getSetByName(setName);
@@ -236,6 +238,7 @@ export class QuickReplyApi {
236238 qr.executeOnAi = executeOnAi ?? false;
237239 qr.executeOnChatChange = executeOnChatChange ?? false;
238240 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
241+ qr.executeOnNewChat = executeOnNewChat ?? false;
239242 qr.automationId = automationId ?? '';
240243 qr.onUpdate();
241244 return qr;
@@ -258,6 +261,7 @@ export class QuickReplyApi {
258261 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
259262 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
260263 * @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
261265 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
262266 * @returns {QuickReply} the altered quick reply
263267 */
@@ -273,6 +277,7 @@ export class QuickReplyApi {
273277 executeOnAi,
274278 executeOnChatChange,
275279 executeOnGroupMemberDraft,
280+ executeOnNewChat,
276281 automationId,
277282 } = {}) {
278283 const qr = this.getQrByLabel(setName, label);
@@ -290,6 +295,7 @@ export class QuickReplyApi {
290295 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
291296 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
292297 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
298+ qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
293299 qr.automationId = automationId ?? qr.automationId;
294300 qr.onUpdate();
295301 return qr;
public/scripts/extensions/quick-reply/html/qrEditor.html+4 -0
@@ -109,6 +109,10 @@
109109 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>
110110 </label>
111111 <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">
112116 <input type="checkbox" id="qr--executeOnGroupMemberDraft">
113117 <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>
114118 </label>
public/scripts/extensions/quick-reply/html/settings.html+3 -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
public/scripts/extensions/quick-reply/index.js+6 -0
@@ -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,
@@ -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/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+8 -0
@@ -44,6 +44,7 @@ export class QuickReply {
4444 /**@type {boolean}*/ executeOnAi = false;
4545 /**@type {boolean}*/ executeOnChatChange = false;
4646 /**@type {boolean}*/ executeOnGroupMemberDraft = false;
47+ /**@type {boolean}*/ executeOnNewChat = false;
4748 /**@type {string}*/ automationId = '';
4849
4950 /**@type {function}*/ onExecute;
@@ -1061,6 +1062,13 @@ export class QuickReply {
10611062 this.updateContext();
10621063 });
10631064 /**@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}*/
10641072 const automationId = dom.querySelector('#qr--automationId');
10651073 automationId.value = this.automationId;
10661074 automationId.addEventListener('input', () => {
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/ui/ButtonUi.js+3 -0
@@ -69,6 +69,8 @@ export class ButtonUi {
6969 root.id = 'qr--bar';
7070 root.classList.add('flex-container');
7171 root.classList.add('flexGap5');
72+ if (this.settings.showPopoutButton) {
73+ root.classList.add('popoutVisible');
7274 const popout = document.createElement('div'); {
7375 popout.id = 'qr--popoutTrigger';
7476 popout.classList.add('menu_button');
@@ -81,6 +83,7 @@ export class ButtonUi {
8183 });
8284 root.append(popout);
8385 }
86+ }
8487 if (this.settings.isCombined) {
8588 const buttons = document.createElement('div'); {
8689 buttonHolder = buttons;
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+10 -0
@@ -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
@@ -79,6 +80,10 @@ export class SettingsUi {
7980 this.isCombined = this.dom.querySelector('#qr--isCombined');
8081 this.isCombined.checked = this.settings.isCombined;
8182 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());
8287 }
8388
8489 prepareGlobalSetList() {
@@ -235,6 +240,11 @@ export class SettingsUi {
235240 this.settings.save();
236241 }
237242
243+ async onShowPopoutButton() {
244+ this.settings.showPopoutButton = this.showPopoutButton.checked;
245+ this.settings.save();
246+ }
247+
238248 async onGlobalSetListSort() {
239249 this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{
240250 const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
public/scripts/extensions/quick-reply/style.css+3 -1
@@ -27,7 +27,6 @@
2727 max-width: 100%;
2828 overflow-x: auto;
2929 order: 1;
30- padding-right: 2.5em;
3130 position: relative;
3231}
3332#qr--bar > #qr--popoutTrigger {
@@ -35,6 +34,9 @@
3534 right: 0.25em;
3635 top: 0;
3736}
37+#qr--bar.popoutVisible {
38+ padding-right: 2.5em;
39+}
3840#qr--popout {
3941 display: flex;
4042 flex-direction: column;
public/scripts/extensions/quick-reply/style.less+3 -1
@@ -25,7 +25,6 @@
2525 max-width: 100%;
2626 overflow-x: auto;
2727 order: 1;
28- padding-right: 2.5em;
2928 position: relative;
3029
3130 >#qr--popoutTrigger {
@@ -34,6 +33,9 @@
3433 top: 0;
3534 }
3635}
36+#qr--bar.popoutVisible {
37+ padding-right: 2.5em;
38+}
3739
3840#qr--popout {
3941 display: flex;
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+85 -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';
@@ -141,7 +141,7 @@ function throwIfInvalidModel(useReverseProxy) {
141141 }
142142
143143 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
144144 throw new Error('MakerSuiteGoogle AI Studio API key is not set.');
145145 }
146146
147147 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
@@ -176,3 +176,86 @@ function throwIfInvalidModel(useReverseProxy) {
176176 throw new Error('Custom API URL is not set.');
177177 }
178178}
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/index.js+272 -48
@@ -30,7 +30,7 @@ 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';
@@ -51,6 +51,8 @@ const sources = {
5151 drawthings: 'drawthings',
5252 pollinations: 'pollinations',
5353 stability: 'stability',
54+ blockentropy: 'blockentropy',
55+ huggingface: 'huggingface',
5456};
5557
5658const initiators = {
@@ -58,6 +60,7 @@ const initiators = {
5860 action: 'action',
5961 interactive: 'interactive',
6062 wand: 'wand',
63+ swipe: 'swipe',
6164};
6265
6366const generationMode = {
@@ -452,6 +455,7 @@ async function loadSettings() {
452455 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
453456 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
454457 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
458+ $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
455459
456460 for (const style of extension_settings.sd.styles) {
457461 const option = document.createElement('option');
@@ -1089,6 +1093,11 @@ function onComfyUrlInput() {
10891093 saveSettingsDebounced();
10901094}
10911095
1096+function onHFModelInput() {
1097+ extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();
1098+ saveSettingsDebounced();
1099+}
1100+
10921101function onComfyWorkflowChange() {
10931102 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();
10941103 saveSettingsDebounced();
@@ -1096,7 +1105,18 @@ function onComfyWorkflowChange() {
10961105
10971106async function onStabilityKeyClick() {
10981107 const popupText = 'Stability AI API Key:';
10991108 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+ });
11001120
11011121 if (!key) {
11021122 return;
@@ -1222,7 +1242,16 @@ async function onModelChange() {
12221242 extension_settings.sd.model = $('#sd_model').find(':selected').val();
12231243 saveSettingsDebounced();
12241244
1225- 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+ ];
12261255
12271256 if (cloudSources.includes(extension_settings.sd.source)) {
12281257 return;
@@ -1434,6 +1463,12 @@ async function loadSamplers() {
14341463 case sources.stability:
14351464 samplers = ['N/A'];
14361465 break;
1466+ case sources.blockentropy:
1467+ samplers = ['N/A'];
1468+ break;
1469+ case sources.huggingface:
1470+ samplers = ['N/A'];
1471+ break;
14371472 }
14381473
14391474 for (const sampler of samplers) {
@@ -1620,6 +1655,12 @@ async function loadModels() {
16201655 case sources.stability:
16211656 models = await loadStabilityModels();
16221657 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;
16231664 }
16241665
16251666 for (const model of models) {
@@ -1649,49 +1690,13 @@ async function loadStabilityModels() {
16491690async function loadPollinationsModels() {
16501691 return [
16511692 {
16521693 value: 'pixartflux',
16531694 text: 'PixArt-αlphaFLUX.1 [schnell]',
1654- },
1655- {
1656- value: 'playground',
1657- text: 'Playground v2',
1658- },
1659- {
1660- value: 'dalle3xl',
1661- text: 'DALL•E 3 XL',
1662- },
1663- {
1664- value: 'formulaxl',
1665- text: 'FormulaXL',
1666- },
1667- {
1668- value: 'dreamshaper',
1669- text: 'DreamShaper',
1670- },
1671- {
1672- value: 'deliberate',
1673- text: 'Deliberate',
1674- },
1675- {
1676- value: 'dpo',
1677- text: 'SDXL-DPO',
1678- },
1679- {
1680- value: 'swizz8',
1681- text: 'Swizz8',
1682- },
1683- {
1684- value: 'juggernaut',
1685- text: 'Juggernaut',
16861695 },
16871696 {
16881697 value: 'turbo',
16891698 text: 'SDXL Turbo',
16901699 },
1691- {
1692- value: 'realvis',
1693- text: 'Realistic Vision',
1694- },
16951700 ];
16961701}
16971702
@@ -1714,6 +1719,26 @@ async function loadTogetherAIModels() {
17141719 return [];
17151720}
17161721
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+
17171742async function loadHordeModels() {
17181743 const result = await fetch('/api/horde/sd-models', {
17191744 method: 'POST',
@@ -1980,6 +2005,12 @@ async function loadSchedulers() {
19802005 case sources.stability:
19812006 schedulers = ['N/A'];
19822007 break;
2008+ case sources.blockentropy:
2009+ schedulers = ['N/A'];
2010+ break;
2011+ case sources.huggingface:
2012+ schedulers = ['N/A'];
2013+ break;
19832014 }
19842015
19852016 for (const scheduler of schedulers) {
@@ -2056,6 +2087,12 @@ async function loadVaes() {
20562087 case sources.stability:
20572088 vaes = ['N/A'];
20582089 break;
2090+ case sources.blockentropy:
2091+ vaes = ['N/A'];
2092+ break;
2093+ case sources.huggingface:
2094+ vaes = ['N/A'];
2095+ break;
20592096 }
20602097
20612098 for (const vae of vaes) {
@@ -2267,9 +2304,9 @@ async function generatePicture(initiator, args, trigger, message, callback) {
22672304 const quietPrompt = getQuietPrompt(generationType, trigger);
22682305 const context = getContext();
22692306
2270- // 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
2271- // 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()
2272- 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;
22732310
22742311 if (generationType == generationMode.BACKGROUND) {
22752312 const callbackOriginal = callback;
@@ -2584,6 +2621,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
25842621 case sources.stability:
25852622 result = await generateStabilityImage(prefixedPrompt, negativePrompt, signal);
25862623 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;
25872630 }
25882631
25892632 if (!result.data) {
@@ -2639,6 +2682,40 @@ async function generateTogetherAIImage(prompt, negativePrompt, signal) {
26392682 }
26402683}
26412684
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+
26422719/**
26432720 * Generates an image using the Pollinations API.
26442721 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3183,6 +3260,34 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
31833260 return { format: 'png', data: await promptResult.text() };
31843261}
31853262
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+
31863291async function onComfyOpenWorkflowEditorClick() {
31873292 let workflow = await (await fetch('/api/sd/comfy/workflow', {
31883293 method: 'POST',
@@ -3348,11 +3453,15 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
33483453 generationType: generationType,
33493454 negative: additionalNegativePrefix,
33503455 inline_image: false,
3456+ image_swipes: [image],
33513457 },
33523458 };
33533459 context.chat.push(message);
3460+ const messageId = context.chat.length - 1;
3461+ await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
33543462 context.addOneMessage(message);
3355- context.saveChat();
3463+ await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
3464+ await context.saveChat();
33563465}
33573466
33583467/**
@@ -3396,7 +3505,7 @@ async function addSDGenButtons() {
33963505 $(document).on('click touchend', function (e) {
33973506 const target = $(e.target);
33983507 if (target.is(dropdown) || target.closest(dropdown).length) return;
33993508 if ((target.is(button) || target.closest(button).length) && !dropdown.is(':visible') && $('#send_but').is(':visible')) {
34003509 e.preventDefault();
34013510
34023511 dropdown.fadeIn(animation_duration);
@@ -3456,6 +3565,10 @@ function isValidState() {
34563565 return true;
34573566 case sources.stability:
34583567 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];
34593572 }
34603573}
34613574
@@ -3485,7 +3598,9 @@ async function sdMessageButton(e) {
34853598 const $mes = $icon.closest('.mes');
34863599 const message_id = $mes.attr('mesid');
34873600 const message = context.chat[message_id];
3488- 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;
34893604 const messageText = message?.mes;
34903605 const hasSavedImage = message?.extra?.image && message?.extra?.title;
34913606 const hasSavedNegative = message?.extra?.negative;
@@ -3529,10 +3644,23 @@ async function sdMessageButton(e) {
35293644
35303645 function saveGeneratedImage(prompt, image, generationType, negative) {
35313646 // Some message sources may not create the extra object
35323647 if (typeof message.extra !== 'object' || message.extra === null) {
35333648 message.extra = {};
35343649 }
35353650
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+
35363664 // If already contains an image and it's not inline - leave it as is
35373665 message.extra.inline_image = message.extra.image && !message.extra.inline_image ? false : true;
35383666 message.extra.image = image;
@@ -3571,6 +3699,99 @@ async function writePromptFields(characterId) {
35713699 await writeExtensionField(characterId, 'sd_character_prompt', promptObject);
35723700}
35733701
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+
35743795jQuery(async () => {
35753796 await addSDGenButtons();
35763797
@@ -3688,6 +3909,7 @@ jQuery(async () => {
36883909 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
36893910 $('#sd_stability_key').on('click', onStabilityKeyClick);
36903911 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
3912+ $('#sd_huggingface_model_id').on('input', onHFModelInput);
36913913
36923914 $('.sd_settings .inline-drawer-toggle').on('click', function () {
36933915 initScrollHeight($('#sd_prompt_prefix'));
@@ -3709,6 +3931,8 @@ jQuery(async () => {
37093931 }
37103932 });
37113933
3934+ eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped);
3935+
37123936 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
37133937
37143938 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/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+14 -2
@@ -1,5 +1,5 @@
11import { getRequestHeaders } from '../../../script.js';
22import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
33import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';
44import { getPreviewString, saveTtsProviderSettings } from './index.js';
55export { AzureTtsProvider };
@@ -70,7 +70,19 @@ class AzureTtsProvider {
7070 const popupText = 'Azure TTS API Key';
7171 const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : '';
7272
7373 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey);, {
74+ customButtons: [{
75+ text: 'Remove Key',
76+ appendAtEnd: true,
77+ result: POPUP_RESULT.NEGATIVE,
78+ action: async () => {
79+ await writeSecret(SECRET_KEYS.AZURE_TTS, '');
80+ $('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]);
81+ toastr.success('API Key removed');
82+ await this.onRefreshClick();
83+ },
84+ }],
85+ });
7486
7587 if (key == false || key == '') {
7688 return;
public/scripts/extensions/tts/index.js+16 -9
@@ -9,6 +9,7 @@ import { SystemTtsProvider } from './system.js';
99import { NovelTtsProvider } from './novel.js';
1010import { power_user } from '../../power-user.js';
1111import { OpenAITtsProvider } from './openai.js';
12+import { OpenAICompatibleTtsProvider } from './openai-compatible.js';
1213import { XTTSTtsProvider } from './xtts.js';
1314import { VITSTtsProvider } from './vits.js';
1415import { GSVITtsProvider } from './gsvi.js';
@@ -82,20 +83,21 @@ export function getPreviewString(lang) {
8283}
8384
8485const ttsProviders = {
8586 ElevenLabsAllTalk: ElevenLabsTtsProviderAllTalkTtsProvider,
8687 SileroAzure: SileroTtsProviderAzureTtsProvider,
87- XTTSv2: XTTSTtsProvider,
88- VITS: VITSTtsProvider,
89- GSVI: GSVITtsProvider,
90- SBVits2: SBVits2TtsProvider,
91- System: SystemTtsProvider,
9288 Coqui: CoquiTtsProvider,
9389 Edge: EdgeTtsProvider,
90+ ElevenLabs: ElevenLabsTtsProvider,
91+ GSVI: GSVITtsProvider,
9492 Novel: NovelTtsProvider,
9593 OpenAI: OpenAITtsProvider,
9694 AllTalk'OpenAI Compatible': AllTalkTtsProviderOpenAICompatibleTtsProvider,
95+ SBVits2: SBVits2TtsProvider,
96+ Silero: SileroTtsProvider,
9797 SpeechT5: SpeechT5TtsProvider,
9898 AzureSystem: AzureTtsProviderSystemTtsProvider,
99+ VITS: VITSTtsProvider,
100+ XTTSv2: XTTSTtsProvider,
99101};
100102let ttsProvider;
101103let ttsProviderName;
@@ -753,6 +755,11 @@ async function onMessageEvent(messageId, lastCharIndex) {
753755 const message = structuredClone(context.chat[messageId]);
754756 const hashNew = getStringHash(message?.mes ?? '');
755757
758+ // Ignore prompt-hidden messages
759+ if (message.is_system) {
760+ return;
761+ }
762+
756763 // if no new messages, or same message, or same message hash, do nothing
757764 if (hashNew === lastMessageHash) {
758765 return;
public/scripts/extensions/tts/openai-compatible.js+193 -0
@@ -0,0 +1,193 @@
1+import { getRequestHeaders } from '../../../script.js';
2+import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
3+import { findSecret, SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
4+import { getPreviewString, saveTtsProviderSettings } from './index.js';
5+
6+export { OpenAICompatibleTtsProvider };
7+
8+class OpenAICompatibleTtsProvider {
9+ settings;
10+ voices = [];
11+ separator = ' . ';
12+
13+ audioElement = document.createElement('audio');
14+
15+ defaultSettings = {
16+ voiceMap: {},
17+ model: 'tts-1',
18+ speed: 1,
19+ available_voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'],
20+ provider_endpoint: 'http://127.0.0.1:8000/v1/audio/speech',
21+ };
22+
23+ get settingsHtml() {
24+ let html = `
25+ <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
26+ <div class="flex-container alignItemsCenter">
27+ <div class="flex1">
28+ <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
29+ </div>
30+ <div id="openai_compatible_tts_key" class="menu_button menu_button_icon">
31+ <i class="fa-solid fa-key"></i>
32+ <span>API Key</span>
33+ </div>
34+ </div>
35+ <label for="openai_compatible_model">Model:</label>
36+ <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/>
37+ <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
38+ <input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/>
39+ <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
40+ <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
41+ return html;
42+ }
43+
44+ async loadSettings(settings) {
45+ // Populate Provider UI given input settings
46+ if (Object.keys(settings).length == 0) {
47+ console.info('Using default TTS Provider settings');
48+ }
49+
50+ // Only accept keys defined in defaultSettings
51+ this.settings = this.defaultSettings;
52+
53+ for (const key in settings) {
54+ if (key in this.settings) {
55+ this.settings[key] = settings[key];
56+ } else {
57+ throw `Invalid setting passed to TTS Provider: ${key}`;
58+ }
59+ }
60+
61+ $('#openai_compatible_tts_endpoint').val(this.settings.provider_endpoint);
62+ $('#openai_compatible_tts_endpoint').on('input', () => { this.onSettingsChange(); });
63+
64+ $('#openai_compatible_model').val(this.defaultSettings.model);
65+ $('#openai_compatible_model').on('input', () => { this.onSettingsChange(); });
66+
67+ $('#openai_compatible_tts_voices').val(this.settings.available_voices.join());
68+ $('#openai_compatible_tts_voices').on('input', () => { this.onSettingsChange(); });
69+
70+ $('#openai_compatible_tts_speed').val(this.settings.speed);
71+ $('#openai_compatible_tts_speed').on('input', () => {
72+ this.onSettingsChange();
73+ });
74+
75+ $('#openai_compatible_tts_speed_output').text(this.settings.speed);
76+
77+ $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
78+ $('#openai_compatible_tts_key').on('click', async () => {
79+ const popupText = 'OpenAI-compatible TTS API Key';
80+ const savedKey = secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS] ? await findSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS) : '';
81+
82+ const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey, {
83+ customButtons: [{
84+ text: 'Remove Key',
85+ appendAtEnd: true,
86+ result: POPUP_RESULT.NEGATIVE,
87+ action: async () => {
88+ await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, '');
89+ $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
90+ toastr.success('API Key removed');
91+ await this.onRefreshClick();
92+ },
93+ }],
94+ });
95+
96+ if (key == false || key == '') {
97+ return;
98+ }
99+
100+ await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, String(key));
101+
102+ toastr.success('API Key saved');
103+ $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
104+ await this.onRefreshClick();
105+ });
106+
107+ await this.checkReady();
108+
109+ console.debug('OpenAI Compatible TTS: Settings loaded');
110+ }
111+
112+ onSettingsChange() {
113+ // Update dynamically
114+ this.settings.provider_endpoint = String($('#openai_compatible_tts_endpoint').val());
115+ this.settings.model = String($('#openai_compatible_model').val());
116+ this.settings.available_voices = String($('#openai_compatible_tts_voices').val()).split(',');
117+ this.settings.speed = Number($('#openai_compatible_tts_speed').val());
118+ $('#openai_compatible_tts_speed_output').text(this.settings.speed);
119+ saveTtsProviderSettings();
120+ }
121+
122+ async checkReady() {
123+ await this.fetchTtsVoiceObjects();
124+ }
125+
126+ async onRefreshClick() {
127+ return;
128+ }
129+
130+ async getVoice(voiceName) {
131+ if (this.voices.length == 0) {
132+ this.voices = await this.fetchTtsVoiceObjects();
133+ }
134+ const match = this.voices.filter(
135+ oaicVoice => oaicVoice.name == voiceName,
136+ )[0];
137+ if (!match) {
138+ throw `TTS Voice name ${voiceName} not found`;
139+ }
140+ return match;
141+ }
142+
143+ async generateTts(text, voiceId) {
144+ const response = await this.fetchTtsGeneration(text, voiceId);
145+ return response;
146+ }
147+
148+ async fetchTtsVoiceObjects() {
149+ return this.settings.available_voices.map(v => {
150+ return { name: v, voice_id: v, lang: 'en-US' };
151+ });
152+ }
153+
154+ async previewTtsVoice(voiceId) {
155+ this.audioElement.pause();
156+ this.audioElement.currentTime = 0;
157+
158+ const text = getPreviewString('en-US');
159+ const response = await this.fetchTtsGeneration(text, voiceId);
160+ if (!response.ok) {
161+ throw new Error(`HTTP ${response.status}`);
162+ }
163+
164+ const audio = await response.blob();
165+ const url = URL.createObjectURL(audio);
166+ this.audioElement.src = url;
167+ this.audioElement.play();
168+ this.audioElement.onended = () => URL.revokeObjectURL(url);
169+ }
170+
171+ async fetchTtsGeneration(inputText, voiceId) {
172+ console.info(`Generating new TTS for voice_id ${voiceId}`);
173+ const response = await fetch('/api/openai/custom/generate-voice', {
174+ method: 'POST',
175+ headers: getRequestHeaders(),
176+ body: JSON.stringify({
177+ provider_endpoint: this.settings.provider_endpoint,
178+ model: this.settings.model,
179+ input: inputText,
180+ voice: voiceId,
181+ response_format: 'mp3',
182+ speed: this.settings.speed,
183+ }),
184+ });
185+
186+ if (!response.ok) {
187+ toastr.error(response.statusText, 'TTS Generation Failed');
188+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
189+ }
190+
191+ return response;
192+ }
193+}
public/scripts/extensions/tts/system.js+1 -1
@@ -124,7 +124,7 @@ class SystemTtsProvider {
124124 if (hasEnabledVoice) {
125125 return;
126126 }
127127 const utterance = new SpeechSynthesisUtterance('hi . ');
128128 utterance.volume = 0;
129129 speechSynthesis.speak(utterance);
130130 hasEnabledVoice = true;
public/scripts/extensions/vectors/index.js+75 -13
@@ -30,6 +30,13 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
3030import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
3131import { SlashCommand } from '../../slash-commands/SlashCommand.js';
3232import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
33+import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
34+import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
35+
36+/**
37+ * @typedef {object} HashedMessage
38+ * @property {string} text - The hashed message text
39+ */
3340
3441const MODULE_NAME = 'vectors';
3542
@@ -191,6 +198,11 @@ function splitByChunks(items) {
191198 return chunkedItems;
192199}
193200
201+/**
202+ * Summarizes messages using the Extras API method.
203+ * @param {HashedMessage[]} hashedMessages Array of hashed messages
204+ * @returns {Promise<HashedMessage[]>} Summarized messages
205+ */
194206async function summarizeExtra(hashedMessages) {
195207 for (const element of hashedMessages) {
196208 try {
@@ -222,6 +234,11 @@ async function summarizeExtra(hashedMessages) {
222234 return hashedMessages;
223235}
224236
237+/**
238+ * Summarizes messages using the main API method.
239+ * @param {HashedMessage[]} hashedMessages Array of hashed messages
240+ * @returns {Promise<HashedMessage[]>} Summarized messages
241+ */
225242async function summarizeMain(hashedMessages) {
226243 for (const element of hashedMessages) {
227244 element.text = await generateRaw(element.text, '', false, false, settings.summary_prompt);
@@ -230,12 +247,39 @@ async function summarizeMain(hashedMessages) {
230247 return hashedMessages;
231248}
232249
250+/**
251+ * Summarizes messages using WebLLM.
252+ * @param {HashedMessage[]} hashedMessages Array of hashed messages
253+ * @returns {Promise<HashedMessage[]>} Summarized messages
254+ */
255+async function summarizeWebLLM(hashedMessages) {
256+ if (!isWebLlmSupported()) {
257+ console.warn('Vectors: WebLLM is not supported');
258+ return hashedMessages;
259+ }
260+
261+ for (const element of hashedMessages) {
262+ const messages = [{ role:'system', content: settings.summary_prompt }, { role:'user', content: element.text }];
263+ element.text = await generateWebLlmChatPrompt(messages);
264+ }
265+
266+ return hashedMessages;
267+}
268+
269+/**
270+ * Summarizes messages using the chosen method.
271+ * @param {HashedMessage[]} hashedMessages Array of hashed messages
272+ * @param {string} endpoint Type of endpoint to use
273+ * @returns {Promise<HashedMessage[]>} Summarized messages
274+ */
233275async function summarize(hashedMessages, endpoint = 'main') {
234276 switch (endpoint) {
235277 case 'main':
236278 return await summarizeMain(hashedMessages);
237279 case 'extras':
238280 return await summarizeExtra(hashedMessages);
281+ case 'webllm':
282+ return await summarizeWebLLM(hashedMessages);
239283 default:
240284 console.error('Unsupported endpoint', endpoint);
241285 }
@@ -357,7 +401,7 @@ async function processFiles(chat) {
357401 const dataBankCollectionIds = await ingestDataBankAttachments();
358402
359403 if (dataBankCollectionIds.length) {
360404 const queryText = await getQueryText(chat, 'file');
361405 await injectDataBankChunks(queryText, dataBankCollectionIds);
362406 }
363407
@@ -391,7 +435,7 @@ async function processFiles(chat) {
391435 await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size, settings.overlap_percent);
392436 }
393437
394438 const queryText = await getQueryText(chat, 'file');
395439 const fileChunks = await retrieveFileChunks(queryText, collectionId);
396440
397441 message.mes = `${fileChunks}\n\n${message.mes}`;
@@ -552,7 +596,7 @@ async function rearrangeChat(chat) {
552596 return;
553597 }
554598
555599 const queryText = await getQueryText(chat, 'chat');
556600
557601 if (queryText.length === 0) {
558602 console.debug('Vectors: No text to query');
@@ -639,15 +683,16 @@ const onChatEvent = debounce(async () => await moduleWorker.update(), debounce_t
639683/**
640684 * Gets the text to query from the chat
641685 * @param {object[]} chat Chat messages
686+ * @param {'file'|'chat'|'world-info'} initiator Initiator of the query
642687 * @returns {Promise<string>} Text to query
643688 */
644689async function getQueryText(chat, initiator) {
645690 let queryText = '';
646691 let i = 0;
647692
648693 let hashedMessages = chat.map(x => ({ text: String(substituteParams(x.mes)) }));
649694
650695 if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) {
651696 hashedMessages = await summarize(hashedMessages, settings.summary_source);
652697 }
653698
@@ -1235,7 +1280,7 @@ async function activateWorldInfo(chat) {
12351280 }
12361281
12371282 // Perform a multi-query
12381283 const queryText = await getQueryText(chat, 'world-info');
12391284
12401285 if (queryText.length === 0) {
12411286 console.debug('Vectors: No text to query for WI');
@@ -1299,11 +1344,30 @@ jQuery(async () => {
12991344 saveSettingsDebounced();
13001345 toggleSettings();
13011346 });
13021347 $('#api_key_nomicai').on('changeclick', async () => {
1303- const nomicKey = String($('#api_key_nomicai').val()).trim();
1348+ const popupText = 'NomicAI API Key:';
1304- if (nomicKey.length) {
1349+ const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {
1305- writeSecret(SECRET_KEYS.NOMICAI, nomicKey);
1350+ customButtons: [{
1351+ text: 'Remove Key',
1352+ appendAtEnd: true,
1353+ result: POPUP_RESULT.NEGATIVE,
1354+ action: async () => {
1355+ await writeSecret(SECRET_KEYS.NOMICAI, '');
1356+ toastr.success('API Key removed');
1357+ $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]);
1358+ saveSettingsDebounced();
1359+ },
1360+ }],
1361+ });
1362+
1363+ if (!key) {
1364+ return;
13061365 }
1366+
1367+ await writeSecret(SECRET_KEYS.NOMICAI, String(key));
1368+ $('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]);
1369+
1370+ toastr.success('API Key saved');
13071371 saveSettingsDebounced();
13081372 });
13091373 $('#vectors_togetherai_model').val(settings.togetherai_model).on('change', () => {
@@ -1531,9 +1595,7 @@ jQuery(async () => {
15311595 $('#dialogue_popup_input').val(presetModel);
15321596 });
15331597
15341598 const validSecret =$('#api_key_nomicai').toggleClass('success', !!secret_state[SECRET_KEYS.NOMICAI]);
1535- const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
1536- $('#api_key_nomicai').attr('placeholder', placeholder);
15371599
15381600 toggleSettings();
15391601 eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);
public/scripts/extensions/vectors/settings.html+8 -11
@@ -12,7 +12,7 @@
1212 <select id="vectors_source" class="text_pole">
1313 <option value="cohere">Cohere</option>
1414 <option value="extras">Extras</option>
1515 <option value="palm">Google MakerSuiteAI Studio</option>
1616 <option value="llamacpp">llama.cpp</option>
1717 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
1818 <option value="mistral">MistralAI</option>
@@ -103,17 +103,13 @@
103103 </span>
104104 </small>
105105
106106 <div class="flex-container flexFlowColumnalignItemsCenter" id="nomicai_apiKey">
107107 <label for="api_key_nomicai" class="flex1">
108108 <span data-i18n="NomicAI API Key">NomicAI API Key</span>
109109 </label>
110110 <div id="api_key_nomicai" class="flex-containermenu_button menu_button_icon">
111- <input id="api_key_nomicai" name="api_key_nomicai" class="text_pole flex1 wide100p" maxlength="500" size="35" type="text" autocomplete="off">
111+ <i class="fa-solid fa-key"></i>
112- <div title="Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_nomicai">
112+ <span data-i18n="Click to set">Click to set</span>
113- </div>
114- </div>
115- <div data-for="api_key_nomicai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
116- For privacy reasons, your API key will be hidden after you reload the page.
117113 </div>
118114 </div>
119115
@@ -378,10 +374,11 @@
378374 <select id="vectors_summary_source" class="text_pole">
379375 <option value="main" data-i18n="Main API">Main API</option>
380376 <option value="extras" data-i18n="Extras API">Extras API</option>
377+ <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
381378 </select>
382379
383380 <label for="vectors_summary_prompt" title="Summary Prompt:">Summary Prompt:</label>
384381 <small data-i18n="Only used when Main API or WebLLM Extension is selected.">Only used when Main API or WebLLM Extension is selected.</small>
385382 <textarea id="vectors_summary_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation."></textarea>
386383 </div>
387384 </div>
public/scripts/openai.js+83 -5
@@ -120,6 +120,7 @@ const default_bias_presets = {
120120const max_2k = 2047;
121121const max_4k = 4095;
122122const max_8k = 8191;
123+const max_12k = 12287;
123124const max_16k = 16383;
124125const max_32k = 32767;
125126const max_64k = 65535;
@@ -186,6 +187,7 @@ export const chat_completion_sources = {
186187 PERPLEXITY: 'perplexity',
187188 GROQ: 'groq',
188189 ZEROONEAI: '01ai',
190+ BLOCKENTROPY: 'blockentropy',
189191};
190192
191193const character_names_behavior = {
@@ -238,7 +240,7 @@ const default_settings = {
238240 top_p_openai: 1.0,
239241 top_k_openai: 0,
240242 min_p_openai: 0,
241243 top_a_openai: 10,
242244 repetition_penalty_openai: 1,
243245 stream_openai: false,
244246 websearch_cohere: false,
@@ -268,6 +270,7 @@ const default_settings = {
268270 perplexity_model: 'llama-3.1-70b-instruct',
269271 groq_model: 'llama-3.1-70b-versatile',
270272 zerooneai_model: 'yi-large',
273+ blockentropy_model: 'be-70b-base-llama3.1',
271274 custom_model: '',
272275 custom_url: '',
273276 custom_include_body: '',
@@ -318,7 +321,7 @@ const oai_settings = {
318321 top_p_openai: 1.0,
319322 top_k_openai: 0,
320323 min_p_openai: 0,
321324 top_a_openai: 10,
322325 repetition_penalty_openai: 1,
323326 stream_openai: false,
324327 websearch_cohere: false,
@@ -348,6 +351,7 @@ const oai_settings = {
348351 perplexity_model: 'llama-3.1-70b-instruct',
349352 groq_model: 'llama-3.1-70b-versatile',
350353 zerooneai_model: 'yi-large',
354+ blockentropy_model: 'be-70b-base-llama3.1',
351355 custom_model: '',
352356 custom_url: '',
353357 custom_include_body: '',
@@ -804,7 +808,8 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
804808
805809 // Reserve budget for group nudge
806810 let groupNudgeMessage = null;
807- if (selected_group) {
811+ const noGroupNudgeTypes = ['impersonate'];
812+ if (selected_group && prompts.has('groupNudge') && !noGroupNudgeTypes.includes(type)) {
808813 groupNudgeMessage = Message.fromPrompt(prompts.get('groupNudge'));
809814 chatCompletion.reserveBudget(groupNudgeMessage);
810815 }
@@ -1542,6 +1547,8 @@ function getChatCompletionModel() {
15421547 return oai_settings.groq_model;
15431548 case chat_completion_sources.ZEROONEAI:
15441549 return oai_settings.zerooneai_model;
1550+ case chat_completion_sources.BLOCKENTROPY:
1551+ return oai_settings.blockentropy_model;
15451552 default:
15461553 throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`);
15471554 }
@@ -1655,6 +1662,23 @@ function saveModelList(data) {
16551662
16561663 $('#model_01ai_select').val(oai_settings.zerooneai_model).trigger('change');
16571664 }
1665+
1666+ if (oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY) {
1667+ $('#model_blockentropy_select').empty();
1668+ model_list.forEach((model) => {
1669+ $('#model_blockentropy_select').append(
1670+ $('<option>', {
1671+ value: model.id,
1672+ text: model.id,
1673+ }));
1674+ });
1675+
1676+ if (!oai_settings.blockentropy_model && model_list.length > 0) {
1677+ oai_settings.blockentropy_model = model_list[0].id;
1678+ }
1679+
1680+ $('#model_blockentropy_select').val(oai_settings.blockentropy_model).trigger('change');
1681+ }
16581682}
16591683
16601684function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {
@@ -3015,6 +3039,7 @@ function loadOpenAISettings(data, settings) {
30153039 oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;
30163040 oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model;
30173041 oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;
3042+ oai_settings.blockentropy_model = settings.blockentropy_model ?? default_settings.blockentropy_model;
30183043 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
30193044 oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
30203045 oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
@@ -3048,6 +3073,7 @@ function loadOpenAISettings(data, settings) {
30483073 oai_settings.names_behavior = settings.names_behavior ?? default_settings.names_behavior;
30493074 oai_settings.continue_postfix = settings.continue_postfix ?? default_settings.continue_postfix;
30503075 oai_settings.function_calling = settings.function_calling ?? default_settings.function_calling;
3076+ oai_settings.openrouter_providers = settings.openrouter_providers ?? default_settings.openrouter_providers;
30513077
30523078 // Migrate from old settings
30533079 if (settings.names_in_completion === true) {
@@ -3093,6 +3119,7 @@ function loadOpenAISettings(data, settings) {
30933119 $('#model_groq_select').val(oai_settings.groq_model);
30943120 $(`#model_groq_select option[value="${oai_settings.groq_model}"`).attr('selected', true);
30953121 $('#model_01ai_select').val(oai_settings.zerooneai_model);
3122+ $('#model_blockentropy_select').val(oai_settings.blockentropy_model);
30963123 $('#custom_model_id').val(oai_settings.custom_model);
30973124 $('#custom_api_url_text').val(oai_settings.custom_url);
30983125 $('#openai_max_context').val(oai_settings.openai_max_context);
@@ -3354,6 +3381,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
33543381 perplexity_model: settings.perplexity_model,
33553382 groq_model: settings.groq_model,
33563383 zerooneai_model: settings.zerooneai_model,
3384+ blockentropy_model: settings.blockentropy_model,
33573385 custom_model: settings.custom_model,
33583386 custom_url: settings.custom_url,
33593387 custom_include_body: settings.custom_include_body,
@@ -3596,6 +3624,8 @@ async function onPresetImportFileChange(e) {
35963624 }
35973625 }
35983626
3627+ await eventSource.emit(event_types.OAI_PRESET_IMPORT_READY, { data: presetBody, presetName: name });
3628+
35993629 const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, {
36003630 method: 'POST',
36013631 headers: getRequestHeaders(),
@@ -3651,6 +3681,7 @@ async function onExportPresetClick() {
36513681 sensitiveFields.forEach(field => delete preset[field]);
36523682 }
36533683
3684+ await eventSource.emit(event_types.OAI_PRESET_EXPORT_READY, preset);
36543685 const presetJsonString = JSON.stringify(preset, null, 4);
36553686 const presetFileName = `${oai_settings.preset_settings_openai}.json`;
36563687 download(presetJsonString, presetFileName, 'application/json');
@@ -3791,6 +3822,7 @@ function onSettingsPresetChange() {
37913822 perplexity_model: ['#model_perplexity_select', 'perplexity_model', false],
37923823 groq_model: ['#model_groq_select', 'groq_model', false],
37933824 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false],
3825+ blockentropy_model: ['#model_blockentropy_select', 'blockentropy_model', false],
37943826 custom_model: ['#custom_model_id', 'custom_model', false],
37953827 custom_url: ['#custom_api_url_text', 'custom_url', false],
37963828 custom_include_body: ['#custom_include_body', 'custom_include_body', false],
@@ -3889,7 +3921,7 @@ function getMaxContextOpenAI(value) {
38893921 if (oai_settings.max_context_unlocked) {
38903922 return unlocked_max;
38913923 }
38923924 else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
38933925 return max_128k;
38943926 }
38953927 else if (value.includes('gpt-3.5-turbo-1106')) {
@@ -4038,6 +4070,12 @@ async function onModelChange() {
40384070 oai_settings.zerooneai_model = value;
40394071 }
40404072
4073+ if (value && $(this).is('#model_blockentropy_select')) {
4074+ console.log('Block Entropy model changed to', value);
4075+ oai_settings.blockentropy_model = value;
4076+ $('#blockentropy_model_id').val(value).trigger('input');
4077+ }
4078+
40414079 if (value && $(this).is('#model_custom_select')) {
40424080 console.log('Custom model changed to', value);
40434081 oai_settings.custom_model = value;
@@ -4326,6 +4364,29 @@ async function onModelChange() {
43264364 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
43274365 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
43284366 }
4367+ if (oai_settings.chat_completion_source === chat_completion_sources.BLOCKENTROPY) {
4368+ if (oai_settings.max_context_unlocked) {
4369+ $('#openai_max_context').attr('max', unlocked_max);
4370+ }
4371+ else if (oai_settings.blockentropy_model.includes('llama3.1')) {
4372+ $('#openai_max_context').attr('max', max_16k);
4373+ }
4374+ else if (oai_settings.blockentropy_model.includes('72b')) {
4375+ $('#openai_max_context').attr('max', max_16k);
4376+ }
4377+ else if (oai_settings.blockentropy_model.includes('120b')) {
4378+ $('#openai_max_context').attr('max', max_12k);
4379+ }
4380+ else {
4381+ $('#openai_max_context').attr('max', max_8k);
4382+ }
4383+
4384+ oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
4385+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
4386+
4387+ oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
4388+ $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
4389+ }
43294390
43304391 $('#openai_max_context_counter').attr('max', Number($('#openai_max_context').attr('max')));
43314392
@@ -4412,7 +4473,7 @@ async function onConnectButtonClick(e) {
44124473 }
44134474
44144475 if (!secret_state[SECRET_KEYS.MAKERSUITE] && !oai_settings.reverse_proxy) {
44154476 console.log('No secret key saved for MakerSuiteGoogle AI Studio');
44164477 return;
44174478 }
44184479 }
@@ -4533,6 +4594,18 @@ async function onConnectButtonClick(e) {
45334594 return;
45344595 }
45354596 }
4597+ if (oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY) {
4598+ const api_key_blockentropy = String($('#api_key_blockentropy').val()).trim();
4599+
4600+ if (api_key_blockentropy.length) {
4601+ await writeSecret(SECRET_KEYS.BLOCKENTROPY, api_key_blockentropy);
4602+ }
4603+
4604+ if (!secret_state[SECRET_KEYS.BLOCKENTROPY]) {
4605+ console.log('No secret key saved for Block Entropy');
4606+ return;
4607+ }
4608+ }
45364609
45374610 startStatusLoading();
45384611 saveSettingsDebounced();
@@ -4584,6 +4657,9 @@ function toggleChatCompletionForms() {
45844657 else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
45854658 $('#model_custom_select').trigger('change');
45864659 }
4660+ else if (oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY) {
4661+ $('#model_blockentropy_select').trigger('change');
4662+ }
45874663 $('[data-source]').each(function () {
45884664 const validSources = $(this).data('source').split(',');
45894665 $(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -4687,6 +4763,7 @@ export function isImageInliningSupported() {
46874763 'gpt-4-turbo',
46884764 'gpt-4o',
46894765 'gpt-4o-mini',
4766+ 'chatgpt-4o-latest',
46904767 'yi-vision',
46914768 ];
46924769
@@ -5313,6 +5390,7 @@ $(document).ready(async function () {
53135390 $('#model_perplexity_select').on('change', onModelChange);
53145391 $('#model_groq_select').on('change', onModelChange);
53155392 $('#model_01ai_select').on('change', onModelChange);
5393+ $('#model_blockentropy_select').on('change', onModelChange);
53165394 $('#model_custom_select').on('change', onModelChange);
53175395 $('#settings_preset_openai').on('change', onSettingsPresetChange);
53185396 $('#new_oai_preset').on('click', onNewPresetClick);
public/scripts/popup.js+8 -8
@@ -40,8 +40,8 @@ export const POPUP_RESULT = {
4040 * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
4141 * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
4242 * @property {CustomPopupInput[]?} [customInputs=null] - Custom inputs to add to the popup. The display below the content and the input box, one by one.
4343 * @property {(popup: Popup) => Promise<boolean?>|boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close
4444 * @property {(popup: Popup) => Promise<void?>|void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up
4545 * @property {number?} [cropAspect=null] - Aspect ratio for the crop popup
4646 * @property {string?} [cropImage=null] - Image URL to display in the crop popup
4747 */
@@ -138,8 +138,8 @@ export class Popup {
138138 /** @readonly @type {CustomPopupButton[]|string[]?} */ customButtons;
139139 /** @readonly @type {CustomPopupInput[]} */ customInputs;
140140
141141 /** @type {(popup: Popup) => Promise<boolean?>|boolean?} */ onClosing;
142142 /** @type {(popup: Popup) => Promise<void?>|void?} */ onClose;
143143
144144 /** @type {POPUP_RESULT|number} */ result;
145145 /** @type {any} */ value;
@@ -509,7 +509,7 @@ export class Popup {
509509 this.result = result;
510510
511511 if (this.onClosing) {
512512 const shouldClose = await this.onClosing(this);
513513 if (!shouldClose) {
514514 this.#isClosingPrevented = true;
515515 // Set values back if we cancel out of closing the popup
@@ -547,13 +547,13 @@ export class Popup {
547547 fixToastrForDialogs();
548548
549549 // After the dialog is actually completely closed, remove it from the DOM
550550 runAfterAnimation(this.dlg, async () => {
551551 // Call the close on the dialog
552552 this.dlg.close();
553553
554554 // Run a possible custom handler right before DOM removal
555555 if (this.onClose) {
556556 await this.onClose(this);
557557 }
558558
559559 // Remove it from the dom
@@ -596,7 +596,7 @@ export class Popup {
596596
597597 /** @returns {boolean} Checks if any modal popup dialog is open */
598598 isPopupOpen() {
599599 return Popup.util.popups.filter(x => x.dlg.hasAttribute('open')).length > 0;
600600 },
601601
602602 /**
public/scripts/power-user.js+41 -177
@@ -60,6 +60,7 @@ export {
6060 power_user,
6161 send_on_enter_options,
6262 getContextSettings,
63+ applyPowerUserSettings,
6364};
6465
6566export const MAX_CONTEXT_DEFAULT = 8192;
@@ -202,6 +203,7 @@ let power_user = {
202203 trim_spaces: true,
203204 relaxed_api_urls: false,
204205 world_import_dialog: true,
206+ enable_auto_select_input: false,
205207 tag_import_setting: tag_import_setting.ASK,
206208 disable_group_trimming: false,
207209 single_line: false,
@@ -300,45 +302,9 @@ let movingUIPresets = [];
300302export let context_presets = [];
301303
302304const storage_keys = {
303- fast_ui_mode: 'TavernAI_fast_ui_mode',
304- avatar_style: 'TavernAI_avatar_style',
305- chat_display: 'TavernAI_chat_display',
306- chat_width: 'chat_width',
307- font_scale: 'TavernAI_font_scale',
308-
309- main_text_color: 'TavernAI_main_text_color',
310- italics_text_color: 'TavernAI_italics_text_color',
311- underline_text_color: 'TavernAI_underline_text_color',
312- quote_text_color: 'TavernAI_quote_text_color',
313- blur_tint_color: 'TavernAI_blur_tint_color',
314- chat_tint_color: 'TavernAI_chat_tint_color',
315- user_mes_blur_tint_color: 'TavernAI_user_mes_blur_tint_color',
316- bot_mes_blur_tint_color: 'TavernAI_bot_mes_blur_tint_color',
317- blur_strength: 'TavernAI_blur_strength',
318- shadow_color: 'TavernAI_shadow_color',
319- shadow_width: 'TavernAI_shadow_width',
320- border_color: 'TavernAI_border_color',
321-
322- custom_css: 'TavernAI_custom_css',
323-
324- waifuMode: 'TavernAI_waifuMode',
325- movingUI: 'TavernAI_movingUI',
326- noShadows: 'TavernAI_noShadows',
327-
328- hotswap_enabled: 'HotswapEnabled',
329- timer_enabled: 'TimerEnabled',
330- timestamps_enabled: 'TimestampsEnabled',
331- timestamp_model_icon: 'TimestampModelIcon',
332- mesIDDisplay_enabled: 'mesIDDisplayEnabled',
333- hideChatAvatars_enabled: 'hideChatAvatarsEnabled',
334- message_token_count_enabled: 'MessageTokenCountEnabled',
335- expand_message_actions: 'ExpandMessageActions',
336- enableZenSliders: 'enableZenSliders',
337- enableLabMode: 'enableLabMode',
338- reduced_motion: 'reduced_motion',
339- compact_input_area: 'compact_input_area',
340305 auto_connect_legacy: 'AutoConnectEnabled',
341306 auto_load_chat_legacy: 'AutoLoadChatEnabled',
307+ hideChatAvatars_legacy: 'hideChatAvatarsEnabled',
342308
343309 storyStringValidationCache: 'StoryStringValidationCache',
344310};
@@ -458,73 +424,47 @@ function fixMarkdown(text, forDisplay) {
458424}
459425
460426function switchHotswap() {
461- const value = localStorage.getItem(storage_keys.hotswap_enabled);
462- power_user.hotswap_enabled = value === null ? true : value == 'true';
463427 $('body').toggleClass('no-hotswap', !power_user.hotswap_enabled);
464428 $('#hotswapEnabled').prop('checked', power_user.hotswap_enabled);
465429}
466430
467431function switchTimer() {
468- const value = localStorage.getItem(storage_keys.timer_enabled);
469- power_user.timer_enabled = value === null ? true : value == 'true';
470432 $('body').toggleClass('no-timer', !power_user.timer_enabled);
471433 $('#messageTimerEnabled').prop('checked', power_user.timer_enabled);
472434}
473435
474436function switchTimestamps() {
475- const value = localStorage.getItem(storage_keys.timestamps_enabled);
476- power_user.timestamps_enabled = value === null ? true : value == 'true';
477437 $('body').toggleClass('no-timestamps', !power_user.timestamps_enabled);
478438 $('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
479439}
480440
481441function switchIcons() {
482- const value = localStorage.getItem(storage_keys.timestamp_model_icon);
483- power_user.timestamp_model_icon = value === null ? true : value == 'true';
484442 $('body').toggleClass('no-modelIcons', !power_user.timestamp_model_icon);
485443 $('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
486444}
487445
488446function switchTokenCount() {
489- const value = localStorage.getItem(storage_keys.message_token_count_enabled);
490- power_user.message_token_count_enabled = value === null ? false : value == 'true';
491447 $('body').toggleClass('no-tokenCount', !power_user.message_token_count_enabled);
492448 $('#messageTokensEnabled').prop('checked', power_user.message_token_count_enabled);
493449}
494450
495451function switchMesIDDisplay() {
496- const value = localStorage.getItem(storage_keys.mesIDDisplay_enabled);
497- power_user.mesIDDisplay_enabled = value === null ? true : value == 'true';
498- /* console.log(`
499- localstorage value:${value},
500- poweruser before:${before},
501- poweruser after:${power_user.mesIDDisplay_enabled}`) */
502452 $('body').toggleClass('no-mesIDDisplay', !power_user.mesIDDisplay_enabled);
503453 $('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
504454}
505455
506456function switchHideChatAvatars() {
507- const value = localStorage.getItem(storage_keys.hideChatAvatars_enabled);
508- power_user.hideChatAvatars_enabled = value === null ? false : value == 'true';
509- /*console.log(`
510- localstorage value:${value},
511- poweruser after:${power_user.hideChatAvatars_enabled}`)
512- */
513457 $('body').toggleClass('hideChatAvatars', power_user.hideChatAvatars_enabled);
514458 $('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
515459}
516460
517461function switchMessageActions() {
518- const value = localStorage.getItem(storage_keys.expand_message_actions);
519- power_user.expand_message_actions = value === null ? false : value == 'true';
520462 $('body').toggleClass('expandMessageActions', power_user.expand_message_actions);
521463 $('#expandMessageActions').prop('checked', power_user.expand_message_actions);
522464 $('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
523465}
524466
525467function switchReducedMotion() {
526- const value = localStorage.getItem(storage_keys.reduced_motion);
527- power_user.reduced_motion = value === null ? false : value == 'true';
528468 jQuery.fx.off = power_user.reduced_motion;
529469 const overrideDuration = power_user.reduced_motion ? 0 : ANIMATION_DURATION_DEFAULT;
530470 setAnimationDuration(overrideDuration);
@@ -533,8 +473,6 @@ function switchReducedMotion() {
533473}
534474
535475function switchCompactInputArea() {
536- const value = localStorage.getItem(storage_keys.compact_input_area);
537- power_user.compact_input_area = value === null ? true : value == 'true';
538476 $('#send_form').toggleClass('compact', power_user.compact_input_area);
539477 $('#compact_input_area').prop('checked', power_user.compact_input_area);
540478}
@@ -550,8 +488,6 @@ async function switchLabMode() {
550488 }
551489 */
552490 await delay(100);
553- const value = localStorage.getItem(storage_keys.enableLabMode);
554- power_user.enableLabMode = value === null ? false : value == 'true';
555491 $('body').toggleClass('enableLabMode', power_user.enableLabMode);
556492 $('#enableLabMode').prop('checked', power_user.enableLabMode);
557493
@@ -598,8 +534,6 @@ async function switchLabMode() {
598534
599535async function switchZenSliders() {
600536 await delay(100);
601- const value = localStorage.getItem(storage_keys.enableZenSliders);
602- power_user.enableZenSliders = value === null ? false : value == 'true';
603537 $('body').toggleClass('enableZenSliders', power_user.enableZenSliders);
604538 $('#enableZenSliders').prop('checked', power_user.enableZenSliders);
605539
@@ -971,8 +905,6 @@ async function CreateZenSliders(elmnt) {
971905 }
972906}
973907function switchUiMode() {
974- const fastUi = localStorage.getItem(storage_keys.fast_ui_mode);
975- power_user.fast_ui_mode = fastUi === null ? true : fastUi == 'true';
976908 $('body').toggleClass('no-blur', power_user.fast_ui_mode);
977909 $('#fast_ui_mode').prop('checked', power_user.fast_ui_mode);
978910 if (power_user.fast_ui_mode) {
@@ -1022,8 +954,6 @@ function switchMovingUI() {
1022954 $('.drawer-content.maximized').each(function () {
1023955 $(this).find('.inline-drawer-maximize').trigger('click');
1024956 });
1025- const movingUI = localStorage.getItem(storage_keys.movingUI);
1026- power_user.movingUI = movingUI === null ? false : movingUI == 'true';
1027957 $('body').toggleClass('movingUI', power_user.movingUI);
1028958 if (power_user.movingUI === true) {
1029959 initMovingUI();
@@ -1039,9 +969,7 @@ function switchMovingUI() {
1039969 }
1040970}
1041971
1042972function noShadowsapplyNoShadows() {
1043- const noShadows = localStorage.getItem(storage_keys.noShadows);
1044- power_user.noShadows = noShadows === null ? false : noShadows == 'true';
1045973 $('body').toggleClass('noShadows', power_user.noShadows);
1046974 $('#noShadowsmode').prop('checked', power_user.noShadows);
1047975 if (power_user.noShadows) {
@@ -1055,12 +983,9 @@ function noShadows() {
1055983}
1056984
1057985function applyAvatarStyle() {
1058- power_user.avatar_style = Number(localStorage.getItem(storage_keys.avatar_style) ?? avatar_styles.ROUND);
1059986 $('body').toggleClass('big-avatars', power_user.avatar_style === avatar_styles.RECTANGULAR);
1060987 $('body').toggleClass('square-avatars', power_user.avatar_style === avatar_styles.SQUARE);
1061988 $('#avatar_style').val(power_user.avatar_style).prop('selected', true);
1062- //$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop("checked", true);
1063-
1064989}
1065990
1066991function applyChatDisplay() {
@@ -1095,8 +1020,6 @@ function applyChatDisplay() {
10951020}
10961021
10971022function applyChatWidth(type) {
1098- power_user.chat_width = Number(localStorage.getItem(storage_keys.chat_width) ?? 50);
1099-
11001023 if (type === 'forced') {
11011024 let r = document.documentElement;
11021025 r.style.setProperty('--sheldWidth', `${power_user.chat_width}vw`);
@@ -1158,8 +1081,6 @@ async function applyThemeColor(type) {
11581081}
11591082
11601083async function applyCustomCSS() {
1161- power_user.custom_css = String(localStorage.getItem(storage_keys.custom_css) ?? '');
1162-
11631084 $('#customCSS').val(power_user.custom_css);
11641085 var styleId = 'custom-style';
11651086 var style = document.getElementById(styleId);
@@ -1173,32 +1094,26 @@ async function applyCustomCSS() {
11731094}
11741095
11751096async function applyBlurStrength() {
1176- power_user.blur_strength = Number(localStorage.getItem(storage_keys.blur_strength) ?? 1);
1097+ document.documentElement.style.setProperty('--blurStrength', String(power_user.blur_strength));
1177- document.documentElement.style.setProperty('--blurStrength', power_user.blur_strength);
11781098 $('#blur_strength_counter').val(power_user.blur_strength);
11791099 $('#blur_strength').val(power_user.blur_strength);
1180-
1181-
11821100}
11831101
11841102async function applyShadowWidth() {
1185- power_user.shadow_width = Number(localStorage.getItem(storage_keys.shadow_width) ?? 2);
1103+ document.documentElement.style.setProperty('--shadowWidth', String(power_user.shadow_width));
1186- document.documentElement.style.setProperty('--shadowWidth', power_user.shadow_width);
11871104 $('#shadow_width_counter').val(power_user.shadow_width);
11881105 $('#shadow_width').val(power_user.shadow_width);
11891106
11901107}
11911108
11921109async function applyFontScale(type) {
1193-
1194- power_user.font_scale = Number(localStorage.getItem(storage_keys.font_scale) ?? 1);
11951110 //this is to allow forced setting on page load, theme swap, etc
11961111 if (type === 'forced') {
11971112 document.documentElement.style.setProperty('--fontScale', String(power_user.font_scale));
11981113 } else {
11991114 //this is to prevent the slider from updating page in real time
12001115 $('#font_scale').off('mouseup touchend').on('mouseup touchend', () => {
12011116 document.documentElement.style.setProperty('--fontScale', String(power_user.font_scale));
12021117 });
12031118 }
12041119
@@ -1227,64 +1142,55 @@ async function applyTheme(name) {
12271142 {
12281143 key: 'blur_strength',
12291144 action: async () => {
1230- localStorage.setItem(storage_keys.blur_strength, power_user.blur_strength);
12311145 await applyBlurStrength();
12321146 },
12331147 },
12341148 {
12351149 key: 'custom_css',
12361150 action: async () => {
1237- localStorage.setItem(storage_keys.custom_css, power_user.custom_css);
12381151 await applyCustomCSS();
12391152 },
12401153 },
12411154 {
12421155 key: 'shadow_width',
12431156 action: async () => {
1244- localStorage.setItem(storage_keys.shadow_width, power_user.shadow_width);
12451157 await applyShadowWidth();
12461158 },
12471159 },
12481160 {
12491161 key: 'font_scale',
12501162 action: async () => {
1251- localStorage.setItem(storage_keys.font_scale, power_user.font_scale);
12521163 await applyFontScale('forced');
12531164 },
12541165 },
12551166 {
12561167 key: 'fast_ui_mode',
12571168 action: async () => {
1258- localStorage.setItem(storage_keys.fast_ui_mode, power_user.fast_ui_mode);
12591169 switchUiMode();
12601170 },
12611171 },
12621172 {
12631173 key: 'waifuMode',
12641174 action: async () => {
1265- localStorage.setItem(storage_keys.waifuMode, power_user.waifuMode);
12661175 switchWaifuMode();
12671176 },
12681177 },
12691178 {
12701179 key: 'chat_display',
12711180 action: async () => {
1272- localStorage.setItem(storage_keys.chat_display, power_user.chat_display);
12731181 applyChatDisplay();
12741182 },
12751183 },
12761184 {
12771185 key: 'avatar_style',
12781186 action: async () => {
1279- localStorage.setItem(storage_keys.avatar_style, power_user.avatar_style);
12801187 applyAvatarStyle();
12811188 },
12821189 },
12831190 {
12841191 key: 'noShadows',
12851192 action: async () => {
1286- localStorage.setItem(storage_keys.noShadows, power_user.noShadows);
1193+ applyNoShadows();
1287- noShadows();
12881194 },
12891195 },
12901196 {
@@ -1294,78 +1200,66 @@ async function applyTheme(name) {
12941200 if (!power_user.chat_width) {
12951201 power_user.chat_width = 50;
12961202 }
1297-
1298- localStorage.setItem(storage_keys.chat_width, String(power_user.chat_width));
12991203 applyChatWidth('forced');
13001204 },
13011205 },
13021206 {
13031207 key: 'timer_enabled',
13041208 action: async () => {
1305- localStorage.setItem(storage_keys.timer_enabled, Boolean(power_user.timer_enabled));
13061209 switchTimer();
13071210 },
13081211 },
13091212 {
13101213 key: 'timestamps_enabled',
13111214 action: async () => {
1312- localStorage.setItem(storage_keys.timestamps_enabled, Boolean(power_user.timestamps_enabled));
13131215 switchTimestamps();
13141216 },
13151217 },
13161218 {
13171219 key: 'timestamp_model_icon',
13181220 action: async () => {
1319- localStorage.setItem(storage_keys.timestamp_model_icon, Boolean(power_user.timestamp_model_icon));
13201221 switchIcons();
13211222 },
13221223 },
13231224 {
13241225 key: 'message_token_count_enabled',
13251226 action: async () => {
1326- localStorage.setItem(storage_keys.message_token_count_enabled, Boolean(power_user.message_token_count_enabled));
13271227 switchTokenCount();
13281228 },
13291229 },
13301230 {
13311231 key: 'mesIDDisplay_enabled',
13321232 action: async () => {
1333- localStorage.setItem(storage_keys.mesIDDisplay_enabled, Boolean(power_user.mesIDDisplay_enabled));
13341233 switchMesIDDisplay();
13351234 },
13361235 },
13371236 {
13381237 key: 'hideChatAvatars_enabled',
13391238 action: async () => {
1340- localStorage.setItem(storage_keys.hideChatAvatars_enabled, Boolean(power_user.hideChatAvatars_enabled));
13411239 switchHideChatAvatars();
13421240 },
13431241 },
13441242 {
13451243 key: 'expand_message_actions',
13461244 action: async () => {
1347- localStorage.setItem(storage_keys.expand_message_actions, Boolean(power_user.expand_message_actions));
13481245 switchMessageActions();
13491246 },
13501247 },
13511248 {
13521249 key: 'enableZenSliders',
13531250 action: async () => {
1354- localStorage.setItem(storage_keys.enableZenSliders, Boolean(power_user.enableZenSliders));
13551251 switchMessageActions();
13561252 },
13571253 },
13581254 {
13591255 key: 'enableLabMode',
13601256 action: async () => {
1361- localStorage.setItem(storage_keys.enableLabMode, Boolean(power_user.enableLabMode));
13621257 switchMessageActions();
13631258 },
13641259 },
13651260 {
13661261 key: 'hotswap_enabled',
13671262 action: async () => {
1368- localStorage.setItem(storage_keys.hotswap_enabled, Boolean(power_user.hotswap_enabled));
13691263 switchHotswap();
13701264 },
13711265 },
@@ -1386,7 +1280,6 @@ async function applyTheme(name) {
13861280 {
13871281 key: 'reduced_motion',
13881282 action: async () => {
1389- localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
13901283 $('#reduced_motion').prop('checked', power_user.reduced_motion);
13911284 switchReducedMotion();
13921285 },
@@ -1394,7 +1287,6 @@ async function applyTheme(name) {
13941287 {
13951288 key: 'compact_input_area',
13961289 action: async () => {
1397- localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
13981290 $('#compact_input_area').prop('checked', power_user.compact_input_area);
13991291 switchCompactInputArea();
14001292 },
@@ -1449,6 +1341,7 @@ async function showDebugMenu() {
14491341 callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true });
14501342}
14511343
1344+function applyPowerUserSettings() {
14521345 switchUiMode();
14531346 applyFontScale('forced');
14541347 applyThemeColor();
@@ -1458,7 +1351,7 @@ applyBlurStrength();
14581351 applyShadowWidth();
14591352 applyCustomCSS();
14601353 switchMovingUI();
14611354noShadows applyNoShadows();
14621355 switchHotswap();
14631356 switchTimer();
14641357 switchTimestamps();
@@ -1467,6 +1360,7 @@ switchMesIDDisplay();
14671360 switchHideChatAvatars();
14681361 switchTokenCount();
14691362 switchMessageActions();
1363+}
14701364
14711365function getExampleMessagesBehavior() {
14721366 if (power_user.strip_examples) {
@@ -1529,20 +1423,10 @@ async function loadPowerUserSettings(settings, data) {
15291423 context_presets = data.context;
15301424 }
15311425
15321426 // These are still local storage. Delete in 1.12.7
1533- const fastUi = localStorage.getItem(storage_keys.fast_ui_mode);
1534- const movingUI = localStorage.getItem(storage_keys.movingUI);
1535- const noShadows = localStorage.getItem(storage_keys.noShadows);
1536- const hotswap = localStorage.getItem(storage_keys.hotswap_enabled);
1537- const timer = localStorage.getItem(storage_keys.timer_enabled);
1538- const timestamps = localStorage.getItem(storage_keys.timestamps_enabled);
1539- const mesIDDisplay = localStorage.getItem(storage_keys.mesIDDisplay_enabled);
1540- const hideChatAvatars = localStorage.getItem(storage_keys.hideChatAvatars_enabled);
1541- const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions);
1542- const enableZenSliders = localStorage.getItem(storage_keys.enableZenSliders);
1543- const enableLabMode = localStorage.getItem(storage_keys.enableLabMode);
15441427 const autoLoadChat = localStorage.getItem(storage_keys.auto_load_chat_legacy);
15451428 const autoConnect = localStorage.getItem(storage_keys.auto_connect_legacy);
1429+ const hideChatAvatars = localStorage.getItem(storage_keys.hideChatAvatars_legacy);
15461430
15471431 if (autoLoadChat) {
15481432 power_user.auto_load_chat = autoLoadChat === 'true';
@@ -1554,22 +1438,10 @@ async function loadPowerUserSettings(settings, data) {
15541438 localStorage.removeItem(storage_keys.auto_connect_legacy);
15551439 }
15561440
1557- power_user.fast_ui_mode = fastUi === null ? true : fastUi == 'true';
1441+ if (hideChatAvatars) {
15581442 power_user.movingUIhideChatAvatars_enabled = movingUIhideChatAvatars === null ? false : movingUI == 'true';
1559- power_user.noShadows = noShadows === null ? false : noShadows == 'true';
1443+ localStorage.removeItem(storage_keys.hideChatAvatars_legacy);
1560- power_user.hotswap_enabled = hotswap === null ? true : hotswap == 'true';
1444+ }
1561- power_user.timer_enabled = timer === null ? true : timer == 'true';
1562- power_user.timestamps_enabled = timestamps === null ? true : timestamps == 'true';
1563- power_user.mesIDDisplay_enabled = mesIDDisplay === null ? true : mesIDDisplay == 'true';
1564- power_user.hideChatAvatars_enabled = hideChatAvatars === null ? true : hideChatAvatars == 'true';
1565- power_user.expand_message_actions = expandMessageActions === null ? true : expandMessageActions == 'true';
1566- power_user.enableZenSliders = enableZenSliders === null ? false : enableZenSliders == 'true';
1567- power_user.enableLabMode = enableLabMode === null ? false : enableLabMode == 'true';
1568- power_user.avatar_style = Number(localStorage.getItem(storage_keys.avatar_style) ?? avatar_styles.ROUND);
1569- //power_user.chat_display = Number(localStorage.getItem(storage_keys.chat_display) ?? chat_styles.DEFAULT);
1570- power_user.chat_width = Number(localStorage.getItem(storage_keys.chat_width) ?? 50);
1571- power_user.font_scale = Number(localStorage.getItem(storage_keys.font_scale) ?? 1);
1572- power_user.blur_strength = Number(localStorage.getItem(storage_keys.blur_strength) ?? 10);
15731445
15741446 if (power_user.chat_display === '') {
15751447 power_user.chat_display = chat_styles.DEFAULT;
@@ -1596,6 +1468,7 @@ async function loadPowerUserSettings(settings, data) {
15961468 $('#single_line').prop('checked', power_user.single_line);
15971469 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
15981470 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
1471+ $('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);
15991472 $('#trim_spaces').prop('checked', power_user.trim_spaces);
16001473 $('#continue_on_send').prop('checked', power_user.continue_on_send);
16011474 $('#quick_continue').prop('checked', power_user.quick_continue);
@@ -1655,7 +1528,7 @@ async function loadPowerUserSettings(settings, data) {
16551528 $('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
16561529 $('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
16571530 $('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
16581531 $('#hideChatAvatarsEndabledhideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
16591532 $('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);
16601533 $('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);
16611534 $('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');
@@ -3298,10 +3171,8 @@ $(document).ready(() => {
32983171 saveSettingsDebounced();
32993172 });
33003173
3301- // Settings that go to local storage
33023174 $('#fast_ui_mode').change(function () {
33033175 power_user.fast_ui_mode = $(this).prop('checked');
3304- localStorage.setItem(storage_keys.fast_ui_mode, power_user.fast_ui_mode);
33053176 switchUiMode();
33063177 saveSettingsDebounced();
33073178 });
@@ -3312,24 +3183,21 @@ $(document).ready(() => {
33123183 saveSettingsDebounced();
33133184 });
33143185
33153186 $('#customCSS').on('changeinput', () => {
33163187 power_user.custom_css = String($('#customCSS').val());
3317- localStorage.setItem(storage_keys.custom_css, power_user.custom_css);
33183188 saveSettingsDebounced();
33193189 applyCustomCSS();
33203190 });
33213191
33223192 $('#movingUImode').change(function () {
33233193 power_user.movingUI = $(this).prop('checked');
3324- localStorage.setItem(storage_keys.movingUI, power_user.movingUI);
33253194 switchMovingUI();
33263195 saveSettingsDebounced();
33273196 });
33283197
33293198 $('#noShadowsmode').change(function () {
33303199 power_user.noShadows = $(this).prop('checked');
3331- localStorage.setItem(storage_keys.noShadows, power_user.noShadows);
3200+ applyNoShadows();
3332- noShadows();
33333201 saveSettingsDebounced();
33343202 });
33353203
@@ -3338,7 +3206,6 @@ $(document).ready(() => {
33383206 $('#avatar_style').on('change', function () {
33393207 const value = $(this).find(':selected').val();
33403208 power_user.avatar_style = Number(value);
3341- localStorage.setItem(storage_keys.avatar_style, power_user.avatar_style);
33423209 applyAvatarStyle();
33433210 saveSettingsDebounced();
33443211 });
@@ -3346,17 +3213,15 @@ $(document).ready(() => {
33463213 $('#chat_display').on('change', function () {
33473214 const value = $(this).find(':selected').val();
33483215 power_user.chat_display = Number(value);
3349- localStorage.setItem(storage_keys.chat_display, power_user.chat_display);
33503216 applyChatDisplay();
33513217 saveSettingsDebounced();
3352-
33533218 });
33543219
33553220 $('#chat_width_slider').on('input', function (e, data) {
33563221 const applyMode = data?.forced ? 'forced' : 'normal';
33573222 power_user.chat_width = Number(e.target.value);
3358- localStorage.setItem(storage_keys.chat_width, power_user.chat_width);
33593223 applyChatWidth(applyMode);
3224+ saveSettingsDebounced();
33603225 setHotswapsDebounced();
33613226 });
33623227
@@ -3386,7 +3251,6 @@ $(document).ready(() => {
33863251 const applyMode = data?.forced ? 'forced' : 'normal';
33873252 power_user.font_scale = Number(e.target.value);
33883253 $('#font_scale_counter').val(power_user.font_scale);
3389- localStorage.setItem(storage_keys.font_scale, power_user.font_scale);
33903254 await applyFontScale(applyMode);
33913255 saveSettingsDebounced();
33923256 });
@@ -3394,7 +3258,6 @@ $(document).ready(() => {
33943258 $('input[name="blur_strength"]').on('input', async function (e) {
33953259 power_user.blur_strength = Number(e.target.value);
33963260 $('#blur_strength_counter').val(power_user.blur_strength);
3397- localStorage.setItem(storage_keys.blur_strength, power_user.blur_strength);
33983261 await applyBlurStrength();
33993262 saveSettingsDebounced();
34003263 });
@@ -3402,7 +3265,6 @@ $(document).ready(() => {
34023265 $('input[name="shadow_width"]').on('input', async function (e) {
34033266 power_user.shadow_width = Number(e.target.value);
34043267 $('#shadow_width_counter').val(power_user.shadow_width);
3405- localStorage.setItem(storage_keys.shadow_width, power_user.shadow_width);
34063268 await applyShadowWidth();
34073269 saveSettingsDebounced();
34083270 });
@@ -3643,36 +3505,36 @@ $(document).ready(() => {
36433505 $('#messageTimerEnabled').on('input', function () {
36443506 const value = !!$(this).prop('checked');
36453507 power_user.timer_enabled = value;
3646- localStorage.setItem(storage_keys.timer_enabled, Boolean(power_user.timer_enabled));
36473508 switchTimer();
3509+ saveSettingsDebounced();
36483510 });
36493511
36503512 $('#messageTimestampsEnabled').on('input', function () {
36513513 const value = !!$(this).prop('checked');
36523514 power_user.timestamps_enabled = value;
3653- localStorage.setItem(storage_keys.timestamps_enabled, Boolean(power_user.timestamps_enabled));
36543515 switchTimestamps();
3516+ saveSettingsDebounced();
36553517 });
36563518
36573519 $('#messageModelIconEnabled').on('input', function () {
36583520 const value = !!$(this).prop('checked');
36593521 power_user.timestamp_model_icon = value;
3660- localStorage.setItem(storage_keys.timestamp_model_icon, Boolean(power_user.timestamp_model_icon));
36613522 switchIcons();
3523+ saveSettingsDebounced();
36623524 });
36633525
36643526 $('#messageTokensEnabled').on('input', function () {
36653527 const value = !!$(this).prop('checked');
36663528 power_user.message_token_count_enabled = value;
3667- localStorage.setItem(storage_keys.message_token_count_enabled, Boolean(power_user.message_token_count_enabled));
36683529 switchTokenCount();
3530+ saveSettingsDebounced();
36693531 });
36703532
36713533 $('#expandMessageActions').on('input', function () {
36723534 const value = !!$(this).prop('checked');
36733535 power_user.expand_message_actions = value;
3674- localStorage.setItem(storage_keys.expand_message_actions, Boolean(power_user.expand_message_actions));
36753536 switchMessageActions();
3537+ saveSettingsDebounced();
36763538 });
36773539
36783540 $('#enableZenSliders').on('input', function () {
@@ -3684,9 +3546,8 @@ $(document).ready(() => {
36843546 return;
36853547 }
36863548 power_user.enableZenSliders = value;
3687- localStorage.setItem(storage_keys.enableZenSliders, Boolean(power_user.enableZenSliders));
3688- saveSettingsDebounced();
36893549 switchZenSliders();
3550+ saveSettingsDebounced();
36903551 });
36913552
36923553 $('#enableLabMode').on('input', function () {
@@ -3699,30 +3560,29 @@ $(document).ready(() => {
36993560 }
37003561
37013562 power_user.enableLabMode = value;
3702- localStorage.setItem(storage_keys.enableLabMode, Boolean(power_user.enableLabMode));
3703- saveSettingsDebounced();
37043563 switchLabMode();
3564+ saveSettingsDebounced();
37053565 });
37063566
37073567 $('#mesIDDisplayEnabled').on('input', function () {
37083568 const value = !!$(this).prop('checked');
37093569 power_user.mesIDDisplay_enabled = value;
3710- localStorage.setItem(storage_keys.mesIDDisplay_enabled, Boolean(power_user.mesIDDisplay_enabled));
37113570 switchMesIDDisplay();
3571+ saveSettingsDebounced();
37123572 });
37133573
37143574 $('#hideChatAvatarsEnabled').on('input', function () {
37153575 const value = !!$(this).prop('checked');
37163576 power_user.hideChatAvatars_enabled = value;
3717- localStorage.setItem(storage_keys.hideChatAvatars_enabled, Boolean(power_user.hideChatAvatars_enabled));
37183577 switchHideChatAvatars();
3578+ saveSettingsDebounced();
37193579 });
37203580
37213581 $('#hotswapEnabled').on('input', function () {
37223582 const value = !!$(this).prop('checked');
37233583 power_user.hotswap_enabled = value;
3724- localStorage.setItem(storage_keys.hotswap_enabled, Boolean(power_user.hotswap_enabled));
37253584 switchHotswap();
3585+ saveSettingsDebounced();
37263586 });
37273587
37283588 $('#prefer_character_prompt').on('input', function () {
@@ -3775,6 +3635,12 @@ $(document).ready(() => {
37753635 saveSettingsDebounced();
37763636 });
37773637
3638+ $('#enable_auto_select_input').on('input', function () {
3639+ const value = !!$(this).prop('checked');
3640+ power_user.enable_auto_select_input = value;
3641+ saveSettingsDebounced();
3642+ });
3643+
37783644 $('#spoiler_free_mode').on('input', function () {
37793645 power_user.spoiler_free_mode = !!$(this).prop('checked');
37803646 switchSpoilerMode();
@@ -3824,8 +3690,8 @@ $(document).ready(() => {
38243690 $('#ui_mode_select').on('change', function () {
38253691 const value = $(this).find(':selected').val();
38263692 power_user.ui_mode = Number(value);
3827- saveSettingsDebounced();
38283693 switchSimpleMode();
3694+ saveSettingsDebounced();
38293695 });
38303696
38313697 $('#bogus_folders').on('input', function () {
@@ -3929,14 +3795,12 @@ $(document).ready(() => {
39293795
39303796 $('#reduced_motion').on('input', function () {
39313797 power_user.reduced_motion = !!$(this).prop('checked');
3932- localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
39333798 switchReducedMotion();
39343799 saveSettingsDebounced();
39353800 });
39363801
39373802 $('#compact_input_area').on('input', function () {
39383803 power_user.compact_input_area = !!$(this).prop('checked');
3939- localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
39403804 switchCompactInputArea();
39413805 saveSettingsDebounced();
39423806 });
public/scripts/secrets.js+4 -1
@@ -32,6 +32,8 @@ export const SECRET_KEYS = {
3232 ZEROONEAI: 'api_key_01ai',
3333 HUGGINGFACE: 'api_key_huggingface',
3434 STABILITY: 'api_key_stability',
35+ BLOCKENTROPY: 'api_key_blockentropy',
36+ CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
3537};
3638
3739const INPUT_MAP = {
@@ -63,6 +65,7 @@ const INPUT_MAP = {
6365 [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',
6466 [SECRET_KEYS.ZEROONEAI]: '#api_key_01ai',
6567 [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
68+ [SECRET_KEYS.BLOCKENTROPY]: '#api_key_blockentropy',
6669};
6770
6871async function clearSecret() {
@@ -125,7 +128,7 @@ export async function writeSecret(key, value) {
125128 const text = await response.text();
126129
127130 if (text == 'ok') {
128131 secret_state[key] = true!!value;
129132 updateSecretDisplay();
130133 }
131134 }
public/scripts/showdown-underscore.js+13 -3
@@ -7,9 +7,19 @@ export const markdownUnderscoreExt = () => {
77 }
88
99 return [{
1010 type: 'langoutput',
1111 regex: new RegExp('(<code(?:\\s+[^>]*)?>[\\s\\S]*?<\\/code>)|\\b(?<!_)_(?!_)(.*?)(?<!_)_(?!_)\\b', 'g'),
12- replace: '<em>$1</em>',
12+ replace: function(match, codeContent, italicContent) {
13+ if (codeContent) {
14+ // If it's inside <code> tags, return unchanged
15+ return match;
16+ } else if (italicContent) {
17+ // If it's an italic group, apply the replacement
18+ return '<em>' + italicContent + '</em>';
19+ }
20+ // If none of the conditions are met, return the original match
21+ return match;
22+ },
1323 }];
1424 } catch (e) {
1525 console.error('Error in Showdown-underscore extension:', e);
public/scripts/slash-commands.js+252 -37
@@ -1,7 +1,9 @@
11import {
22 Generate,
3+ UNIQUE_APIS,
34 activateSendButtons,
45 addOneMessage,
6+ api_server,
57 callPopup,
68 characters,
79 chat,
@@ -49,8 +51,8 @@ import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSel
4951import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
5052import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
5153import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5254import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
5355import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
5456import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
5557import { registerVariableCommands, resolveVariable } from './variables.js';
5658import { background_settings } from './backgrounds.js';
@@ -717,6 +719,7 @@ export function initDefaultSlashCommands() {
717719 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
718720 name: 'delswipe',
719721 callback: deleteSwipeCallback,
722+ returns: 'the new, currently selected swipe id',
720723 aliases: ['swipedel'],
721724 unnamedArgumentList: [
722725 SlashCommandArgument.fromProps({
@@ -912,13 +915,28 @@ export function initDefaultSlashCommands() {
912915 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
913916 name: 'addswipe',
914917 callback: addSwipeCallback,
918+ returns: 'the new swipe id',
915919 aliases: ['swipeadd'],
920+ namedArgumentList: [
921+ SlashCommandNamedArgument.fromProps({
922+ name: 'switch',
923+ description: 'switch to the new swipe',
924+ typeList: [ARGUMENT_TYPE.BOOLEAN],
925+ enumList: commonEnumProviders.boolean()(),
926+ }),
927+ ],
916928 unnamedArgumentList: [
917929 new SlashCommandArgument(
918930 'text', [ARGUMENT_TYPE.STRING], true,
919931 ),
920932 ],
921- helpString: 'Adds a swipe to the last chat message.',
933+ helpString: `
934+ <div>
935+ Adds a swipe to the last chat message.
936+ </div>
937+ <div>
938+ Use switch=true to switch to directly switch to the new swipe.
939+ </div>`,
922940 }));
923941 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
924942 name: 'stop',
@@ -1480,7 +1498,8 @@ export function initDefaultSlashCommands() {
14801498 ],
14811499 helpString: 'Sets the specified prompt manager entry/entries on or off.',
14821500 }));
14831501 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',
1502+ name: 'pick-icon',
14841503 callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
14851504 returns: 'The chosen icon name or false if cancelled.',
14861505 helpString: `
@@ -1495,6 +1514,72 @@ export function initDefaultSlashCommands() {
14951514 </div>
14961515 `,
14971516 }));
1517+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1518+ name: 'api-url',
1519+ callback: setApiUrlCallback,
1520+ returns: 'the current API url',
1521+ aliases: ['server'],
1522+ namedArgumentList: [
1523+ SlashCommandNamedArgument.fromProps({
1524+ name: 'api',
1525+ description: 'API to set/get the URL for - if not provided, current API is used',
1526+ typeList: [ARGUMENT_TYPE.STRING],
1527+ enumList: [
1528+ new SlashCommandEnumValue('custom', 'custom OpenAI-compatible', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'openai')), 'O'),
1529+ new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'),
1530+ ...Object.values(textgen_types).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')),
1531+ ],
1532+ }),
1533+ SlashCommandNamedArgument.fromProps({
1534+ name: 'connect',
1535+ description: 'Whether to auto-connect to the API after setting the URL',
1536+ typeList: [ARGUMENT_TYPE.BOOLEAN],
1537+ defaultValue: 'true',
1538+ enumList: commonEnumProviders.boolean('trueFalse')(),
1539+ }),
1540+ ],
1541+ unnamedArgumentList: [
1542+ SlashCommandArgument.fromProps({
1543+ description: 'API url to connect to',
1544+ typeList: [ARGUMENT_TYPE.STRING],
1545+ }),
1546+ ],
1547+ helpString: `
1548+ <div>
1549+ Set the API url / server url for the currently selected API, including the port. If no argument is provided, it will return the current API url.
1550+ </div>
1551+ <div>
1552+ If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API,
1553+ or consider switching to it with <code>/api</code> first.
1554+ </div>
1555+ <div>
1556+ This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible for the Chat Completion sources. If unsure which APIs are supported,
1557+ check the auto-completion of the optional <code>api</code> argument of this command.
1558+ </div>
1559+ `,
1560+ }));
1561+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1562+ name: 'tokenizer',
1563+ callback: selectTokenizerCallback,
1564+ returns: 'current tokenizer',
1565+ unnamedArgumentList: [
1566+ SlashCommandArgument.fromProps({
1567+ description: 'tokenizer name',
1568+ typeList: [ARGUMENT_TYPE.STRING],
1569+ enumList: getAvailableTokenizers().map(tokenizer =>
1570+ new SlashCommandEnumValue(tokenizer.tokenizerKey, tokenizer.tokenizerName, enumTypes.enum, enumIcons.default)),
1571+ }),
1572+ ],
1573+ helpString: `
1574+ <div>
1575+ Selects tokenizer by name. Gets the current tokenizer if no name is provided.
1576+ </div>
1577+ <div>
1578+ <strong>Available tokenizers:</strong>
1579+ <pre><code>${getAvailableTokenizers().map(t => t.tokenizerKey).join(', ')}</code></pre>
1580+ </div>
1581+ `,
1582+ }));
14981583
14991584 registerVariableCommands();
15001585}
@@ -1772,7 +1857,7 @@ async function popupCallback(args, value) {
17721857 return String(value);
17731858}
17741859
17751860async function getMessagesCallback(args, value) {
17761861 const includeNames = !isFalseBoolean(args?.names);
17771862 const includeHidden = isTrueBoolean(args?.hidden);
17781863 const role = args?.role;
@@ -1805,33 +1890,34 @@ function getMessagesCallback(args, value) {
18051890 throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
18061891 };
18071892
1808- const messages = [];
1893+ const processMessage = async (mesId) => {
1809-
1894+ const msg = chat[mesId];
1810- for (let messageId = range.start; messageId <= range.end; messageId++) {
1895+ if (!msg) {
1811- const message = chat[messageId];
1896+ console.warn(`WARN: No message found with ID ${mesId}`);
1812- if (!message) {
1897+ return null;
1813- console.warn(`WARN: No message found with ID ${messageId}`);
1814- continue;
18151898 }
18161899
18171900 if (role && !filterByRole(messagemsg)) {
18181901 console.debug(`/messages: Skipping message with ID ${messageIdmesId} due to role filter`);
18191902 continuereturn null;
18201903 }
18211904
18221905 if (!includeHidden && messagemsg.is_system) {
18231906 console.debug(`/messages: Skipping hidden message with ID ${messageIdmesId}`);
18241907 continuereturn null;
18251908 }
18261909
1827- if (includeNames) {
1910+ return includeNames ? `${msg.name}: ${msg.mes}` : msg.mes;
1828- messages.push(`${message.name}: ${message.mes}`);
1911+ };
1829- } else {
1912+
1830- messages.push(message.mes);
1913+ const messagePromises = [];
1831- }
1914+
1832- }
1915+ for (let rInd = range.start; rInd <= range.end; ++rInd)
1916+ messagePromises.push(processMessage(rInd));
18331917
1834- return messages.join('\n\n');
1918+ const messages = await Promise.all(messagePromises);
1919+
1920+ return messages.filter(m => m !== null).join('\n\n');
18351921}
18361922
18371923async function runCallback(args, name) {
@@ -2061,12 +2147,13 @@ async function generateRawCallback(args, value) {
20612147 }
20622148}
20632149
2150+/**
2151+ * Callback for the /gen command
2152+ * @param {object} args Named arguments
2153+ * @param {string} value Unnamed argument
2154+ * @returns {Promise<string>} The generated text
2155+ */
20642156async function generateCallback(args, value) {
2065- if (!value) {
2066- console.warn('WARN: No argument provided for /gen command');
2067- return;
2068- }
2069-
20702157 // Prevent generate recursion
20712158 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
20722159 const lock = isTrueBoolean(args?.lock);
@@ -2154,8 +2241,11 @@ async function echoCallback(args, value) {
21542241 }
21552242}
21562243
2157-
2244+/**
2158-async function addSwipeCallback(_, arg) {
2245+ * @param {{switch?: string}} args - named arguments
2246+ * @param {string} value - The swipe text to add (unnamed argument)
2247+ */
2248+async function addSwipeCallback(args, value) {
21592249 const lastMessage = chat[chat.length - 1];
21602250
21612251 if (!lastMessage) {
@@ -2163,7 +2253,7 @@ async function addSwipeCallback(_, arg) {
21632253 return '';
21642254 }
21652255
21662256 if (!argvalue) {
21672257 console.warn('WARN: No argument provided for /addswipe command');
21682258 return '';
21692259 }
@@ -2192,23 +2282,30 @@ async function addSwipeCallback(_, arg) {
21922282 lastMessage.swipe_info = lastMessage.swipes.map(() => ({}));
21932283 }
21942284
21952285 lastMessage.swipes.push(argvalue);
21962286 lastMessage.swipe_info.push({
21972287 send_date: getMessageTimeStamp(),
21982288 gen_started: null,
21992289 gen_finished: null,
22002290 extra: {
22012291 bias: extractMessageBias(argvalue),
22022292 gen_id: Date.now(),
22032293 api: 'manual',
22042294 model: 'slash command',
22052295 },
22062296 });
22072297
2298+ const newSwipeId = lastMessage.swipes.length - 1;
2299+
2300+ if (isTrueBoolean(args.switch)) {
2301+ lastMessage.swipe_id = newSwipeId;
2302+ lastMessage.mes = lastMessage.swipes[newSwipeId];
2303+ }
2304+
22082305 await saveChatConditional();
22092306 await reloadCurrentChat();
22102307
22112308 return ''String(newSwipeId);
22122309}
22132310
22142311async function deleteSwipeCallback(_, arg) {
@@ -2244,7 +2341,7 @@ async function deleteSwipeCallback(_, arg) {
22442341 await saveChatConditional();
22452342 await reloadCurrentChat();
22462343
22472344 return ''String(newSwipeId);
22482345}
22492346
22502347async function askCharacter(args, text) {
@@ -3223,6 +3320,7 @@ function getModelOptions() {
32233320 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
32243321 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
32253322 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
3323+ { id: 'model_blockentropy_select', api: 'openai', type: chat_completion_sources.BLOCKENTROPY },
32263324 { id: 'model_novel_select', api: 'novel', type: null },
32273325 { id: 'horde_model', api: 'koboldhorde', type: null },
32283326 ];
@@ -3391,6 +3489,123 @@ function setPromptEntryCallback(args, targetState) {
33913489 return '';
33923490}
33933491
3492+/**
3493+ * Sets the API URL and triggers the text generation web UI button click.
3494+ *
3495+ * @param {object} args - named args
3496+ * @param {string?} [args.api=null] - the API name to set/get the URL for
3497+ * @param {string?} [args.connect=true] - whether to connect to the API after setting
3498+ * @param {string} url - the API URL to set
3499+ * @returns {Promise<string>}
3500+ */
3501+async function setApiUrlCallback({ api = null, connect = 'true' }, url) {
3502+ const autoConnect = isTrueBoolean(connect);
3503+
3504+ // Special handling for Chat Completion Custom OpenAI compatible, that one can also support API url handling
3505+ const isCurrentlyCustomOpenai = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
3506+ if (api === chat_completion_sources.CUSTOM || (!api && isCurrentlyCustomOpenai)) {
3507+ if (!url) {
3508+ return oai_settings.custom_url ?? '';
3509+ }
3510+
3511+ if (!isCurrentlyCustomOpenai && autoConnect) {
3512+ toastr.warning('Custom OpenAI API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
3513+ return '';
3514+ }
3515+
3516+ $('#custom_api_url_text').val(url).trigger('input');
3517+
3518+ if (autoConnect) {
3519+ $('#api_button_openai').trigger('click');
3520+ }
3521+
3522+ return url;
3523+ }
3524+
3525+ // Special handling for Kobold Classic API
3526+ const isCurrentlyKoboldClassic = main_api === 'kobold';
3527+ if (api === 'kobold' || (!api && isCurrentlyKoboldClassic)) {
3528+ if (!url) {
3529+ return api_server ?? '';
3530+ }
3531+
3532+ if (!isCurrentlyKoboldClassic && autoConnect) {
3533+ toastr.warning('Kobold Classic API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
3534+ return '';
3535+ }
3536+
3537+ $('#api_url_text').val(url).trigger('input');
3538+ // trigger blur debounced, so we hide the autocomplete menu
3539+ setTimeout(() => $('#api_url_text').trigger('blur'), 1);
3540+
3541+ if (autoConnect) {
3542+ $('#api_button').trigger('click');
3543+ }
3544+
3545+ return api_server ?? '';
3546+ }
3547+
3548+ // Do some checks and get the api type we are targeting with this command
3549+ if (api && !Object.values(textgen_types).includes(api)) {
3550+ toastr.warning(`API '${api}' is not a valid text_gen API.`);
3551+ return '';
3552+ }
3553+ if (!api && !Object.values(textgen_types).includes(textgenerationwebui_settings.type)) {
3554+ toastr.warning(`API '${textgenerationwebui_settings.type}' is not a valid text_gen API.`);
3555+ return '';
3556+ }
3557+ if (api && url && autoConnect && api !== textgenerationwebui_settings.type) {
3558+ toastr.warning(`API '${api}' is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
3559+ return '';
3560+ }
3561+ const type = api || textgenerationwebui_settings.type;
3562+
3563+ const inputSelector = SERVER_INPUTS[type];
3564+ if (!inputSelector) {
3565+ toastr.warning(`API '${type}' does not have a server url input.`);
3566+ return '';
3567+ }
3568+
3569+ // If no url was provided, return the current one
3570+ if (!url) {
3571+ return textgenerationwebui_settings.server_urls[type] ?? '';
3572+ }
3573+
3574+ // else, we want to actually set the url
3575+ $(inputSelector).val(url).trigger('input');
3576+ // trigger blur debounced, so we hide the autocomplete menu
3577+ setTimeout(() => $(inputSelector).trigger('blur'), 1);
3578+
3579+ // Trigger the auto connect via connect button, if requested
3580+ if (autoConnect) {
3581+ $('#api_button_textgenerationwebui').trigger('click');
3582+ }
3583+
3584+ // We still re-acquire the value, as it might have been modified by the validation on connect
3585+ return textgenerationwebui_settings.server_urls[type] ?? '';
3586+}
3587+
3588+async function selectTokenizerCallback(_, name) {
3589+ if (!name) {
3590+ return getAvailableTokenizers().find(tokenizer => tokenizer.tokenizerId === power_user.tokenizer)?.tokenizerKey ?? '';
3591+ }
3592+
3593+ const tokenizers = getAvailableTokenizers();
3594+ const fuse = new Fuse(tokenizers, { keys: ['tokenizerKey', 'tokenizerName'] });
3595+ const result = fuse.search(name);
3596+
3597+ if (result.length === 0) {
3598+ toastr.warning(`Tokenizer "${name}" not found`);
3599+ return '';
3600+ }
3601+
3602+ /** @type {import('./tokenizers.js').Tokenizer} */
3603+ const foundTokenizer = result[0].item;
3604+ selectTokenizer(foundTokenizer.tokenizerId);
3605+
3606+ return foundTokenizer.tokenizerKey;
3607+}
3608+
33943609export let isExecutingCommandsFromChatInput = false;
33953610export let commandsFromChatInputAbortController;
33963611
public/scripts/tags.js+17 -9
@@ -445,7 +445,11 @@ export function getTagKeyForEntity(entityOrKey) {
445445 }
446446
447447 // Next lets check if its a valid character or character id, so we can swith it to its tag
448- const character = characters.indexOf(x) >= 0 ? x : characters[x];
448+ let character;
449+ if (!character && characters.indexOf(x) >= 0) character = x; // Check for char object
450+ if (!character && !isNaN(parseInt(entityOrKey))) character = characters[x]; // check if its a char id
451+ if (!character) character = characters.find(y => y.avatar === x); // check if its a char key
452+
449453 if (character) {
450454 x = character.avatar;
451455 }
@@ -708,12 +712,12 @@ const ANTI_TROLL_MAX_TAGS = 15;
708712 *
709713 * @param {Character} character - The character
710714 * @param {object} [options] - Options
711715 * @param {booleantag_import_setting} [options.forceShowimportSetting=falsenull] - Whether to forceForce showinga thetag import dialogsetting
712716 * @returns {Promise<boolean>} Boolean indicating whether any tag was imported
713717 */
714718async function importTags(character, { forceShowimportSetting = falsenull } = {}) {
715719 // Gather the tags to import based on the selected setting
716720 const tagNamesToImport = await handleTagImport(character, { forceShowimportSetting });
717721 if (!tagNamesToImport?.length) {
718722 console.debug('No tags to import');
719723 return;
@@ -722,7 +726,11 @@ async function importTags(character, { forceShow = false } = {}) {
722726 const tagsToImport = tagNamesToImport.map(tag => getTag(tag, { createNew: true }));
723727 const added = addTagsToEntity(tagsToImport, character.avatar);
724728
729+ if (added) {
725730 toastr.success(`Imported tags:<br />${tagsToImport.map(x => x.name).join(', ')}`, 'Importing Tags', { escapeHtml: false });
731+ } else {
732+ toastr.error(`Couldn't import tags:<br />${tagsToImport.map(x => x.name).join(', ')}`, 'Importing Tags', { escapeHtml: false });
733+ }
726734
727735 return added;
728736}
@@ -732,10 +740,10 @@ async function importTags(character, { forceShow = false } = {}) {
732740 *
733741 * @param {Character} character - The character
734742 * @param {object} [options] - Options
735743 * @param {booleantag_import_setting} [options.forceShowimportSetting=falsenull] - Whether to forceForce showinga thetag import dialogsetting
736744 * @returns {Promise<string[]>} Array of strings representing the tags to import
737745 */
738746async function handleTagImport(character, { forceShowimportSetting = falsenull } = {}) {
739747 /** @type {string[]} */
740748 const importTags = character.tags.map(t => t.trim()).filter(t => t)
741749 .filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))
@@ -745,9 +753,9 @@ async function handleTagImport(character, { forceShow = false } = {}) {
745753 .map(newTag);
746754 const folderTags = getOpenBogusFolders();
747755
748756 // Choose the setting for this dialog. IfFirst fromcheck settingsoverride, verifythen thesaved setting really exists,or otherwisefinally takeuse "ASK".
749757 const setting = forceShowimportSetting ? tag_import_setting.ASKimportSetting :
750758 : Object.values(tag_import_setting).find(setting => setting === power_user.tag_import_setting) ?? tag_import_setting.ASK;
751759
752760 switch (setting) {
753761 case tag_import_setting.ALL:
public/scripts/templates/installExtension.html+7 -0
@@ -0,0 +1,7 @@
1+<h3>Enter the Git URL of the extension to install</h3>
2+<br>
3+<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose
4+ security risks. Always make sure you trust the source before importing an extension. We are not responsible for any
5+ damage caused by third-party extensions.</p>
6+<br>
7+<p>Example: <tt> https://github.com/author/extension-name </tt></p>
public/scripts/templates/worldInfoKeywordHeaders.html+8 -0
@@ -0,0 +1,8 @@
1+<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 4.5em;">
2+ <small class="flex1" data-i18n="Title/Memo">Title/Memo</small>
3+ <small style="width: calc(3.5em + 15px)" data-i18n="Strategy">Strategy</small>
4+ <small style="width: calc(3.5em + 30px)" data-i18n="Position">Position</small>
5+ <small style="width: calc(3.5em + 20px)" data-i18n="Depth">Depth</small>
6+ <small style="width: calc(3.5em + 20px)" data-i18n="Order">Order</small>
7+ <small style="width: calc(3.5em + 15px)" data-i18n="Trigger %">Trigger %</small>
8+</div>
public/scripts/textgen-models.js+7 -2
@@ -599,6 +599,10 @@ export function getCurrentOpenRouterModelTokenizer() {
599599 return tokenizers.YI;
600600 case 'Mistral':
601601 return tokenizers.MISTRAL;
602+ case 'Gemini':
603+ return tokenizers.GEMMA;
604+ case 'Claude':
605+ return tokenizers.CLAUDE;
602606 default:
603607 return tokenizers.OPENAI;
604608 }
@@ -618,7 +622,7 @@ export function getCurrentDreamGenModelTokenizer() {
618622 }
619623}
620624
621625jQuery(export function initTextGenModels() {
622626 $('#mancer_model').on('change', onMancerModelSelect);
623627 $('#model_togetherai_select').on('change', onTogetherModelSelect);
624628 $('#model_infermaticai_select').on('change', onInfermaticAIModelSelect);
@@ -708,6 +712,7 @@ jQuery(function () {
708712 searchInputPlaceholder: 'Search providers...',
709713 searchInputCssClass: 'text_pole',
710714 width: '100%',
715+ closeOnSelect: false,
711716 });
712717 providersSelect.on('select2:select', function (/** @type {any} */ evt) {
713718 const element = evt.params.data.element;
@@ -718,4 +723,4 @@ jQuery(function () {
718723 $(this).trigger('change');
719724 });
720725 }
721726});
public/scripts/textgen-settings.js+30 -2
@@ -94,7 +94,7 @@ let DREAMGEN_SERVER = 'https://dreamgen.com';
9494let OPENROUTER_SERVER = 'https://openrouter.ai/api';
9595let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';
9696
9797export const SERVER_INPUTS = {
9898 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
9999 [textgen_types.VLLM]: '#vllm_api_url_text',
100100 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',
@@ -1064,6 +1064,34 @@ function getLogprobsNumber() {
10641064 return 10;
10651065}
10661066
1067+/**
1068+ * Replaces {{macro}} in a comma-separated or serialized JSON array string.
1069+ * @param {string} str Input string
1070+ * @returns {string} Output string
1071+ */
1072+function replaceMacrosInList(str) {
1073+ if (!str || typeof str !== 'string') {
1074+ return str;
1075+ }
1076+
1077+ try {
1078+ const array = JSON.parse(str);
1079+ if (!Array.isArray(array)) {
1080+ throw new Error('Not an array');
1081+ }
1082+ for (let i = 0; i < array.length; i++) {
1083+ array[i] = substituteParams(array[i]);
1084+ }
1085+ return JSON.stringify(array);
1086+ } catch {
1087+ const array = str.split(',');
1088+ for (let i = 0; i < array.length; i++) {
1089+ array[i] = substituteParams(array[i]);
1090+ }
1091+ return array.join(',');
1092+ }
1093+}
1094+
10671095export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
10681096 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
10691097 const dynatemp = isDynamicTemperatureSupported();
@@ -1103,7 +1131,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
11031131 'dry_allowed_length': settings.dry_allowed_length,
11041132 'dry_multiplier': settings.dry_multiplier,
11051133 'dry_base': settings.dry_base,
11061134 'dry_sequence_breakers': replaceMacrosInList(settings.dry_sequence_breakers),
11071135 'dry_penalty_last_n': settings.dry_penalty_last_n,
11081136 'max_tokens_second': settings.max_tokens_second,
11091137 'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined,
public/scripts/tokenizers.js+72 -3
@@ -26,6 +26,7 @@ export const tokenizers = {
2626 API_KOBOLD: 10,
2727 CLAUDE: 11,
2828 LLAMA3: 12,
29+ GEMMA: 13,
2930 BEST_MATCH: 99,
3031};
3132
@@ -34,6 +35,7 @@ export const SENTENCEPIECE_TOKENIZERS = [
3435 tokenizers.MISTRAL,
3536 tokenizers.YI,
3637 tokenizers.LLAMA3,
38+ tokenizers.GEMMA,
3739 // uncomment when NovelAI releases Kayra and Clio weights, lol
3840 //tokenizers.NERD,
3941 //tokenizers.NERD2,
@@ -91,6 +93,11 @@ const TOKENIZER_URLS = {
9193 decode: '/api/tokenizers/llama3/decode',
9294 count: '/api/tokenizers/llama3/encode',
9395 },
96+ [tokenizers.GEMMA]: {
97+ encode: '/api/tokenizers/gemma/encode',
98+ decode: '/api/tokenizers/gemma/decode',
99+ count: '/api/tokenizers/gemma/encode',
100+ },
94101 [tokenizers.API_TEXTGENERATIONWEBUI]: {
95102 encode: '/api/tokenizers/remote/textgenerationwebui/encode',
96103 count: '/api/tokenizers/remote/textgenerationwebui/encode',
@@ -141,9 +148,45 @@ async function resetTokenCache() {
141148}
142149
143150/**
151+ * @typedef {object} Tokenizer
152+ * @property {number} tokenizerId - The id of the tokenizer option
153+ * @property {string} tokenizerKey - Internal name/key of the tokenizer
154+ * @property {string} tokenizerName - Human-readable detailed name of the tokenizer (as displayed in the UI)
155+ */
156+
157+/**
158+ * Gets all tokenizers available to the user.
159+ * @returns {Tokenizer[]} Tokenizer info.
160+ */
161+export function getAvailableTokenizers() {
162+ const tokenizerOptions = $('#tokenizer').find('option').toArray();
163+ return tokenizerOptions.map(tokenizerOption => ({
164+ tokenizerId: Number(tokenizerOption.value),
165+ tokenizerKey: Object.entries(tokenizers).find(([_, value]) => value === Number(tokenizerOption.value))[0].toLocaleLowerCase(),
166+ tokenizerName: tokenizerOption.text,
167+ }))
168+}
169+
170+/**
171+ * Selects tokenizer if not already selected.
172+ * @param {number} tokenizerId Tokenizer ID.
173+ */
174+export function selectTokenizer(tokenizerId) {
175+ if (tokenizerId !== power_user.tokenizer) {
176+ const tokenizer = getAvailableTokenizers().find(tokenizer => tokenizer.tokenizerId === tokenizerId);
177+ if (!tokenizer) {
178+ console.warn('Failed to find tokenizer with id', tokenizerId);
179+ return;
180+ }
181+ $('#tokenizer').val(tokenizer.tokenizerId).trigger('change');
182+ toastr.info(`Tokenizer: "${tokenizer.tokenizerName}" selected`);
183+ }
184+}
185+
186+/**
144187 * Gets the friendly name of the current tokenizer.
145188 * @param {string} forApi API to get the tokenizer for. Defaults to the main API.
146189 * @returns { { tokenizerName: string, tokenizerId: number } Tokenizer} Tokenizer info
147190 */
148191export function getFriendlyTokenizerName(forApi) {
149192 if (!forApi) {
@@ -178,7 +221,9 @@ export function getFriendlyTokenizerName(forApi) {
178221 ? tokenizers.OPENAI
179222 : tokenizerId;
180223
181- return { tokenizerName, tokenizerId };
224+ const tokenizerKey = Object.entries(tokenizers).find(([_, value]) => value === tokenizerId)[0].toLocaleLowerCase();
225+
226+ return { tokenizerName, tokenizerKey, tokenizerId };
182227}
183228
184229/**
@@ -232,6 +277,9 @@ export function getTokenizerBestMatch(forApi) {
232277 if (model.includes('mistral') || model.includes('mixtral')) {
233278 return tokenizers.MISTRAL;
234279 }
280+ if (model.includes('gemma')) {
281+ return tokenizers.GEMMA;
282+ }
235283 }
236284
237285 return tokenizers.LLAMA;
@@ -441,12 +489,14 @@ export function getTokenizerModel() {
441489 const turbo0301Tokenizer = 'gpt-3.5-turbo-0301';
442490 const turboTokenizer = 'gpt-3.5-turbo';
443491 const gpt4Tokenizer = 'gpt-4';
492+ const gpt4oTokenizer = 'gpt-4o';
444493 const gpt2Tokenizer = 'gpt2';
445494 const claudeTokenizer = 'claude';
446495 const llamaTokenizer = 'llama';
447496 const llama3Tokenizer = 'llama3';
448497 const mistralTokenizer = 'mistral';
449498 const yiTokenizer = 'yi';
499+ const gemmaTokenizer = 'gemma';
450500
451501 // Assuming no one would use it for different models.. right?
452502 if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
@@ -491,6 +541,12 @@ export function getTokenizerModel() {
491541 else if (model?.architecture?.tokenizer === 'Yi') {
492542 return yiTokenizer;
493543 }
544+ else if (model?.architecture?.tokenizer === 'Gemini') {
545+ return gemmaTokenizer;
546+ }
547+ else if (oai_settings.openrouter_model.includes('gpt-4o')) {
548+ return gpt4oTokenizer;
549+ }
494550 else if (oai_settings.openrouter_model.includes('gpt-4')) {
495551 return gpt4Tokenizer;
496552 }
@@ -509,7 +565,7 @@ export function getTokenizerModel() {
509565 }
510566
511567 if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
512568 return oai_settings.google_modelgemmaTokenizer;
513569 }
514570
515571 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
@@ -543,12 +599,24 @@ export function getTokenizerModel() {
543599 if (oai_settings.groq_model.includes('mistral') || oai_settings.groq_model.includes('mixtral')) {
544600 return mistralTokenizer;
545601 }
602+ if (oai_settings.groq_model.includes('gemma')) {
603+ return gemmaTokenizer;
604+ }
546605 }
547606
548607 if (oai_settings.chat_completion_source === chat_completion_sources.ZEROONEAI) {
549608 return yiTokenizer;
550609 }
551610
611+ if (oai_settings.chat_completion_source === chat_completion_sources.BLOCKENTROPY) {
612+ if (oai_settings.blockentropy_model.includes('llama3')) {
613+ return llama3Tokenizer;
614+ }
615+ if (oai_settings.blockentropy_model.includes('miqu') || oai_settings.blockentropy_model.includes('mixtral')) {
616+ return mistralTokenizer;
617+ }
618+ }
619+
552620 // Default to Turbo 3.5
553621 return turboTokenizer;
554622}
@@ -770,6 +838,7 @@ function getTextgenAPITokenizationParams(str) {
770838 url: getTextGenServer(),
771839 legacy_api: textgen_settings.legacy_api && (textgen_settings.type === OOBA || textgen_settings.type === APHRODITE),
772840 vllm_model: textgen_settings.vllm_model,
841+ aphrodite_model: textgen_settings.aphrodite_model,
773842 };
774843}
775844
public/scripts/utils.js+38 -7
@@ -498,9 +498,8 @@ export function restoreCaretPosition(element, position) {
498498}
499499
500500export async function resetScrollHeight(element) {
501- let scrollHeight = $(element).prop('scrollHeight');
502501 $(element).css('height', '0px');
503502 $(element).css('height', $(element).prop('scrollHeight') + 3 + 'px');
504503}
505504
506505/**
@@ -1729,20 +1728,24 @@ export function select2ModifyOptions(element, items, { select = false, changeEve
17291728 /** @type {Select2Option[]} */
17301729 const dataItems = items.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
17311730
17321731 const existingValuesoptionsToSelect = [];
1732+ const newOptions = [];
1733+
17331734 dataItems.forEach(item => {
17341735 // Set the value, creating a new option if necessary
17351736 if (element.find('option[value=\'' + item.id + '\']').length) {
17361737 if (select) existingValuesoptionsToSelect.push(item.id);
17371738 } else {
17381739 // Create a DOM Option and optionally pre-select by default
17391740 var newOption = new Option(item.text, item.id, select, select);
17401741 // Append it to the select
17411742 elementnewOptions.appendpush(newOption);
17421743 if (select) elementoptionsToSelect.triggerpush('change', changeEventArgsitem.id);
17431744 }
1744- if (existingValues.length) element.val(existingValues).trigger('change', changeEventArgs);
17451745 });
1746+
1747+ element.append(newOptions);
1748+ if (optionsToSelect.length) element.val(optionsToSelect).trigger('change', changeEventArgs);
17461749}
17471750
17481751/**
@@ -1931,6 +1934,34 @@ export function getFreeName(name, list, numberFormatter = (n) => ` #${n}`) {
19311934 return `${name}${numberFormatter(counter)}`;
19321935}
19331936
1937+
1938+/**
1939+ * Toggles the visibility of a drawer by changing the display style of its content.
1940+ * This function skips the usual drawer animation.
1941+ *
1942+ * @param {HTMLElement} drawer - The drawer element to toggle
1943+ * @param {boolean} [expand=true] - Whether to expand or collapse the drawer
1944+ */
1945+export function toggleDrawer(drawer, expand = true) {
1946+ /** @type {HTMLElement} */
1947+ const icon = drawer.querySelector('.inline-drawer-icon');
1948+ /** @type {HTMLElement} */
1949+ const content = drawer.querySelector('.inline-drawer-content');
1950+
1951+ if (expand) {
1952+ icon.classList.remove('up', 'fa-circle-chevron-up');
1953+ icon.classList.add('down', 'fa-circle-chevron-down');
1954+ content.style.display = 'block';
1955+ } else {
1956+ icon.classList.remove('down', 'fa-circle-chevron-down');
1957+ icon.classList.add('up', 'fa-circle-chevron-up');
1958+ content.style.display = 'none';
1959+ }
1960+
1961+ // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height
1962+ content.querySelectorAll('textarea.autoSetHeight').forEach(resetScrollHeight);
1963+}
1964+
19341965export async function fetchFaFile(name) {
19351966 const style = document.createElement('style');
19361967 style.innerHTML = await (await fetch(`/css/${name}`)).text();
public/scripts/world-info.js+60 -63
@@ -17,6 +17,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
1717import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
1818import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
1919import { StructuredCloneMap } from './util/StructuredCloneMap.js';
20+import { renderTemplateAsync } from './templates.js';
2021
2122export const world_info_insertion_strategy = {
2223 evenly: 0,
@@ -72,6 +73,7 @@ export let world_info_match_whole_words = false;
7273export let world_info_use_group_scoring = false;
7374export let world_info_character_strategy = world_info_insertion_strategy.character_first;
7475export let world_info_budget_cap = 0;
76+export let world_info_max_recursion_steps = 0;
7577const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), debounce_timeout.relaxed);
7678const saveSettingsDebounced = debounce(() => {
7779 Object.assign(world_info, { globalSelect: selected_world_info });
@@ -709,6 +711,7 @@ export function getWorldInfoSettings() {
709711 world_info_character_strategy,
710712 world_info_budget_cap,
711713 world_info_use_group_scoring,
714+ world_info_max_recursion_steps,
712715 };
713716}
714717
@@ -795,6 +798,8 @@ export function setWorldInfoSettings(settings, data) {
795798 world_info_budget_cap = Number(settings.world_info_budget_cap);
796799 if (settings.world_info_use_group_scoring !== undefined)
797800 world_info_use_group_scoring = Boolean(settings.world_info_use_group_scoring);
801+ if (settings.world_info_max_recursion_steps !== undefined)
802+ world_info_max_recursion_steps = Number(settings.world_info_max_recursion_steps);
798803
799804 // Migrate old settings
800805 if (world_info_budget > 100) {
@@ -843,6 +848,9 @@ export function setWorldInfoSettings(settings, data) {
843848 $('#world_info_budget_cap').val(world_info_budget_cap);
844849 $('#world_info_budget_cap_counter').val(world_info_budget_cap);
845850
851+ $('#world_info_max_recursion_steps').val(world_info_max_recursion_steps);
852+ $('#world_info_max_recursion_steps_counter').val(world_info_max_recursion_steps);
853+
846854 world_names = data.world_names?.length ? data.world_names : [];
847855
848856 // Add to existing selected WI if it exists
@@ -1854,28 +1862,9 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
18541862 worldEntriesList.find('*').off();
18551863 worldEntriesList.empty();
18561864
18571865 const keywordHeaders = `await renderTemplateAsync('worldInfoKeywordHeaders');
1858- <div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 4.5em;">
1866+ const blocksPromises = page.map(async (entry) => await getWorldEntry(name, data, entry)).filter(x => x);
1859- <small class="flex1">
1867+ const blocks = await Promise.all(blocksPromises);
1860- Title/Memo
1861- </small>
1862- <small style="width: calc(3.5em + 15px)">
1863- Status
1864- </small>
1865- <small style="width: calc(3.5em + 30px)">
1866- Position
1867- </small>
1868- <small style="width: calc(3.5em + 20px)">
1869- Depth
1870- </small>
1871- <small style="width: calc(3.5em + 20px)">
1872- Order
1873- </small>
1874- <small style="width: calc(3.5em + 15px)">
1875- Trigger %
1876- </small>
1877- </div>`;
1878- const blocks = page.map(entry => getWorldEntry(name, data, entry)).filter(x => x);
18791868 const isCustomOrder = $('#world_info_sort_order').find(':selected').data('rule') === 'custom';
18801869 if (!isCustomOrder) {
18811870 blocks.forEach(block => {
@@ -2275,7 +2264,7 @@ export function parseRegexFromString(input) {
22752264 }
22762265}
22772266
22782267async function getWorldEntry(name, data, entry) {
22792268 if (!data.entries[entry.uid]) {
22802269 return;
22812270 }
@@ -2317,6 +2306,9 @@ function getWorldEntry(name, data, entry) {
23172306 }
23182307
23192308 if (isFancyInput) {
2309+ // First initialize existing values as options, before initializing select2, to speed up performance
2310+ select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
2311+
23202312 input.select2({
23212313 ajax: dynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
23222314 tags: true,
@@ -2358,8 +2350,6 @@ function getWorldEntry(name, data, entry) {
23582350 input.next('span.select2-container').find('textarea')
23592351 .val(key).trigger('input');
23602352 }, { openDrawer: true });
2361-
2362- select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
23632353 }
23642354 else {
23652355 // Compatibility with mobile devices. On mobile we need a text input field, not a select option control, so we need its own event handlers
@@ -2476,7 +2466,7 @@ function getWorldEntry(name, data, entry) {
24762466 if (!isMobile()) {
24772467 $(characterFilter).select2({
24782468 width: '100%',
24792469 placeholder: 'AllTie this entry to specific characters willor pullcharacters fromwith thisspecific entry.tags',
24802470 allowClear: true,
24812471 closeOnSelect: false,
24822472 });
@@ -2876,21 +2866,7 @@ function getWorldEntry(name, data, entry) {
28762866 //add UID above content box (less important doesn't need to be always visible)
28772867 template.find('.world_entry_form_uid_value').text(`(UID: ${entry.uid})`);
28782868
2879- // disable
2869+ //new tri-state selector for constant/normal/vectorized
2880- /*
2881- const disableInput = template.find('input[name="disable"]');
2882- disableInput.data("uid", entry.uid);
2883- disableInput.on("input", async function () {
2884- const uid = $(this).data("uid");
2885- const value = $(this).prop("checked");
2886- data.entries[uid].disable = value;
2887- setOriginalDataValue(data, uid, "enabled", !data.entries[uid].disable);
2888- await saveWorldInfo(name, data);
2889- });
2890- disableInput.prop("checked", entry.disable).trigger("input");
2891- */
2892-
2893- //new tri-state selector for constant/normal/disabled
28942870 const entryStateSelector = template.find('select[name="entryStateSelector"]');
28952871 entryStateSelector.data('uid', entry.uid);
28962872 entryStateSelector.on('click', function (event) {
@@ -2903,49 +2879,43 @@ function getWorldEntry(name, data, entry) {
29032879 switch (value) {
29042880 case 'constant':
29052881 data.entries[uid].constant = true;
2906- data.entries[uid].disable = false;
29072882 data.entries[uid].vectorized = false;
2908- setWIOriginalDataValue(data, uid, 'enabled', true);
29092883 setWIOriginalDataValue(data, uid, 'constant', true);
29102884 setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
2911- template.removeClass('disabledWIEntry');
29122885 break;
29132886 case 'normal':
29142887 data.entries[uid].constant = false;
2915- data.entries[uid].disable = false;
29162888 data.entries[uid].vectorized = false;
2917- setWIOriginalDataValue(data, uid, 'enabled', true);
29182889 setWIOriginalDataValue(data, uid, 'constant', false);
29192890 setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
2920- template.removeClass('disabledWIEntry');
29212891 break;
29222892 case 'vectorized':
29232893 data.entries[uid].constant = false;
2924- data.entries[uid].disable = false;
29252894 data.entries[uid].vectorized = true;
2926- setWIOriginalDataValue(data, uid, 'enabled', true);
29272895 setWIOriginalDataValue(data, uid, 'constant', false);
29282896 setWIOriginalDataValue(data, uid, 'extensions.vectorized', true);
2929- template.removeClass('disabledWIEntry');
2930- break;
2931- case 'disabled':
2932- data.entries[uid].constant = false;
2933- data.entries[uid].disable = true;
2934- data.entries[uid].vectorized = false;
2935- setWIOriginalDataValue(data, uid, 'enabled', false);
2936- setWIOriginalDataValue(data, uid, 'constant', false);
2937- setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
2938- template.addClass('disabledWIEntry');
29392897 break;
29402898 }
29412899 await saveWorldInfo(name, data);
29422900
29432901 });
29442902
2903+ const entryKillSwitch = template.find('div[name="entryKillSwitch"]');
2904+ entryKillSwitch.data('uid', entry.uid);
2905+ entryKillSwitch.on('click', async function (event) {
2906+ const uid = entry.uid;
2907+ data.entries[uid].disable = !data.entries[uid].disable;
2908+ const isActive = !data.entries[uid].disable;
2909+ setWIOriginalDataValue(data, uid, 'enabled', isActive);
2910+ template.toggleClass('disabledWIEntry', !isActive);
2911+ entryKillSwitch.toggleClass('fa-toggle-off', !isActive);
2912+ entryKillSwitch.toggleClass('fa-toggle-on', isActive);
2913+ await saveWorldInfo(name, data);
2914+
2915+ });
2916+
29452917 const entryState = function () {
29462918 if (entry.disableconstant === true) {
2947- return 'disabled';
2948- } else if (entry.constant === true) {
29492919 return 'constant';
29502920 } else if (entry.vectorized === true) {
29512921 return 'vectorized';
@@ -2953,6 +2923,12 @@ function getWorldEntry(name, data, entry) {
29532923 return 'normal';
29542924 }
29552925 };
2926+
2927+ const isActive = !entry.disable;
2928+ template.toggleClass('disabledWIEntry', !isActive);
2929+ entryKillSwitch.toggleClass('fa-toggle-off', !isActive);
2930+ entryKillSwitch.toggleClass('fa-toggle-on', isActive);
2931+
29562932 template
29572933 .find(`select[name="entryStateSelector"] option[value=${entryState()}]`)
29582934 .prop('selected', true)
@@ -3754,6 +3730,12 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
37543730 console.debug(`[WI] --- SEARCHING ENTRIES (on ${sortedEntries.length} entries) ---`);
37553731
37563732 while (scanState) {
3733+ //if world_info_max_recursion_steps is non-zero min activations are disabled, and vice versa
3734+ if (world_info_max_recursion_steps && world_info_max_recursion_steps <= count) {
3735+ console.debug('[WI] Search stopped by reaching max recursion steps', world_info_max_recursion_steps);
3736+ break;
3737+ }
3738+
37573739 // Track how many times the loop has run. May be useful for debugging.
37583740 count++;
37593741
@@ -4793,8 +4775,13 @@ jQuery(() => {
47934775
47944776 $('#world_info_min_activations').on('input', function () {
47954777 world_info_min_activations = Number($(this).val());
47964778 $('#world_info_min_activations_counter').val($(this).val()world_info_min_activations);
4779+
4780+ if (world_info_min_activations !== 0) {
4781+ $('#world_info_max_recursion_steps').val(0).trigger('input');
4782+ } else {
47974783 saveSettings();
4784+ }
47984785 });
47994786
48004787 $('#world_info_min_activations_depth_max').on('input', function () {
@@ -4850,6 +4837,16 @@ jQuery(() => {
48504837 saveSettings();
48514838 });
48524839
4840+ $('#world_info_max_recursion_steps').on('input', function () {
4841+ world_info_max_recursion_steps = Number($(this).val());
4842+ $('#world_info_max_recursion_steps_counter').val(world_info_max_recursion_steps);
4843+ if (world_info_max_recursion_steps !== 0) {
4844+ $('#world_info_min_activations').val(0).trigger('input');
4845+ } else {
4846+ saveSettings();
4847+ }
4848+ });
4849+
48534850 $('#world_button').on('click', async function (event) {
48544851 const chid = $('#set_character_world').data('chid');
48554852
public/style.css+25 -0
@@ -3515,6 +3515,8 @@ grammarly-extension {
35153515
35163516.drag-handle {
35173517 cursor: grab;
3518+ /* Make the drag handle not selectable in most browsers */
3519+ user-select: none;
35183520}
35193521
35203522#form_rename_chat {
@@ -4577,6 +4579,7 @@ a {
45774579 image-rendering: -webkit-optimize-contrast;
45784580}
45794581
4582+.mes_img_swipes,
45804583.mes_img_controls {
45814584 position: absolute;
45824585 top: 0.1em;
@@ -4586,9 +4589,16 @@ a {
45864589 opacity: 0;
45874590 flex-direction: row;
45884591 justify-content: space-between;
4592+ align-items: center;
45894593 padding: 1em;
45904594}
45914595
4596+.mes_img_swipes {
4597+ top: unset;
4598+ bottom: 0.1rem;
4599+}
4600+
4601+.mes_img_swipes .right_menu_button,
45924602.mes_img_controls .right_menu_button {
45934603 filter: brightness(90%);
45944604 text-shadow: 1px 1px var(--SmartThemeShadowColor) !important;
@@ -4597,16 +4607,20 @@ a {
45974607 width: 1.25em;
45984608}
45994609
4610+.mes_img_swipes .right_menu_button::before,
46004611.mes_img_controls .right_menu_button::before {
46014612 /* Fix weird alignment with this font-awesome icons on focus */
46024613 position: relative;
46034614 top: 0.6125em;
46044615}
46054616
4617+.mes_img_swipes .right_menu_button:hover,
46064618.mes_img_controls .right_menu_button:hover {
46074619 filter: brightness(150%);
46084620}
46094621
4622+.mes_img_container:hover .mes_img_swipes,
4623+.mes_img_container:focus-within .mes_img_swipes,
46104624.mes_img_container:hover .mes_img_controls,
46114625.mes_img_container:focus-within .mes_img_controls {
46124626 opacity: 1;
@@ -4620,6 +4634,17 @@ body:not(.caption) .mes_img_caption {
46204634 display: none;
46214635}
46224636
4637+.mes_img_container:not(.img_swipes) .mes_img_swipes,
4638+body:not(.sd) .mes_img_swipes {
4639+ display: none;
4640+}
4641+
4642+.mes_img_swipe_counter {
4643+ font-weight: 600;
4644+ filter: drop-shadow(2px 4px 6px black);
4645+ cursor: default;
4646+}
4647+
46234648.img_enlarged_holder {
46244649 /* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */
46254650 min-height: 120px;
server.js+212 -31
@@ -43,6 +43,8 @@ const {
4343 getConfigValue,
4444 color,
4545 forwardFetchResponse,
46+ removeColorFormatting,
47+ getSeparator,
4648} = require('./src/util');
4749const { ensureThumbnailCache } = require('./src/endpoints/thumbnails');
4850
@@ -54,9 +56,6 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
5456 if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
5557}
5658
57-// Set default DNS resolution order to IPv4 first
58-dns.setDefaultResultOrder('ipv4first');
59-
6059const DEFAULT_PORT = 8000;
6160const DEFAULT_AUTORUN = false;
6261const DEFAULT_LISTEN = false;
@@ -66,16 +65,46 @@ const DEFAULT_ACCOUNTS = false;
6665const DEFAULT_CSRF_DISABLED = false;
6766const DEFAULT_BASIC_AUTH = false;
6867
68+const DEFAULT_ENABLE_IPV6 = false;
69+const DEFAULT_ENABLE_IPV4 = true;
70+
71+const DEFAULT_PREFER_IPV6 = false;
72+
73+const DEFAULT_AVOID_LOCALHOST = false;
74+
75+const DEFAULT_AUTORUN_HOSTNAME = 'auto';
76+const DEFAULT_AUTORUN_PORT = -1;
77+
6978const cliArguments = yargs(hideBin(process.argv))
7079 .usage('Usage: <your-start-script> <command> [options]')
7180 .option('portenableIPv6', {
81+ type: 'boolean',
82+ default: null,
83+ describe: `Enables IPv6.\n[config default: ${DEFAULT_ENABLE_IPV6}]`,
84+ }).option('enableIPv4', {
85+ type: 'boolean',
86+ default: null,
87+ describe: `Enables IPv4.\n[config default: ${DEFAULT_ENABLE_IPV4}]`,
88+ }).option('port', {
7289 type: 'number',
7390 default: null,
7491 describe: `Sets the port under which SillyTavern will run.\nIf not provided falls back to yaml config 'port'.\n[config default: ${DEFAULT_PORT}]`,
92+ }).option('dnsPreferIPv6', {
93+ type: 'boolean',
94+ default: null,
95+ describe: `Prefers IPv6 for dns\nyou should probably have the enabled if you're on an IPv6 only network\nIf not provided falls back to yaml config 'preferIPv6'.\n[config default: ${DEFAULT_PREFER_IPV6}]`,
7596 }).option('autorun', {
7697 type: 'boolean',
7798 default: null,
7899 describe: `Automatically launch SillyTavern in the browser.\nAutorun is automatically disabled if --ssl is set to true.\nIf not provided falls back to yaml config 'autorun'.\n[config default: ${DEFAULT_AUTORUN}]`,
100+ }).option('autorunHostname', {
101+ type: 'string',
102+ default: null,
103+ describe: 'the autorun hostname, probably best left on \'auto\'.\nuse values like \'localhost\', \'st.example.com\'',
104+ }).option('autorunPortOverride', {
105+ type: 'string',
106+ default: null,
107+ describe: 'Overrides the port for autorun with open your browser with this port and ignore what port the server is running on. -1 is use server port',
79108 }).option('listen', {
80109 type: 'boolean',
81110 default: null,
@@ -108,6 +137,10 @@ const cliArguments = yargs(hideBin(process.argv))
108137 type: 'string',
109138 default: null,
110139 describe: 'Root directory for data storage',
140+ }).option('avoidLocalhost', {
141+ type: 'boolean',
142+ default: null,
143+ describe: 'Avoids using \'localhost\' for autorun in auto mode.\nuse if you don\'t have \'localhost\' in your hosts file',
111144 }).option('basicAuthMode', {
112145 type: 'boolean',
113146 default: null,
@@ -138,6 +171,31 @@ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
138171
139172const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
140173
174+const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
175+const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
176+
177+const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
178+const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
179+
180+const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
181+
182+const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
183+
184+if (dnsPreferIPv6) {
185+ // Set default DNS resolution order to IPv6 first
186+ dns.setDefaultResultOrder('ipv6first');
187+ console.log('Preferring IPv6 for DNS resolution');
188+} else {
189+ // Set default DNS resolution order to IPv4 first
190+ dns.setDefaultResultOrder('ipv4first');
191+ console.log('Preferring IPv4 for DNS resolution');
192+}
193+
194+if (!enableIPv6 && !enableIPv4) {
195+ console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
196+ process.exit(1);
197+}
198+
141199// CORS Settings //
142200const CORS = cors({
143201 origin: 'null',
@@ -546,15 +604,15 @@ app.use('/api/speech', require('./src/endpoints/speech').router);
546604// Azure TTS
547605app.use('/api/azure', require('./src/endpoints/azure').router);
548606
549607const tavernUrltavernUrlV6 = new URL(
550608 (cliArguments.ssl ? 'https://' : 'http://') +
551609 (listen ? '0.0.0.0[::]' : '127.0.0.[::1]') +
552610 (':' + server_port),
553611);
554612
555613const autorunUrltavernUrl = new URL(
556614 (cliArguments.ssl ? 'https://' : 'http://') +
557615 (listen ? '0.0.0.0' : '127.0.0.1') +
558616 (':' + server_port),
559617);
560618
@@ -607,19 +665,67 @@ const preSetupTasks = async function () {
607665};
608666
609667/**
668+ * Gets the hostname to use for autorun in the browser.
669+ * @returns {string} The hostname to use for autorun
670+ */
671+function getAutorunHostname() {
672+ if (autorunHostname === 'auto') {
673+ if (enableIPv6 && enableIPv4) {
674+ if (avoidLocalhost) return '[::1]';
675+ return 'localhost';
676+ }
677+
678+ if (enableIPv6) {
679+ return '[::1]';
680+ }
681+
682+ if (enableIPv4) {
683+ return '127.0.0.1';
684+ }
685+ }
686+
687+ return autorunHostname;
688+}
689+
690+/**
610691 * Tasks that need to be run after the server starts listening.
692+ * @param {boolean} v6Failed If the server failed to start on IPv6
693+ * @param {boolean} v4Failed If the server failed to start on IPv4
611694 */
612695const postSetupTasks = async function (v6Failed, v4Failed) {
696+ const autorunUrl = new URL(
697+ (cliArguments.ssl ? 'https://' : 'http://') +
698+ (getAutorunHostname()) +
699+ (':') +
700+ ((autorunPortOverride >= 0) ? autorunPortOverride : server_port),
701+ );
702+
613703 console.log('Launching...');
614704
615705 if (autorun) open(autorunUrl.toString());
616706
617707 setWindowTitle('SillyTavern WebServer');
618708
619709 console.log(color.green(let logListen = 'SillyTavern is listening on: ' + tavernUrl));
710+
711+ if (enableIPv6 && !v6Failed) {
712+ logListen += color.green(' IPv6: ' + tavernUrlV6.host);
713+ }
714+
715+ if (enableIPv4 && !v4Failed) {
716+ logListen += color.green(' IPv4: ' + tavernUrl.host);
717+ }
718+
719+ const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern';
720+ const plainGoToLog = removeColorFormatting(goToLog);
721+
722+ console.log(logListen);
723+ console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
724+ console.log(goToLog);
725+ console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
620726
621727 if (listen) {
622728 console.log('\n0[::] or 0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n');
623729 }
624730
625731 if (basicAuthMode) {
@@ -674,6 +780,100 @@ function logSecurityAlert(message) {
674780 process.exit(1);
675781}
676782
783+/**
784+ * Handles the case where the server failed to start on one or both protocols.
785+ * @param {boolean} v6Failed If the server failed to start on IPv6
786+ * @param {boolean} v4Failed If the server failed to start on IPv4
787+ */
788+function handleServerListenFail(v6Failed, v4Failed) {
789+ if (v6Failed && !enableIPv4) {
790+ console.error(color.red('fatal error: Failed to start server on IPv6 and IPv4 disabled'));
791+ process.exit(1);
792+ }
793+
794+ if (v4Failed && !enableIPv6) {
795+ console.error(color.red('fatal error: Failed to start server on IPv4 and IPv6 disabled'));
796+ process.exit(1);
797+ }
798+
799+ if (v6Failed && v4Failed) {
800+ console.error(color.red('fatal error: Failed to start server on both IPv6 and IPv4'));
801+ process.exit(1);
802+ }
803+}
804+
805+/**
806+ * Creates an HTTPS server.
807+ * @param {URL} url The URL to listen on
808+ * @returns {Promise<void>} A promise that resolves when the server is listening
809+ * @throws {Error} If the server fails to start
810+ */
811+function createHttpsServer(url) {
812+ return new Promise((resolve, reject) => {
813+ const server = https.createServer(
814+ {
815+ cert: fs.readFileSync(cliArguments.certPath),
816+ key: fs.readFileSync(cliArguments.keyPath),
817+ }, app);
818+ server.on('error', reject);
819+ server.on('listening', resolve);
820+ server.listen(url.port || 443, url.hostname);
821+ });
822+}
823+
824+/**
825+ * Creates an HTTP server.
826+ * @param {URL} url The URL to listen on
827+ * @returns {Promise<void>} A promise that resolves when the server is listening
828+ * @throws {Error} If the server fails to start
829+ */
830+function createHttpServer(url) {
831+ return new Promise((resolve, reject) => {
832+ const server = http.createServer(app);
833+ server.on('error', reject);
834+ server.on('listening', resolve);
835+ server.listen(url.port || 80, url.hostname);
836+ });
837+}
838+
839+async function startHTTPorHTTPS() {
840+ let v6Failed = false;
841+ let v4Failed = false;
842+
843+ const createFunc = cliArguments.ssl ? createHttpsServer : createHttpServer;
844+
845+ if (enableIPv6) {
846+ try {
847+ await createFunc(tavernUrlV6);
848+ } catch (error) {
849+ console.error('non-fatal error: failed to start server on IPv6');
850+ console.error(error);
851+
852+ v6Failed = true;
853+ }
854+ }
855+
856+ if (enableIPv4) {
857+ try {
858+ await createFunc(tavernUrl);
859+ } catch (error) {
860+ console.error('non-fatal error: failed to start server on IPv4');
861+ console.error(error);
862+
863+ v4Failed = true;
864+ }
865+ }
866+
867+ return [v6Failed, v4Failed];
868+}
869+
870+async function startServer() {
871+ const [v6Failed, v4Failed] = await startHTTPorHTTPS();
872+
873+ handleServerListenFail(v6Failed, v4Failed);
874+ postSetupTasks(v6Failed, v4Failed);
875+}
876+
677877async function verifySecuritySettings() {
678878 // Skip all security checks as listen is set to false
679879 if (!listen) {
@@ -707,23 +907,4 @@ userModule.initUserStorage(dataRoot)
707907 .then(userModule.migrateUserData)
708908 .then(verifySecuritySettings)
709909 .then(preSetupTasks)
710910 .finally((startServer) => {;
711- if (cliArguments.ssl) {
712- https.createServer(
713- {
714- cert: fs.readFileSync(cliArguments.certPath),
715- key: fs.readFileSync(cliArguments.keyPath),
716- }, app)
717- .listen(
718- Number(tavernUrl.port) || 443,
719- tavernUrl.hostname,
720- postSetupTasks,
721- );
722- } else {
723- http.createServer(app).listen(
724- Number(tavernUrl.port) || 80,
725- tavernUrl.hostname,
726- postSetupTasks,
727- );
728- }
729- });
src/constants.js+1 -0
@@ -195,6 +195,7 @@ const CHAT_COMPLETION_SOURCES = {
195195 PERPLEXITY: 'perplexity',
196196 GROQ: 'groq',
197197 ZEROONEAI: '01ai',
198+ BLOCKENTROPY: 'blockentropy',
198199};
199200
200201/**
src/endpoints/anthropic.js+1 -1
@@ -28,7 +28,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
2828 ],
2929 },
3030 ],
3131 max_tokens: 8004096,
3232 };
3333
3434 console.log('Multimodal captioning request', body);
src/endpoints/backends/chat-completions.js+30 -14
@@ -18,6 +18,7 @@ const API_PERPLEXITY = 'https://api.perplexity.ai';
1818const API_GROQ = 'https://api.groq.com/openai/v1';
1919const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
2020const API_01AI = 'https://api.01.ai/v1';
21+const API_BLOCKENTROPY = 'https://api.blockentropy.ai/v1';
2122
2223/**
2324 * Applies a post-processing step to the generated messages.
@@ -104,6 +105,7 @@ async function sendClaudeRequest(request, response) {
104105 const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();
105106 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);
106107 const divider = '-'.repeat(process.stdout.columns);
108+ const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false);
107109
108110 if (!apiKey) {
109111 console.log(color.red(`Claude API key is missing.\n${divider}`));
@@ -117,8 +119,8 @@ async function sendClaudeRequest(request, response) {
117119 controller.abort();
118120 });
119121 const additionalHeaders = {};
120122 letconst use_system_promptuseSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
121123 letconst converted_promptconvertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, use_system_promptuseSystemPrompt, request.body.human_sysprompt_message, request.body.char_name, request.body.user_name);
122124 // Add custom stop sequences
123125 const stopSequences = [];
124126 if (Array.isArray(request.body.stop)) {
@@ -126,7 +128,7 @@ async function sendClaudeRequest(request, response) {
126128 }
127129
128130 const requestBody = {
129131 messages: converted_promptconvertedPrompt.messages,
130132 model: request.body.model,
131133 max_tokens: request.body.max_tokens,
132134 stop_sequences: stopSequences,
@@ -135,13 +137,15 @@ async function sendClaudeRequest(request, response) {
135137 top_k: request.body.top_k,
136138 stream: request.body.stream,
137139 };
138140 if (use_system_promptuseSystemPrompt) {
139141 requestBody.system = converted_prompt.systemPrompt;enableSystemPromptCache
142+ ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]
143+ : convertedPrompt.systemPrompt;
140144 }
141145 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
142146 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
143147 if (converted_promptconvertedPrompt.messages.length && converted_promptconvertedPrompt.messages[converted_promptconvertedPrompt.messages.length - 1].role === 'assistant') {
144148 converted_promptconvertedPrompt.messages.push({ role: 'user', content: '.' });
145149 }
146150 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';
147151 requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };
@@ -150,6 +154,9 @@ async function sendClaudeRequest(request, response) {
150154 .map(tool => tool.function)
151155 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
152156 }
157+ if (enableSystemPromptCache) {
158+ additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
159+ }
153160 console.log('Claude request:', requestBody);
154161
155162 const generateResponse = await fetch(apiUrl + '/messages', {
@@ -252,7 +259,7 @@ async function sendMakerSuiteRequest(request, response) {
252259 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
253260
254261 if (!request.body.reverse_proxy && !apiKey) {
255262 console.log('MakerSuiteGoogle AI Studio API key is missing.');
256263 return response.status(400).send({ error: true });
257264 }
258265
@@ -319,7 +326,7 @@ async function sendMakerSuiteRequest(request, response) {
319326 }
320327
321328 const body = isGemini ? getGeminiBody() : getBisonBody();
322329 console.log('MakerSuiteGoogle AI Studio request:', body);
323330
324331 try {
325332 const controller = new AbortController();
@@ -355,7 +362,7 @@ async function sendMakerSuiteRequest(request, response) {
355362 }
356363 } else {
357364 if (!generateResponse.ok) {
358365 console.log(`MakerSuiteGoogle AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
359366 return response.status(generateResponse.status).send({ error: true });
360367 }
361368
@@ -363,7 +370,7 @@ async function sendMakerSuiteRequest(request, response) {
363370
364371 const candidates = generateResponseJson?.candidates;
365372 if (!candidates || candidates.length === 0) {
366373 let message = 'MakerSuiteGoogle AI Studio API returned no candidate';
367374 console.log(message, generateResponseJson);
368375 if (generateResponseJson?.promptFeedback?.blockReason) {
369376 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;
@@ -374,19 +381,19 @@ async function sendMakerSuiteRequest(request, response) {
374381 const responseContent = candidates[0].content ?? candidates[0].output;
375382 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.[0]?.text;
376383 if (!responseText) {
377384 let message = 'MakerSuiteGoogle AI Studio Candidate text empty';
378385 console.log(message, generateResponseJson);
379386 return response.send({ error: { message } });
380387 }
381388
382389 console.log('MakerSuiteGoogle AI Studio response:', responseText);
383390
384391 // Wrap it back to OAI format
385392 const reply = { choices: [{ 'message': { 'content': responseText } }] };
386393 return response.send(reply);
387394 }
388395 } catch (error) {
389396 console.log('Error communicating with MakerSuiteGoogle AI Studio API: ', error);
390397 if (!response.headersSent) {
391398 return response.status(500).send({ error: true });
392399 }
@@ -675,6 +682,10 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
675682 api_url = API_01AI;
676683 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
677684 headers = {};
685+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.BLOCKENTROPY) {
686+ api_url = API_BLOCKENTROPY;
687+ api_key_openai = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
688+ headers = {};
678689 } else {
679690 console.log('This chat completion source is not supported yet.');
680691 return response_getstatus_openai.status(400).send({ error: true });
@@ -941,6 +952,11 @@ router.post('/generate', jsonParser, function (request, response) {
941952 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
942953 headers = {};
943954 bodyParams = {};
955+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.BLOCKENTROPY) {
956+ apiUrl = API_BLOCKENTROPY;
957+ apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
958+ headers = {};
959+ bodyParams = {};
944960 } else {
945961 console.log('This chat completion source is not supported yet.');
946962 return response.status(400).send({ error: true });
src/endpoints/backends/text-completions.js+3 -2
@@ -5,7 +5,7 @@ const Readable = require('stream').Readable;
55
66const { jsonParser } = require('../../express-common');
77const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS, FEATHERLESS_KEYS } = require('../../constants');
88const { forwardFetchResponse, trimV1, getConfigValue } = require('../../util');
99const { setAdditionalHeaders } = require('../../additional-headers');
1010
1111const router = express.Router();
@@ -325,11 +325,12 @@ router.post('/generate', jsonParser, async function (request, response) {
325325 }
326326
327327 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
328+ const keepAlive = getConfigValue('ollama.keepAlive', -1);
328329 args.body = JSON.stringify({
329330 model: request.body.model,
330331 prompt: request.body.prompt,
331332 stream: request.body.stream ?? false,
332333 keep_alive: -1keepAlive,
333334 raw: true,
334335 options: _.pickBy(request.body, (_, key) => OLLAMA_KEYS.includes(key)),
335336 });
src/endpoints/google.js+1 -1
@@ -44,7 +44,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4444
4545 if (!result.ok) {
4646 const error = await result.json();
4747 console.log(`MakerSuiteGoogle AI Studio API returned error: ${result.status} ${result.statusText}`, error);
4848 return response.status(result.status).send({ error: true });
4949 }
5050
src/endpoints/images.js+1 -1
@@ -82,7 +82,7 @@ router.post('/list/:folder', (request, response) => {
8282 }
8383
8484 try {
8585 const images = getImages(directoryPath, 'date');
8686 return response.send(images);
8787 } catch (error) {
8888 console.error(error);
src/endpoints/openai.js+44 -1
@@ -67,7 +67,6 @@ router.post('/caption-image', jsonParser, async (request, response) => {
6767 ],
6868 },
6969 ],
70- max_tokens: 500,
7170 ...bodyParams,
7271 };
7372
@@ -283,4 +282,48 @@ router.post('/generate-image', jsonParser, async (request, response) => {
283282 }
284283});
285284
285+const custom = express.Router();
286+
287+custom.post('/generate-voice', jsonParser, async (request, response) => {
288+ try {
289+ const key = readSecret(request.user.directories, SECRET_KEYS.CUSTOM_OPENAI_TTS);
290+ const { input, provider_endpoint, response_format, voice, speed, model } = request.body;
291+
292+ if (!provider_endpoint) {
293+ console.log('No OpenAI-compatible TTS provider endpoint provided');
294+ return response.sendStatus(400);
295+ }
296+
297+ const result = await fetch(provider_endpoint, {
298+ method: 'POST',
299+ headers: {
300+ 'Content-Type': 'application/json',
301+ Authorization: `Bearer ${key ?? ''}`,
302+ },
303+ body: JSON.stringify({
304+ input: input ?? '',
305+ response_format: response_format ?? 'mp3',
306+ voice: voice ?? 'alloy',
307+ speed: speed ?? 1,
308+ model: model ?? 'tts-1',
309+ }),
310+ });
311+
312+ if (!result.ok) {
313+ const text = await result.text();
314+ console.log('OpenAI request failed', result.statusText, text);
315+ return response.status(500).send(text);
316+ }
317+
318+ const buffer = await result.arrayBuffer();
319+ response.setHeader('Content-Type', 'audio/mpeg');
320+ return response.send(Buffer.from(buffer));
321+ } catch (error) {
322+ console.error('OpenAI TTS generation failed', error);
323+ response.status(500).send('Internal server error');
324+ }
325+});
326+
327+router.use('/custom', custom);
328+
286329module.exports = { router };
src/endpoints/secrets.js+2 -0
@@ -44,6 +44,8 @@ const SECRET_KEYS = {
4444 ZEROONEAI: 'api_key_01ai',
4545 HUGGINGFACE: 'api_key_huggingface',
4646 STABILITY: 'api_key_stability',
47+ BLOCKENTROPY: 'api_key_blockentropy',
48+ CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
4749};
4850
4951// These are the keys that are safe to expose, even if allowKeysExposure is false
src/endpoints/settings.js+2 -0
@@ -9,6 +9,7 @@ const { jsonParser } = require('../express-common');
99const { getAllUserHandles, getUserDirectories } = require('../users');
1010
1111const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);
12+const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('enableExtensionsAutoUpdate', true);
1213const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1314
1415// 10 minutes
@@ -268,6 +269,7 @@ router.post('/get', jsonParser, (request, response) => {
268269 instruct,
269270 context,
270271 enable_extensions: ENABLE_EXTENSIONS,
272+ enable_extensions_auto_update: ENABLE_EXTENSIONS_AUTO_UPDATE,
271273 enable_accounts: ENABLE_ACCOUNTS,
272274 });
273275});
src/endpoints/stable-diffusion.js+125 -0
@@ -908,10 +908,135 @@ stability.post('/generate', jsonParser, async (request, response) => {
908908 }
909909});
910910
911+const blockentropy = express.Router();
912+
913+blockentropy.post('/models', jsonParser, async (request, response) => {
914+ try {
915+ const key = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
916+
917+ if (!key) {
918+ console.log('Block Entropy key not found.');
919+ return response.sendStatus(400);
920+ }
921+
922+ const modelsResponse = await fetch('https://api.blockentropy.ai/sdapi/v1/sd-models', {
923+ method: 'GET',
924+ headers: {
925+ 'Authorization': `Bearer ${key}`,
926+ },
927+ });
928+
929+ if (!modelsResponse.ok) {
930+ console.log('Block Entropy returned an error.');
931+ return response.sendStatus(500);
932+ }
933+
934+ const data = await modelsResponse.json();
935+
936+ if (!Array.isArray(data)) {
937+ console.log('Block Entropy returned invalid data.');
938+ return response.sendStatus(500);
939+ }
940+ const models = data.map(x => ({ value: x.name, text: x.name }));
941+ return response.send(models);
942+
943+ } catch (error) {
944+ console.log(error);
945+ return response.sendStatus(500);
946+ }
947+});
948+
949+blockentropy.post('/generate', jsonParser, async (request, response) => {
950+ try {
951+ const key = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY);
952+
953+ if (!key) {
954+ console.log('Block Entropy key not found.');
955+ return response.sendStatus(400);
956+ }
957+
958+ console.log('Block Entropy request:', request.body);
959+
960+ const result = await fetch('https://api.blockentropy.ai/sdapi/v1/txt2img', {
961+ method: 'POST',
962+ body: JSON.stringify({
963+ prompt: request.body.prompt,
964+ negative_prompt: request.body.negative_prompt,
965+ model: request.body.model,
966+ steps: request.body.steps,
967+ width: request.body.width,
968+ height: request.body.height,
969+ // Random seed if negative.
970+ seed: request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000),
971+ }),
972+ headers: {
973+ 'Content-Type': 'application/json',
974+ 'Authorization': `Bearer ${key}`,
975+ },
976+ });
977+
978+ if (!result.ok) {
979+ console.log('Block Entropy returned an error.');
980+ return response.sendStatus(500);
981+ }
982+
983+ const data = await result.json();
984+ console.log('Block Entropy response:', data);
985+
986+ return response.send(data);
987+ } catch (error) {
988+ console.log(error);
989+ return response.sendStatus(500);
990+ }
991+});
992+
993+
994+const huggingface = express.Router();
995+
996+huggingface.post('/generate', jsonParser, async (request, response) => {
997+ try {
998+ const key = readSecret(request.user.directories, SECRET_KEYS.HUGGINGFACE);
999+
1000+ if (!key) {
1001+ console.log('Hugging Face key not found.');
1002+ return response.sendStatus(400);
1003+ }
1004+
1005+ console.log('Hugging Face request:', request.body);
1006+
1007+ const result = await fetch(`https://api-inference.huggingface.co/models/${request.body.model}`, {
1008+ method: 'POST',
1009+ body: JSON.stringify({
1010+ inputs: request.body.prompt,
1011+ }),
1012+ headers: {
1013+ 'Content-Type': 'application/json',
1014+ 'Authorization': `Bearer ${key}`,
1015+ },
1016+ });
1017+
1018+ if (!result.ok) {
1019+ console.log('Hugging Face returned an error.');
1020+ return response.sendStatus(500);
1021+ }
1022+
1023+ const buffer = await result.buffer();
1024+ return response.send({
1025+ image: buffer.toString('base64'),
1026+ });
1027+ } catch (error) {
1028+ console.log(error);
1029+ return response.sendStatus(500);
1030+ }
1031+});
1032+
1033+
9111034router.use('/comfy', comfy);
9121035router.use('/together', together);
9131036router.use('/drawthings', drawthings);
9141037router.use('/pollinations', pollinations);
9151038router.use('/stability', stability);
1039+router.use('/blockentropy', blockentropy);
1040+router.use('/huggingface', huggingface);
9161041
9171042module.exports = { router };
src/endpoints/tokenizers.js+31 -3
@@ -143,6 +143,7 @@ const spp_nerd = new SentencePieceTokenizer('src/tokenizers/nerdstash.model');
143143const spp_nerd_v2 = new SentencePieceTokenizer('src/tokenizers/nerdstash_v2.model');
144144const spp_mistral = new SentencePieceTokenizer('src/tokenizers/mistral.model');
145145const spp_yi = new SentencePieceTokenizer('src/tokenizers/yi.model');
146+const spp_gemma = new SentencePieceTokenizer('src/tokenizers/gemma.model');
146147const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');
147148const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json');
148149
@@ -152,6 +153,7 @@ const sentencepieceTokenizers = [
152153 'nerdstash_v2',
153154 'mistral',
154155 'yi',
156+ 'gemma',
155157];
156158
157159/**
@@ -180,6 +182,10 @@ function getSentencepiceTokenizer(model) {
180182 return spp_yi;
181183 }
182184
185+ if (model.includes('gemma')) {
186+ return spp_gemma;
187+ }
188+
183189 return null;
184190}
185191
@@ -268,6 +274,10 @@ function getTokenizerModel(requestModel) {
268274 return 'gpt-4o';
269275 }
270276
277+ if (requestModel.includes('chatgpt-4o-latest')) {
278+ return 'gpt-4o';
279+ }
280+
271281 if (requestModel.includes('gpt-4-32k')) {
272282 return 'gpt-4-32k';
273283 }
@@ -308,8 +318,8 @@ function getTokenizerModel(requestModel) {
308318 return 'yi';
309319 }
310320
311321 if (requestModel.includes('gemma') || requestModel.includes('gemini')) {
312322 return 'gpt-4ogemma';
313323 }
314324
315325 // default
@@ -579,6 +589,7 @@ router.post('/nerdstash/encode', jsonParser, createSentencepieceEncodingHandler(
579589router.post('/nerdstash_v2/encode', jsonParser, createSentencepieceEncodingHandler(spp_nerd_v2));
580590router.post('/mistral/encode', jsonParser, createSentencepieceEncodingHandler(spp_mistral));
581591router.post('/yi/encode', jsonParser, createSentencepieceEncodingHandler(spp_yi));
592+router.post('/gemma/encode', jsonParser, createSentencepieceEncodingHandler(spp_gemma));
582593router.post('/gpt2/encode', jsonParser, createTiktokenEncodingHandler('gpt2'));
583594router.post('/claude/encode', jsonParser, createWebTokenizerEncodingHandler(claude_tokenizer));
584595router.post('/llama3/encode', jsonParser, createWebTokenizerEncodingHandler(llama3_tokenizer));
@@ -587,6 +598,7 @@ router.post('/nerdstash/decode', jsonParser, createSentencepieceDecodingHandler(
587598router.post('/nerdstash_v2/decode', jsonParser, createSentencepieceDecodingHandler(spp_nerd_v2));
588599router.post('/mistral/decode', jsonParser, createSentencepieceDecodingHandler(spp_mistral));
589600router.post('/yi/decode', jsonParser, createSentencepieceDecodingHandler(spp_yi));
601+router.post('/gemma/decode', jsonParser, createSentencepieceDecodingHandler(spp_gemma));
590602router.post('/gpt2/decode', jsonParser, createTiktokenDecodingHandler('gpt2'));
591603router.post('/claude/decode', jsonParser, createWebTokenizerDecodingHandler(claude_tokenizer));
592604router.post('/llama3/decode', jsonParser, createWebTokenizerDecodingHandler(llama3_tokenizer));
@@ -620,6 +632,11 @@ router.post('/openai/encode', jsonParser, async function (req, res) {
620632 return handler(req, res);
621633 }
622634
635+ if (queryModel.includes('gemma') || queryModel.includes('gemini')) {
636+ const handler = createSentencepieceEncodingHandler(spp_gemma);
637+ return handler(req, res);
638+ }
639+
623640 const model = getTokenizerModel(queryModel);
624641 const handler = createTiktokenEncodingHandler(model);
625642 return handler(req, res);
@@ -658,6 +675,11 @@ router.post('/openai/decode', jsonParser, async function (req, res) {
658675 return handler(req, res);
659676 }
660677
678+ if (queryModel.includes('gemma') || queryModel.includes('gemini')) {
679+ const handler = createSentencepieceDecodingHandler(spp_gemma);
680+ return handler(req, res);
681+ }
682+
661683 const model = getTokenizerModel(queryModel);
662684 const handler = createTiktokenDecodingHandler(model);
663685 return handler(req, res);
@@ -704,6 +726,11 @@ router.post('/openai/count', jsonParser, async function (req, res) {
704726 return res.send({ 'token_count': num_tokens });
705727 }
706728
729+ if (model === 'gemma' || model === 'gemini') {
730+ num_tokens = await countSentencepieceArrayTokens(spp_gemma, req.body);
731+ return res.send({ 'token_count': num_tokens });
732+ }
733+
707734 const tokensPerName = queryModel.includes('gpt-3.5-turbo-0301') ? -1 : 1;
708735 const tokensPerMessage = queryModel.includes('gpt-3.5-turbo-0301') ? 4 : 3;
709736 const tokensPadding = 3;
@@ -785,6 +812,7 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
785812 const baseUrl = String(request.body.url);
786813 const legacyApi = Boolean(request.body.legacy_api);
787814 const vllmModel = String(request.body.vllm_model) || '';
815+ const aphroditeModel = String(request.body.aphrodite_model) || '';
788816
789817 try {
790818 const args = {
@@ -820,7 +848,7 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
820848 break;
821849 case TEXTGEN_TYPES.APHRODITE:
822850 url += '/v1/tokenize';
823851 args.body = JSON.stringify({ 'model': aphroditeModel, 'prompt': text });
824852 break;
825853 default:
826854 url += '/v1/internal/encode';
src/endpoints/translate.js+6 -3
@@ -1,6 +1,7 @@
11const fetch = require('node-fetch').default;
22const https = require('https');
33const express = require('express');
4+const iconv = require('iconv-lite');
45const { readSecret, SECRET_KEYS } = require('./secrets');
56const { getConfigValue, uuidv4 } = require('../util');
67const { jsonParser } = require('../express-common');
@@ -80,16 +81,18 @@ router.post('/google', jsonParser, async (request, response) => {
8081 const url = generateRequestUrl(text, { to: lang });
8182
8283 https.get(url, (resp) => {
8384 letconst data = ''[];
8485
8586 resp.on('data', (chunk) => {
8687 data += .push(chunk);
8788 });
8889
8990 resp.on('end', () => {
9091 try {
9192 const resultdecodedData = normaliseResponseiconv.decode(JSONBuffer.parseconcat(data), 'utf-8');
93+ const result = normaliseResponse(JSON.parse(decodedData));
9294 console.log('Translated text: ' + result.text);
95+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
9396 return response.send(result.text);
9497 } catch (error) {
9598 console.log('Translation error', error);
src/tokenizers/gemma.model+0 -0

Binary file

src/transformers.mjs+37 -4
@@ -1,6 +1,7 @@
11import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';
22import { getConfigValue } from './util.js';
33import path from 'path';
4+import fs from 'fs';
45
56configureTransformers();
67
@@ -34,7 +35,7 @@ const tasks = {
3435 defaultModel: 'Cohee/fooocus_expansion-onnx',
3536 pipeline: null,
3637 configField: 'extras.promptExpansionModel',
3738 quantized: truefalse,
3839 },
3940 'automatic-speech-recognition': {
4041 defaultModel: 'Xenova/whisper-small',
@@ -48,7 +49,7 @@ const tasks = {
4849 configField: 'extras.textToSpeechModel',
4950 quantized: false,
5051 },
5152};
5253
5354/**
5455 * Gets a RawImage object from a base64-encoded image.
@@ -85,6 +86,36 @@ function getModelForTask(task) {
8586 }
8687}
8788
89+async function migrateCacheToDataDir() {
90+ const oldCacheDir = path.join(process.cwd(), 'cache');
91+ const newCacheDir = path.join(global.DATA_ROOT, '_cache');
92+
93+ if (!fs.existsSync(newCacheDir)) {
94+ fs.mkdirSync(newCacheDir, { recursive: true });
95+ }
96+
97+ if (fs.existsSync(oldCacheDir) && fs.statSync(oldCacheDir).isDirectory()) {
98+ const files = fs.readdirSync(oldCacheDir);
99+
100+ if (files.length === 0) {
101+ return;
102+ }
103+
104+ console.log('Migrating model cache files to data directory. Please wait...');
105+
106+ for (const file of files) {
107+ try {
108+ const oldPath = path.join(oldCacheDir, file);
109+ const newPath = path.join(newCacheDir, file);
110+ fs.cpSync(oldPath, newPath, { recursive: true, force: true });
111+ fs.rmSync(oldPath, { recursive: true, force: true });
112+ } catch (error) {
113+ console.warn('Failed to migrate cache file. The model will be re-downloaded.', error);
114+ }
115+ }
116+ }
117+}
118+
88119/**
89120 * Gets the transformers.js pipeline for a given task.
90121 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for
@@ -92,6 +123,8 @@ function getModelForTask(task) {
92123 * @returns {Promise<Pipeline>} Pipeline for the task
93124 */
94125async function getPipeline(task, forceModel = '') {
126+ await migrateCacheToDataDir();
127+
95128 if (tasks[task].pipeline) {
96129 if (forceModel === '' || tasks[task].currentModel === forceModel) {
97130 return tasks[task].pipeline;
@@ -100,11 +133,11 @@ async function getPipeline(task, forceModel = '') {
100133 await tasks[task].pipeline.dispose();
101134 }
102135
103136 const cache_dircacheDir = path.join(processglobal.cwd()DATA_ROOT, 'cache_cache');
104137 const model = forceModel || getModelForTask(task);
105138 const localOnly = getConfigValue('extras.disableAutoDownload', false);
106139 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
107140 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
108141 tasks[task].pipeline = instance;
109142 tasks[task].currentModel = model;
110143 return instance;
src/users.js+5 -11
@@ -20,12 +20,6 @@ const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
2020const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2121
2222/**
23- * The root directory for user data.
24- * @type {string}
25- */
26-let DATA_ROOT = './data';
27-
28-/**
2923 * Cache for user directories.
3024 * @type {Map<string, UserDirectoryList>}
3125 */
@@ -138,7 +132,7 @@ async function migrateUserData() {
138132
139133 console.log();
140134 console.log(color.magenta('Preparing to migrate user data...'));
141135 console.log(`All public data will be moved to the ${global.DATA_ROOT} directory.`);
142136 console.log('This process may take a while depending on the amount of data to move.');
143137 console.log(`Backups will be placed in the ${PUBLIC_DIRECTORIES.backups} directory.`);
144138 console.log(`The process will start in ${TIMEOUT} seconds. Press Ctrl+C to cancel.`);
@@ -352,11 +346,11 @@ function toAvatarKey(handle) {
352346 * @returns {Promise<void>}
353347 */
354348async function initUserStorage(dataRoot) {
355349 global.DATA_ROOT = dataRoot;
356350 console.log('Using data root:', color.green(global.DATA_ROOT));
357351 console.log();
358352 await storage.init({
359353 dir: path.join(global.DATA_ROOT, '_storage'),
360354 ttl: false, // Never expire
361355 });
362356
@@ -457,7 +451,7 @@ function getUserDirectories(handle) {
457451
458452 const directories = structuredClone(USER_DIRECTORY_TEMPLATE);
459453 for (const key in directories) {
460454 directories[key] = path.join(global.DATA_ROOT, handle, USER_DIRECTORY_TEMPLATE[key]);
461455 }
462456 DIRECTORIES_CACHE.set(handle, directories);
463457 return directories;
src/util.js+41 -3
@@ -382,14 +382,31 @@ function removeOldBackups(directory, prefix) {
382382 }
383383}
384384
385-function getImages(path) {
385+/**
386+ * Get a list of images in a directory.
387+ * @param {string} directoryPath Path to the directory containing the images
388+ * @param {'name' | 'date'} sortBy Sort images by name or date
389+ * @returns {string[]} List of image file names
390+ */
391+function getImages(directoryPath, sortBy = 'name') {
392+ function getSortFunction() {
393+ switch (sortBy) {
394+ case 'name':
395+ return Intl.Collator().compare;
396+ case 'date':
397+ return (a, b) => fs.statSync(path.join(directoryPath, a)).mtimeMs - fs.statSync(path.join(directoryPath, b)).mtimeMs;
398+ default:
399+ return (_a, _b) => 0;
400+ }
401+ }
402+
386403 return fs
387404 .readdirSync(pathdirectoryPath)
388405 .filter(file => {
389406 const type = mime.lookup(file);
390407 return type && type.startsWith('image/');
391408 })
392409 .sort(Intl.CollatorgetSortFunction().compare);
393410}
394411
395412/**
@@ -610,6 +627,25 @@ class Cache {
610627 }
611628}
612629
630+/**
631+ * Removes color formatting from a text string.
632+ * @param {string} text Text with color formatting
633+ * @returns {string} Text without color formatting
634+ */
635+function removeColorFormatting(text) {
636+ // ANSI escape codes for colors are usually in the format \x1b[<codes>m
637+ return text.replace(/\x1b\[\d{1,2}(;\d{1,2})*m/g, '');
638+}
639+
640+/**
641+ * Gets a separator string repeated n times.
642+ * @param {number} n Number of times to repeat the separator
643+ * @returns {string} Separator string
644+ */
645+function getSeparator(n) {
646+ return '='.repeat(n);
647+}
648+
613649module.exports = {
614650 getConfig,
615651 getConfigValue,
@@ -637,4 +673,6 @@ module.exports = {
637673 trimV1,
638674 Cache,
639675 makeHttp2Request,
676+ removeColorFormatting,
677+ getSeparator,
640678};
src/vectors/makersuite-vectors.js+4 -4
@@ -23,8 +23,8 @@ async function getMakerSuiteVector(text, directories) {
2323 const key = readSecret(directories, SECRET_KEYS.MAKERSUITE);
2424
2525 if (!key) {
2626 console.log('No MakerSuiteGoogle AI Studio key found');
2727 throw new Error('No MakerSuiteGoogle AI Studio key found');
2828 }
2929
3030 const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/embedding-gecko-001:embedText?key=${key}`, {
@@ -39,8 +39,8 @@ async function getMakerSuiteVector(text, directories) {
3939
4040 if (!response.ok) {
4141 const text = await response.text();
4242 console.log('MakerSuiteGoogle AI Studio request failed', response.statusText, text);
4343 throw new Error('MakerSuiteGoogle AI Studio request failed');
4444 }
4545
4646 const data = await response.json();
tests/package-lock.json+6 -8
@@ -1653,10 +1653,9 @@
16531653 "license": "MIT"
16541654 },
16551655 "node_modules/axios": {
16561656 "version": "1.7.24",
16571657 "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.24.tgz",
16581658 "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDhDukmaFRnY6AzAALSH4J2M3k6PkaC+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0RmgwMfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
1659- "license": "MIT",
16601659 "dependencies": {
16611660 "follow-redirects": "^1.15.6",
16621661 "form-data": "^4.0.0",
@@ -4514,10 +4513,9 @@
45144513 }
45154514 },
45164515 "node_modules/micromatch": {
45174516 "version": "4.0.78",
45184517 "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.78.tgz",
45194518 "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqOPXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/QPzEJQxsYsEiFCKo2BA==",
4520- "license": "MIT",
45214519 "dependencies": {
45224520 "braces": "^3.0.3",
45234521 "picomatch": "^2.3.1"