Merge branch 'staging' into pr/Cohee1207/2711

64d3ed468081047806f93cbcea3281679733049e

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

99 files changed, +2657 -780Ignore whitespace
.eslintrc.js+1 -0
@@ -55,6 +55,7 @@ module.exports = {
55 isProbablyReaderable: 'readonly',55 isProbablyReaderable: 'readonly',
56 ePub: 'readonly',56 ePub: 'readonly',
57 diff_match_patch: 'readonly',57 diff_match_patch: 'readonly',
58 SillyTavern: 'readonly',
58 },59 },
59 },60 },
60 ],61 ],
.github/readme.md+1 -2
@@ -246,7 +246,6 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
246246
247##### Additional Settings247##### 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)
250- [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/).249- [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/).
251- [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.250- [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
2551. Open your Command Line2541. Open your Command Line
2562. Run the following command2552. Run the following command
257256
258`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]'`257`docker create --name='sillytavern' --net='[DockerNet]' -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
260> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.259> 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 @@
34 - What did you do to achieve this?34 - What did you do to achieve this?
35 - How would a reviewer test the change?35 - How would a reviewer test the change?
366. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer.366. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer.
37
38## Further reading
39
401. [How to write UI extensions](https://docs.sillytavern.app/for-contributors/writing-extensions/)
412. [How to write server plugins](https://docs.sillytavern.app/for-contributors/server-plugins)
Dockerfile+1 -1
@@ -1,4 +1,4 @@
1FROM node:lts-alpine3.181FROM node:lts-alpine3.19
22
3# Arguments3# Arguments
4ARG APP_HOME=/home/node/app4ARG APP_HOME=/home/node/app
default/config.yaml+42 -6
@@ -4,8 +4,22 @@ dataRoot: ./data
4# -- SERVER CONFIGURATION --4# -- SERVER CONFIGURATION --
5# Listen for incoming connections5# Listen for incoming connections
6listen: false6listen: false
7# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
8protocol:
9 ipv4: true
10 ipv6: false
11# Prefers IPv6 for DNS. Enable this on ISPs that don't have issues with IPv6
12dnsPreferIPv6: false
13# The hostname that autorun opens.
14# - Use "auto" to let the server decide
15# - Use options like 'localhost', 'st.example.com'
16autorunHostname: "auto"
7# Server port17# Server port
8port: 800018port: 8000
19# Overrides the port for autorun in browser.
20# - Use -1 to use the server port.
21# - Specify a port to override the default.
22autorunPortOverride: -1
9# -- SECURITY CONFIGURATION --23# -- SECURITY CONFIGURATION --
10# Toggle whitelist mode24# Toggle whitelist mode
11whitelistMode: true25whitelistMode: true
@@ -13,6 +27,7 @@ whitelistMode: true
13enableForwardedWhitelist: true27enableForwardedWhitelist: true
14# Whitelist of allowed IP addresses28# Whitelist of allowed IP addresses
15whitelist:29whitelist:
30 - ::1
16 - 127.0.0.131 - 127.0.0.1
17# Toggle basic authentication for endpoints32# Toggle basic authentication for endpoints
18basicAuthMode: false33basicAuthMode: false
@@ -26,6 +41,11 @@ enableCorsProxy: false
26enableUserAccounts: false41enableUserAccounts: false
27# Enable discreet login mode: hides user list on the login screen42# Enable discreet login mode: hides user list on the login screen
28enableDiscreetLogin: false43enableDiscreetLogin: false
44# User session timeout *in seconds* (defaults to 24 hours).
45## Set to a positive number to expire session after a certain time of inactivity
46## Set to 0 to expire session when the browser is closed
47## Set to a negative number to disable session expiration
48sessionTimeout: 86400
29# Used to sign session cookies. Will be auto-generated if not set49# Used to sign session cookies. Will be auto-generated if not set
30cookieSecret: ''50cookieSecret: ''
31# Disable CSRF protection - NOT RECOMMENDED51# Disable CSRF protection - NOT RECOMMENDED
@@ -35,6 +55,9 @@ securityOverride: false
35# -- ADVANCED CONFIGURATION --55# -- ADVANCED CONFIGURATION --
36# Open the browser automatically56# Open the browser automatically
37autorun: true57autorun: true
58# Avoids using 'localhost' for autorun in auto mode.
59# use if you don't have 'localhost' in your hosts file
60avoidLocalhost: false
38# Disable thumbnail generation61# Disable thumbnail generation
39disableThumbnails: false62disableThumbnails: false
40# Thumbnail quality (0-100)63# Thumbnail quality (0-100)
@@ -67,9 +90,11 @@ whitelistImportDomains:
67## headers:90## headers:
68## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"91## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)"
69requestOverrides: []92requestOverrides: []
70# -- PLUGIN CONFIGURATION --93# -- EXTENSIONS CONFIGURATION --
71# Enable UI extensions94# Enable UI extensions
72enableExtensions: true95enableExtensions: true
96# Automatically update extensions when a release version changes
97enableExtensionsAutoUpdate: true
73# Extension settings98# Extension settings
74extras:99extras:
75 # Disables automatic model download from HuggingFace100 # Disables automatic model download from HuggingFace
@@ -98,10 +123,21 @@ mistral:
98 # Enables prefilling of the reply with the last assistant message in the prompt123 # Enables prefilling of the reply with the last assistant message in the prompt
99 # CAUTION: The prefix is echoed into the completion. You may want to use regex to trim it out.124 # CAUTION: The prefix is echoed into the completion. You may want to use regex to trim it out.
100 enablePrefix: false125 enablePrefix: false
126# -- OLLAMA API CONFIGURATION --
127ollama:
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 --
134claude:
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
101# -- SERVER PLUGIN CONFIGURATION --142# -- SERVER PLUGIN CONFIGURATION --
102enableServerPlugins: false143enableServerPlugins: false
103# User session timeout *in seconds* (defaults to 24 hours).
104## Set to a positive number to expire session after a certain time of inactivity
105## Set to 0 to expire session when the browser is closed
106## Set to a negative number to disable session expiration
107sessionTimeout: 86400
default/content/presets/openai/Default.json+1 -1
@@ -22,7 +22,7 @@
22 "count_penalty": 0,22 "count_penalty": 0,
23 "top_p": 1,23 "top_p": 1,
24 "top_k": 0,24 "top_k": 0,
25 "top_a": 1,25 "top_a": 0,
26 "min_p": 0,26 "min_p": 0,
27 "repetition_penalty": 1,27 "repetition_penalty": 1,
28 "openai_max_context": 4095,28 "openai_max_context": 4095,
default/content/settings.json+1 -0
@@ -142,6 +142,7 @@
142 "timestamps_enabled": true,142 "timestamps_enabled": true,
143 "timestamp_model_icon": true,143 "timestamp_model_icon": true,
144 "mesIDDisplay_enabled": false,144 "mesIDDisplay_enabled": false,
145 "hideChatAvatars_enabled": false,
145 "max_context_unlocked": false,146 "max_context_unlocked": false,
146 "prefer_character_prompt": true,147 "prefer_character_prompt": true,
147 "prefer_character_jailbreak": true,148 "prefer_character_jailbreak": true,
default/content/themes/Azure.json+1 -0
@@ -23,6 +23,7 @@
23 "timestamps_enabled": true,23 "timestamps_enabled": true,
24 "timestamp_model_icon": false,24 "timestamp_model_icon": false,
25 "mesIDDisplay_enabled": true,25 "mesIDDisplay_enabled": true,
26 "hideChatAvatars_enabled": false,
26 "message_token_count_enabled": false,27 "message_token_count_enabled": false,
27 "expand_message_actions": false,28 "expand_message_actions": false,
28 "enableZenSliders": false,29 "enableZenSliders": false,
default/content/themes/Cappuccino.json+1 -0
@@ -23,6 +23,7 @@
23 "timestamps_enabled": true,23 "timestamps_enabled": true,
24 "timestamp_model_icon": true,24 "timestamp_model_icon": true,
25 "mesIDDisplay_enabled": true,25 "mesIDDisplay_enabled": true,
26 "hideChatAvatars_enabled": false,
26 "message_token_count_enabled": false,27 "message_token_count_enabled": false,
27 "expand_message_actions": false,28 "expand_message_actions": false,
28 "enableZenSliders": false,29 "enableZenSliders": false,
default/content/themes/Dark Lite.json+1 -0
@@ -23,6 +23,7 @@
23 "timestamps_enabled": true,23 "timestamps_enabled": true,
24 "timestamp_model_icon": true,24 "timestamp_model_icon": true,
25 "mesIDDisplay_enabled": false,25 "mesIDDisplay_enabled": false,
26 "hideChatAvatars_enabled": false,
26 "message_token_count_enabled": false,27 "message_token_count_enabled": false,
27 "expand_message_actions": false,28 "expand_message_actions": false,
28 "enableZenSliders": "",29 "enableZenSliders": "",
default/content/themes/Dark V 1.0.json+1 -1
@@ -34,4 +34,4 @@
34 "zoomed_avatar_magnification": true,34 "zoomed_avatar_magnification": true,
35 "reduced_motion": true,35 "reduced_motion": true,
36 "compact_input_area": false36 "compact_input_area": false
37}
37 \ No newline at end of file \ No newline at end of file
37}
default/content/user-default.png+0 -0

Binary file

index.d.ts+5 -0
@@ -9,6 +9,11 @@ declare global {
9 };9 };
10 }10 }
11 }11 }
12
13 /**
14 * The root directory for user data.
15 */
16 var DATA_ROOT: string;
12}17}
1318
14declare module 'express-session' {19declare module 'express-session' {
package-lock.json+58 -32
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.12.4",3 "version": "1.12.5",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "sillytavern",8 "name": "sillytavern",
9 "version": "1.12.4",9 "version": "1.12.5",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -27,6 +27,7 @@
27 "google-translate-api-browser": "^3.0.1",27 "google-translate-api-browser": "^3.0.1",
28 "he": "^1.2.0",28 "he": "^1.2.0",
29 "helmet": "^7.1.0",29 "helmet": "^7.1.0",
30 "iconv-lite": "^0.6.3",
30 "ip-matching": "^2.1.2",31 "ip-matching": "^2.1.2",
31 "ipaddr.js": "^2.0.1",32 "ipaddr.js": "^2.0.1",
32 "jimp": "^0.22.10",33 "jimp": "^0.22.10",
@@ -42,7 +43,7 @@
42 "rate-limiter-flexible": "^5.0.0",43 "rate-limiter-flexible": "^5.0.0",
43 "response-time": "^2.3.2",44 "response-time": "^2.3.2",
44 "sanitize-filename": "^1.6.3",45 "sanitize-filename": "^1.6.3",
45 "sillytavern-transformers": "^2.14.6",46 "sillytavern-transformers": "2.14.6",
46 "simple-git": "^3.19.1",47 "simple-git": "^3.19.1",
47 "tiktoken": "^1.0.15",48 "tiktoken": "^1.0.15",
48 "vectra": "^0.2.2",49 "vectra": "^0.2.2",
@@ -58,7 +59,7 @@
58 },59 },
59 "devDependencies": {60 "devDependencies": {
60 "@types/jquery": "^3.5.29",61 "@types/jquery": "^3.5.29",
61 "eslint": "^8.55.0",62 "eslint": "^8.57.0",
62 "jquery": "^3.6.4"63 "jquery": "^3.6.4"
63 },64 },
64 "engines": {65 "engines": {
@@ -166,9 +167,9 @@
166 "license": "MIT"167 "license": "MIT"
167 },168 },
168 "node_modules/@eslint/js": {169 "node_modules/@eslint/js": {
169 "version": "8.55.0",170 "version": "8.57.0",
170 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",171 "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
171 "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",172 "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
172 "dev": true,173 "dev": true,
173 "license": "MIT",174 "license": "MIT",
174 "engines": {175 "engines": {
@@ -185,14 +186,15 @@
185 }186 }
186 },187 },
187 "node_modules/@humanwhocodes/config-array": {188 "node_modules/@humanwhocodes/config-array": {
188 "version": "0.11.13",189 "version": "0.11.14",
189 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",190 "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
190 "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",191 "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
192 "deprecated": "Use @eslint/config-array instead",
191 "dev": true,193 "dev": true,
192 "license": "Apache-2.0",194 "license": "Apache-2.0",
193 "dependencies": {195 "dependencies": {
194 "@humanwhocodes/object-schema": "^2.0.1",196 "@humanwhocodes/object-schema": "^2.0.2",
195 "debug": "^4.1.1",197 "debug": "^4.3.1",
196 "minimatch": "^3.0.5"198 "minimatch": "^3.0.5"
197 },199 },
198 "engines": {200 "engines": {
@@ -200,9 +202,9 @@
200 }202 }
201 },203 },
202 "node_modules/@humanwhocodes/config-array/node_modules/debug": {204 "node_modules/@humanwhocodes/config-array/node_modules/debug": {
203 "version": "4.3.4",205 "version": "4.3.6",
204 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",206 "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
205 "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",207 "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
206 "dev": true,208 "dev": true,
207 "license": "MIT",209 "license": "MIT",
208 "dependencies": {210 "dependencies": {
@@ -239,9 +241,10 @@
239 }241 }
240 },242 },
241 "node_modules/@humanwhocodes/object-schema": {243 "node_modules/@humanwhocodes/object-schema": {
242 "version": "2.0.1",244 "version": "2.0.3",
243 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",245 "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
244 "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",246 "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
247 "deprecated": "Use @eslint/object-schema instead",
245 "dev": true,248 "dev": true,
246 "license": "BSD-3-Clause"249 "license": "BSD-3-Clause"
247 },250 },
@@ -1391,12 +1394,11 @@
1391 "license": "MIT"1394 "license": "MIT"
1392 },1395 },
1393 "node_modules/axios": {1396 "node_modules/axios": {
1394 "version": "1.6.1",1397 "version": "1.7.4",
1395 "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",1398 "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
1396 "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",1399 "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
1397 "license": "MIT",
1398 "dependencies": {1400 "dependencies": {
1399 "follow-redirects": "^1.15.0",1401 "follow-redirects": "^1.15.6",
1400 "form-data": "^4.0.0",1402 "form-data": "^4.0.0",
1401 "proxy-from-env": "^1.1.0"1403 "proxy-from-env": "^1.1.0"
1402 }1404 }
@@ -1491,6 +1493,18 @@
1491 "node": ">= 0.8"1493 "node": ">= 0.8"
1492 }1494 }
1493 },1495 },
1496 "node_modules/body-parser/node_modules/iconv-lite": {
1497 "version": "0.4.24",
1498 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1499 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1500 "license": "MIT",
1501 "dependencies": {
1502 "safer-buffer": ">= 2.1.2 < 3"
1503 },
1504 "engines": {
1505 "node": ">=0.10.0"
1506 }
1507 },
1494 "node_modules/boolbase": {1508 "node_modules/boolbase": {
1495 "version": "1.0.0",1509 "version": "1.0.0",
1496 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",1510 "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -2456,17 +2470,17 @@
2456 }2470 }
2457 },2471 },
2458 "node_modules/eslint": {2472 "node_modules/eslint": {
2459 "version": "8.55.0",2473 "version": "8.57.0",
2460 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",2474 "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
2461 "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",2475 "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
2462 "dev": true,2476 "dev": true,
2463 "license": "MIT",2477 "license": "MIT",
2464 "dependencies": {2478 "dependencies": {
2465 "@eslint-community/eslint-utils": "^4.2.0",2479 "@eslint-community/eslint-utils": "^4.2.0",
2466 "@eslint-community/regexpp": "^4.6.1",2480 "@eslint-community/regexpp": "^4.6.1",
2467 "@eslint/eslintrc": "^2.1.4",2481 "@eslint/eslintrc": "^2.1.4",
2468 "@eslint/js": "8.55.0",2482 "@eslint/js": "8.57.0",
2469 "@humanwhocodes/config-array": "^0.11.13",2483 "@humanwhocodes/config-array": "^0.11.14",
2470 "@humanwhocodes/module-importer": "^1.0.1",2484 "@humanwhocodes/module-importer": "^1.0.1",
2471 "@nodelib/fs.walk": "^1.2.8",2485 "@nodelib/fs.walk": "^1.2.8",
2472 "@ungap/structured-clone": "^1.2.0",2486 "@ungap/structured-clone": "^1.2.0",
@@ -3281,12 +3295,12 @@
3281 }3295 }
3282 },3296 },
3283 "node_modules/iconv-lite": {3297 "node_modules/iconv-lite": {
3284 "version": "0.4.24",3298 "version": "0.6.3",
3285 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",3299 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
3286 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",3300 "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
3287 "license": "MIT",3301 "license": "MIT",
3288 "dependencies": {3302 "dependencies": {
3289 "safer-buffer": ">= 2.1.2 < 3"3303 "safer-buffer": ">= 2.1.2 < 3.0.0"
3290 },3304 },
3291 "engines": {3305 "engines": {
3292 "node": ">=0.10.0"3306 "node": ">=0.10.0"
@@ -4617,6 +4631,18 @@
4617 "node": ">= 0.8"4631 "node": ">= 0.8"
4618 }4632 }
4619 },4633 },
4634 "node_modules/raw-body/node_modules/iconv-lite": {
4635 "version": "0.4.24",
4636 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
4637 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
4638 "license": "MIT",
4639 "dependencies": {
4640 "safer-buffer": ">= 2.1.2 < 3"
4641 },
4642 "engines": {
4643 "node": ">=0.10.0"
4644 }
4645 },
4620 "node_modules/readable-stream": {4646 "node_modules/readable-stream": {
4621 "version": "2.3.8",4647 "version": "2.3.8",
4622 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",4648 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
package.json+4 -3
@@ -17,6 +17,7 @@
17 "google-translate-api-browser": "^3.0.1",17 "google-translate-api-browser": "^3.0.1",
18 "he": "^1.2.0",18 "he": "^1.2.0",
19 "helmet": "^7.1.0",19 "helmet": "^7.1.0",
20 "iconv-lite": "^0.6.3",
20 "ip-matching": "^2.1.2",21 "ip-matching": "^2.1.2",
21 "ipaddr.js": "^2.0.1",22 "ipaddr.js": "^2.0.1",
22 "jimp": "^0.22.10",23 "jimp": "^0.22.10",
@@ -32,7 +33,7 @@
32 "rate-limiter-flexible": "^5.0.0",33 "rate-limiter-flexible": "^5.0.0",
33 "response-time": "^2.3.2",34 "response-time": "^2.3.2",
34 "sanitize-filename": "^1.6.3",35 "sanitize-filename": "^1.6.3",
35 "sillytavern-transformers": "^2.14.6",36 "sillytavern-transformers": "2.14.6",
36 "simple-git": "^3.19.1",37 "simple-git": "^3.19.1",
37 "tiktoken": "^1.0.15",38 "tiktoken": "^1.0.15",
38 "vectra": "^0.2.2",39 "vectra": "^0.2.2",
@@ -70,7 +71,7 @@
70 "type": "git",71 "type": "git",
71 "url": "https://github.com/SillyTavern/SillyTavern.git"72 "url": "https://github.com/SillyTavern/SillyTavern.git"
72 },73 },
73 "version": "1.12.4",74 "version": "1.12.5",
74 "scripts": {75 "scripts": {
75 "start": "node server.js",76 "start": "node server.js",
76 "start:no-csrf": "node server.js --disableCsrf",77 "start:no-csrf": "node server.js --disableCsrf",
@@ -90,7 +91,7 @@
90 "main": "server.js",91 "main": "server.js",
91 "devDependencies": {92 "devDependencies": {
92 "@types/jquery": "^3.5.29",93 "@types/jquery": "^3.5.29",
93 "eslint": "^8.55.0",94 "eslint": "^8.57.0",
94 "jquery": "^3.6.4"95 "jquery": "^3.6.4"
95 }96 }
96}97}
public/css/character-group-overlay.css+1 -1
@@ -99,6 +99,6 @@
99}99}
100100
101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {101#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
102 width: 100px;102 width: unset;
103 padding: 0.25em;103 padding: 0.25em;
104}104}
public/css/world-info.css+12 -0
@@ -120,6 +120,14 @@
120 flex-wrap: wrap;120 flex-wrap: wrap;
121}121}
122122
123.world_entry .inline-drawer-header {
124 cursor: initial;
125}
126
127.world_entry .killSwitch {
128 cursor: pointer;
129}
130
123.world_entry_form_control input[type=button] {131.world_entry_form_control input[type=button] {
124 cursor: pointer;132 cursor: pointer;
125}133}
@@ -173,6 +181,10 @@
173 width: 7em;181 width: 7em;
174}182}
175183
184.world_entry .killSwitch.fa-toggle-on {
185 color: var(--SmartThemeQuoteColor);
186}
187
176.wi-card-entry {188.wi-card-entry {
177 border: 1px solid;189 border: 1px solid;
178 border-color: var(--SmartThemeBorderColor);190 border-color: var(--SmartThemeBorderColor);
public/global.d.ts+5 -0
@@ -14,6 +14,11 @@ declare var isProbablyReaderable;
14declare var ePub;14declare var ePub;
15declare var ai;15declare var ai;
1616
17declare var SillyTavern: {
18 getContext(): any;
19 llm: any;
20};
21
17// Jquery plugins22// Jquery plugins
18interface JQuery {23interface JQuery {
19 nanogallery2(options?: any): JQuery;24 nanogallery2(options?: any): JQuery;
public/img/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+51 -12
@@ -383,7 +383,7 @@
383 Max Response Length (tokens)383 Max Response Length (tokens)
384 </div>384 </div>
385 <div class="wide100p">385 <div class="wide100p">
386 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="50" max="8000">386 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="16384">
387 </div>387 </div>
388 </div>388 </div>
389 <div class="range-block" data-source="openai,custom">389 <div class="range-block" data-source="openai,custom">
@@ -1823,10 +1823,16 @@
1823 </div>1823 </div>
1824 <div data-newbie-hidden class="range-block" data-source="claude">1824 <div data-newbie-hidden class="range-block" data-source="claude">
1825 <div class="wide100p">1825 <div class="wide100p">
1826 <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>1826 <div class="flex-container alignItemsCenter">
1827 <textarea id="claude_assistant_prefill" class="text_pole textarea_compact autoSetHeight" name="assistant_prefill" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>1827 <span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
1828 <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>1828 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_prefill" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
1829 <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact autoSetHeight" name="assistant_impersonation" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>1829 </div>
1830 <textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1831 <div class="flex-container alignItemsCenter">
1832 <span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
1833 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="claude_assistant_impersonation" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
1834 </div>
1835 <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation" rows="6" maxlength="100000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1830 </div>1836 </div>
1831 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">1837 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
1832 <input id="claude_use_sysprompt" type="checkbox" />1838 <input id="claude_use_sysprompt" type="checkbox" />
@@ -2427,10 +2433,11 @@
2427 <optgroup>2433 <optgroup>
2428 <option value="01ai">01.AI (Yi)</option>2434 <option value="01ai">01.AI (Yi)</option>
2429 <option value="ai21">AI21</option>2435 <option value="ai21">AI21</option>
2436 <option value="blockentropy">Block Entropy</option>
2430 <option value="claude">Claude</option>2437 <option value="claude">Claude</option>
2431 <option value="cohere">Cohere</option>2438 <option value="cohere">Cohere</option>
2432 <option value="groq">Groq</option>2439 <option value="groq">Groq</option>
2433 <option value="makersuite">Google MakerSuite</option>2440 <option value="makersuite">Google AI Studio</option>
2434 <option value="mistralai">MistralAI</option>2441 <option value="mistralai">MistralAI</option>
2435 <option value="openrouter">OpenRouter</option>2442 <option value="openrouter">OpenRouter</option>
2436 <option value="perplexity">Perplexity</option>2443 <option value="perplexity">Perplexity</option>
@@ -2570,7 +2577,9 @@
2570 </optgroup>2577 </optgroup>
2571 <optgroup label="GPT-4o">2578 <optgroup label="GPT-4o">
2572 <option value="gpt-4o">gpt-4o</option>2579 <option value="gpt-4o">gpt-4o</option>
2580 <option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
2573 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>2581 <option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
2582 <option value="chatgpt-4o-latest">chatgpt-4o-latest</option>
2574 </optgroup>2583 </optgroup>
2575 <optgroup label="gpt-4o-mini">2584 <optgroup label="gpt-4o-mini">
2576 <option value="gpt-4o-mini">gpt-4o-mini</option>2585 <option value="gpt-4o-mini">gpt-4o-mini</option>
@@ -2791,7 +2800,7 @@
2791 </div>2800 </div>
2792 </form>2801 </form>
2793 <form id="makersuite_form" data-source="makersuite" action="javascript:void(null);" method="post" enctype="multipart/form-data">2802 <form id="makersuite_form" data-source="makersuite" action="javascript:void(null);" method="post" enctype="multipart/form-data">
2794 <h4 data-i18n="MakerSuite API Key">MakerSuite API Key</h4>2803 <h4 data-i18n="Google AI Studio API Key">Google AI Studio API Key</h4>
2795 <div class="flex-container">2804 <div class="flex-container">
2796 <input id="api_key_makersuite" name="api_key_makersuite" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">2805 <input id="api_key_makersuite" name="api_key_makersuite" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
2797 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_makersuite"></div>2806 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_makersuite"></div>
@@ -2899,10 +2908,13 @@
2899 </div>2908 </div>
2900 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>2909 <h4 data-i18n="Perplexity Model">Perplexity Model</h4>
2901 <select id="model_perplexity_select">2910 <select id="model_perplexity_select">
2902 <optgroup label="Perplexity Models">2911 <optgroup label="Perplexity Sonar Models">
2903 <option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>2912 <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>
2905 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>2913 <option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
2914 <option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
2915 </optgroup>
2916 <optgroup label="Perplexity Chat Models">
2917 <option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
2906 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>2918 <option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
2907 </optgroup>2919 </optgroup>
2908 <optgroup label="Open-Source Models">2920 <optgroup label="Open-Source Models">
@@ -2951,6 +2963,20 @@
2951 </select>2963 </select>
2952 </div>2964 </div>
2953 </form>2965 </form>
2966 <form id="blockentropy_form" data-source="blockentropy">
2967 <h4 data-i18n="Block Entropy API Key">Block Entropy API Key</h4>
2968 <div class="flex-container">
2969 <input id="api_key_blockentropy" name="api_key_blockentropy" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
2970 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_blockentropy"></div>
2971 </div>
2972 <div data-for="api_key_blockentropy" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
2973 For privacy reasons, your API key will be hidden after you reload the page.
2974 </div>
2975 <h4 data-i18n="Select a Model">Select a Model</h4>
2976 <div class="flex-container">
2977 <select id="model_blockentropy_select" class="text_pole"></select>
2978 </div>
2979 </form>
2954 <form id="custom_form" data-source="custom">2980 <form id="custom_form" data-source="custom">
2955 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>2981 <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
2956 <div class="flex-container">2982 <div class="flex-container">
@@ -3485,6 +3511,7 @@
3485 <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.">3511 <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.">
3486 <small>3512 <small>
3487 <span data-i18n="Min Activations">Min Activations</span>3513 <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>
3488 </small>3515 </small>
3489 <input class="neo-range-slider" type="range" id="world_info_min_activations" name="world_info_min_activations" min="0" max="100" step="1">3516 <input class="neo-range-slider" type="range" id="world_info_min_activations" name="world_info_min_activations" min="0" max="100" step="1">
3490 <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">3517 <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 @@
3498 <input class="neo-range-slider" type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">3525 <input class="neo-range-slider" type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">
3499 <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">3526 <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">
3500 </div>3527 </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
3502 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink flexBasis48p">3537 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink flexBasis48p">
3503 <small data-i18n="Insertion Strategy">3538 <small data-i18n="Insertion Strategy">
@@ -3761,8 +3796,8 @@
3761 <span data-i18n="Font Scale">Font Scale</span>3796 <span data-i18n="Font Scale">Font Scale</span>
3762 <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Font size" title="Font size"></div>3797 <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Font size" title="Font size"></div>
3763 </small>3798 </small>
3764 <input class="neo-range-slider" type="range" id="font_scale" name="font_scale" min="0.8" max="1.2" step="0.01">3799 <input class="neo-range-slider" type="range" id="font_scale" name="font_scale" min="0.5" max="1.5" step="0.01">
3765 <input class="neo-range-input" type="number" min="0.8" max="1.2" step="0.01" data-for="font_scale" id="font_scale_counter">3800 <input class="neo-range-input" type="number" min="0.5" max="1.5" step="0.01" data-for="font_scale" id="font_scale_counter">
3766 </div>3801 </div>
37673802
3768 <div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">3803 <div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
@@ -3961,6 +3996,10 @@
3961 <input id="world_import_dialog" type="checkbox" />3996 <input id="world_import_dialog" type="checkbox" />
3962 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>3997 <small data-i18n="Lorebook Import Dialog">Lorebook Import Dialog</small>
3963 </label>3998 </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>
3964 <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">4003 <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">
3965 <input id="restore_user_input" type="checkbox" />4004 <input id="restore_user_input" type="checkbox" />
3966 <small data-i18n="Restore User Input">Restore User Input</small>4005 <small data-i18n="Restore User Input">Restore User Input</small>
@@ -4565,7 +4604,7 @@
4565 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>4604 <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div>
4566 <input type="hidden" id="fav_checkbox" name="fav" />4605 <input type="hidden" id="fav_checkbox" name="fav" />
4567 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>4606 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
4568 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore" data-i18n="[title]Character Lore"></div>4607 <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div>
4569 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>4608 <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
4570 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>4609 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
4571 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->4610 <!-- <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 @@
390 "Alt Method": "طريقة بديلة",390 "Alt Method": "طريقة بديلة",
391 "AI21 API Key": "مفتاح API لـ AI21",391 "AI21 API Key": "مفتاح API لـ AI21",
392 "AI21 Model": "نموذج AI21",392 "AI21 Model": "نموذج AI21",
393 "MakerSuite API Key": "مفتاح واجهة برمجة تطبيقات MakerSuite",393 "Google AI Studio API Key": "مفتاح واجهة برمجة تطبيقات Google AI Studio",
394 "Google Model": "نموذج جوجل",394 "Google Model": "نموذج جوجل",
395 "MistralAI API Key": "مفتاح واجهة برمجة التطبيقات MistralAI",395 "MistralAI API Key": "مفتاح واجهة برمجة التطبيقات MistralAI",
396 "MistralAI Model": "نموذج ميسترال آي آي",396 "MistralAI Model": "نموذج ميسترال آي آي",
public/locales/de-de.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "Alternative Methode",390 "Alt Method": "Alternative Methode",
391 "AI21 API Key": "AI21 API-Schlüssel",391 "AI21 API Key": "AI21 API-Schlüssel",
392 "AI21 Model": "AI21-Modell",392 "AI21 Model": "AI21-Modell",
393 "MakerSuite API Key": "MakerSuite API-Schlüssel",393 "Google AI Studio API Key": "Google AI Studio API-Schlüssel",
394 "Google Model": "Google-Modell",394 "Google Model": "Google-Modell",
395 "MistralAI API Key": "MistralAI API-Schlüssel",395 "MistralAI API Key": "MistralAI API-Schlüssel",
396 "MistralAI Model": "MistralAI-Modell",396 "MistralAI Model": "MistralAI-Modell",
public/locales/es-es.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "Método alternativo",390 "Alt Method": "Método alternativo",
391 "AI21 API Key": "Clave API de AI21",391 "AI21 API Key": "Clave API de AI21",
392 "AI21 Model": "Modelo de AI21",392 "AI21 Model": "Modelo de AI21",
393 "MakerSuite API Key": "Clave API de MakerSuite",393 "Google AI Studio API Key": "Clave API de Google AI Studio",
394 "Google Model": "Modelo de Google",394 "Google Model": "Modelo de Google",
395 "MistralAI API Key": "Clave API de MistralAI",395 "MistralAI API Key": "Clave API de MistralAI",
396 "MistralAI Model": "Modelo MistralAI",396 "MistralAI Model": "Modelo MistralAI",
public/locales/fr-fr.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "Méthode alternative",390 "Alt Method": "Méthode alternative",
391 "AI21 API Key": "Clé API AI21",391 "AI21 API Key": "Clé API AI21",
392 "AI21 Model": "Modèle AI21",392 "AI21 Model": "Modèle AI21",
393 "MakerSuite API Key": "Clé API MakerSuite",393 "Google AI Studio API Key": "Clé API Google AI Studio",
394 "Google Model": "Modèle Google",394 "Google Model": "Modèle Google",
395 "MistralAI API Key": "Clé API MistralAI",395 "MistralAI API Key": "Clé API MistralAI",
396 "MistralAI Model": "Modèle MistralAI",396 "MistralAI Model": "Modèle MistralAI",
public/locales/is-is.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "Aðferð Bakmenn",390 "Alt Method": "Aðferð Bakmenn",
391 "AI21 API Key": "Lykill API fyrir AI21",391 "AI21 API Key": "Lykill API fyrir AI21",
392 "AI21 Model": "AI21 Módel",392 "AI21 Model": "AI21 Módel",
393 "MakerSuite API Key": "MakerSuite API lykill",393 "Google AI Studio API Key": "Google AI Studio API lykill",
394 "Google Model": "Google líkan",394 "Google Model": "Google líkan",
395 "MistralAI API Key": "MistralAI API lykill",395 "MistralAI API Key": "MistralAI API lykill",
396 "MistralAI Model": "MistralAI líkan",396 "MistralAI Model": "MistralAI líkan",
public/locales/it-it.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "Metodo alternativo",390 "Alt Method": "Metodo alternativo",
391 "AI21 API Key": "Chiave API di AI21",391 "AI21 API Key": "Chiave API di AI21",
392 "AI21 Model": "Modello AI21",392 "AI21 Model": "Modello AI21",
393 "MakerSuite API Key": "Chiave API MakerSuite",393 "Google AI Studio API Key": "Chiave API Google AI Studio",
394 "Google Model": "Modello Google",394 "Google Model": "Modello Google",
395 "MistralAI API Key": "Chiave API MistralAI",395 "MistralAI API Key": "Chiave API MistralAI",
396 "MistralAI Model": "Modello MistralAI",396 "MistralAI Model": "Modello MistralAI",
public/locales/ja-jp.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "代替手法",390 "Alt Method": "代替手法",
391 "AI21 API Key": "AI21のAPIキー",391 "AI21 API Key": "AI21のAPIキー",
392 "AI21 Model": "AI21モデル",392 "AI21 Model": "AI21モデル",
393 "MakerSuite API Key": "MakerSuite APIキー",393 "Google AI Studio API Key": "Google AI Studio APIキー",
394 "Google Model": "Google モデル",394 "Google Model": "Google モデル",
395 "MistralAI API Key": "MistralAI API キー",395 "MistralAI API Key": "MistralAI API キー",
396 "MistralAI Model": "MistralAI モデル",396 "MistralAI Model": "MistralAI モデル",
public/locales/ko-kr.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "대체 방법",390 "Alt Method": "대체 방법",
391 "AI21 API Key": "AI21 API 키",391 "AI21 API Key": "AI21 API 키",
392 "AI21 Model": "AI21 모델",392 "AI21 Model": "AI21 모델",
393 "MakerSuite API Key": "MakerSuite API 키",393 "Google AI Studio API Key": "Google AI Studio API 키",
394 "Google Model": "구글 모델",394 "Google Model": "구글 모델",
395 "MistralAI API Key": "MistralAI API 키",395 "MistralAI API Key": "MistralAI API 키",
396 "MistralAI Model": "MistralAI 모델",396 "MistralAI Model": "MistralAI 모델",
public/locales/pt-pt.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "Método Alternativo",390 "Alt Method": "Método Alternativo",
391 "AI21 API Key": "Chave da API AI21",391 "AI21 API Key": "Chave da API AI21",
392 "AI21 Model": "Modelo AI21",392 "AI21 Model": "Modelo AI21",
393 "MakerSuite API Key": "Chave API MakerSuite",393 "Google AI Studio API Key": "Chave API Google AI Studio",
394 "Google Model": "Modelo Google",394 "Google Model": "Modelo Google",
395 "MistralAI API Key": "Chave de API MistralAI",395 "MistralAI API Key": "Chave de API MistralAI",
396 "MistralAI Model": "Modelo MistralAI",396 "MistralAI Model": "Modelo MistralAI",
public/locales/ru-ru.json+1 -1
@@ -722,7 +722,7 @@
722 "Proxy Server URL": "Адрес прокси-сервера",722 "Proxy Server URL": "Адрес прокси-сервера",
723 "MistralAI Model": "Модель MistralAI",723 "MistralAI Model": "Модель MistralAI",
724 "MistralAI API Key": "Ключ от API MistralAI",724 "MistralAI API Key": "Ключ от API MistralAI",
725 "MakerSuite API Key": "Ключ от API MakerSuite",725 "Google AI Studio API Key": "Ключ от API Google AI Studio",
726 "Google Model": "Модель Google",726 "Google Model": "Модель Google",
727 "Cohere API Key": "Ключ от API Cohere",727 "Cohere API Key": "Ключ от API Cohere",
728 "Cohere Model": "Модель Cohere",728 "Cohere Model": "Модель Cohere",
public/locales/uk-ua.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "Альтернативний метод",390 "Alt Method": "Альтернативний метод",
391 "AI21 API Key": "Ключ API для AI21",391 "AI21 API Key": "Ключ API для AI21",
392 "AI21 Model": "Модель AI21",392 "AI21 Model": "Модель AI21",
393 "MakerSuite API Key": "Ключ API MakerSuite",393 "Google AI Studio API Key": "Ключ API Google AI Studio",
394 "Google Model": "Модель Google",394 "Google Model": "Модель Google",
395 "MistralAI API Key": "Ключ API MistralAI",395 "MistralAI API Key": "Ключ API MistralAI",
396 "MistralAI Model": "Модель MistralAI",396 "MistralAI Model": "Модель MistralAI",
public/locales/vi-vn.json+1 -1
@@ -390,7 +390,7 @@
390 "Alt Method": "Phương pháp thay thế",390 "Alt Method": "Phương pháp thay thế",
391 "AI21 API Key": "Khóa API của AI21",391 "AI21 API Key": "Khóa API của AI21",
392 "AI21 Model": "Mô hình AI21",392 "AI21 Model": "Mô hình AI21",
393 "MakerSuite API Key": "Khóa API MakerSuite",393 "Google AI Studio API Key": "Khóa API Google AI Studio",
394 "Google Model": "Mô hình Google",394 "Google Model": "Mô hình Google",
395 "MistralAI API Key": "Khóa API MistralAI",395 "MistralAI API Key": "Khóa API MistralAI",
396 "MistralAI Model": "Mô hình MistralAI",396 "MistralAI Model": "Mô hình MistralAI",
public/locales/zh-cn.json+10 -3
@@ -406,7 +406,7 @@
406 "Alt Method": "备用方法",406 "Alt Method": "备用方法",
407 "AI21 API Key": "AI21 API 密钥",407 "AI21 API Key": "AI21 API 密钥",
408 "AI21 Model": "AI21 模型",408 "AI21 Model": "AI21 模型",
409 "MakerSuite API Key": "MakerSuite API 密钥",409 "Google AI Studio API Key": "Google AI Studio API 密钥",
410 "Google Model": "Google 模型",410 "Google Model": "Google 模型",
411 "MistralAI API Key": "MistralAI API 密钥",411 "MistralAI API Key": "MistralAI API 密钥",
412 "MistralAI Model": "MistralAI 模型",412 "MistralAI Model": "MistralAI 模型",
@@ -707,10 +707,10 @@
707 "Restore User Input": "恢复用户输入",707 "Restore User Input": "恢复用户输入",
708 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",708 "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "允许通过拖动重新定位某些UI元素。仅适用于PC,对移动设备无影响",
709 "Movable UI Panels": "可移动 UI 面板",709 "Movable UI Panels": "可移动 UI 面板",
710 "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
710 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",711 "MovingUI preset. Predefined/saved draggable positions": "可移动UI预设。预定义/保存的可拖动位置",
711 "MUI Preset": "可移动 UI 预设",712 "MUI Preset": "可移动 UI 预设",
712 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",713 "Save movingUI changes to a new file": "将可移动UI更改保存到新文件中",
713 "Reset MovingUI panel sizes/locations.": "重置 MovingUI 面板大小/位置。",
714 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",714 "Apply a custom CSS style to all of the ST GUI": "将自定义CSS样式应用于所有ST GUI",
715 "Custom CSS": "自定义 CSS",715 "Custom CSS": "自定义 CSS",
716 "Expand the editor": "展开编辑器",716 "Expand the editor": "展开编辑器",
@@ -730,6 +730,8 @@
730 "Press Send to continue": "按发送键以继续",730 "Press Send to continue": "按发送键以继续",
731 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",731 "Show a button in the input area to ask the AI to continue (extend) its last message": "在输入区域中显示一个按钮,要求AI继续(延长)其上一条消息",
732 "Quick 'Continue' button": "快速“继续”按钮",732 "Quick 'Continue' button": "快速“继续”按钮",
733 "Show a button in the input area to ask the AI to impersonate your character for a single message": "在输入区域中显示一个按钮,让 AI 模仿你的角色发送一条消息。",
734 "Quick 'Impersonate' button": "快速“模仿”按钮",
733 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",735 "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "在聊天窗口的最后一条信息上显示箭头按钮,以生成AI的其他回复选项。适用于电脑和手机端。",
734 "Swipes": "刷新回复按钮",736 "Swipes": "刷新回复按钮",
735 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",737 "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "允许在最后一条聊天消息上使用滑动手势触发滑动生成。仅适用于移动设备,对PC无影响",
@@ -1183,6 +1185,7 @@
1183 "Pause script execution": "暂停执行脚本",1185 "Pause script execution": "暂停执行脚本",
1184 "Abort script execution": "中止执行脚本",1186 "Abort script execution": "中止执行脚本",
1185 "Abort request": "中止请求",1187 "Abort request": "中止请求",
1188 "Ask AI to write your message for you": "让AI为您撰写消息",
1186 "Continue the last message": "继续上一条消息",1189 "Continue the last message": "继续上一条消息",
1187 "Send a message": "发送消息",1190 "Send a message": "发送消息",
1188 "Close chat": "关闭聊天",1191 "Close chat": "关闭聊天",
@@ -1194,7 +1197,6 @@
1194 "Manage chat files": "管理聊天文件",1197 "Manage chat files": "管理聊天文件",
1195 "Delete messages": "删除消息",1198 "Delete messages": "删除消息",
1196 "Regenerate": "重新生成",1199 "Regenerate": "重新生成",
1197 "Ask AI to write your message for you": "请求AI为您撰写消息",
1198 "Impersonate": "AI 帮答",1200 "Impersonate": "AI 帮答",
1199 "Continue": "继续",1201 "Continue": "继续",
1200 "Bind user name to that avatar": "将用户名称绑定到该头像",1202 "Bind user name to that avatar": "将用户名称绑定到该头像",
@@ -1429,6 +1431,7 @@
1429 "ext_regex_export_script": "导出脚本",1431 "ext_regex_export_script": "导出脚本",
1430 "ext_regex_delete_script": "删除脚本",1432 "ext_regex_delete_script": "删除脚本",
1431 "Trigger Stable Diffusion": "触发Stable Diffusion",1433 "Trigger Stable Diffusion": "触发Stable Diffusion",
1434 "Abort current image generation task": "中止当前图像生成",
1432 "sd_Yourself": "你自己",1435 "sd_Yourself": "你自己",
1433 "sd_Your_Face": "你的脸",1436 "sd_Your_Face": "你的脸",
1434 "sd_Me": "我",1437 "sd_Me": "我",
@@ -1582,6 +1585,10 @@
1582 "Only used when Main API is selected.": "仅在选择主 API 时使用。",1585 "Only used when Main API is selected.": "仅在选择主 API 时使用。",
1583 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",1586 "Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.": "随着您聊天,旧消息会逐渐矢量化。\n要处理所有以前的消息,请单击下面的按钮。",
1584 "View Stats": "查看统计数据",1587 "View Stats": "查看统计数据",
1588 "Title/Memo": "标题/备忘录",
1589 "Status": "状态",
1590 "Position": "位置",
1591 "Trigger %": "触发率 %",
1585 "Manager Users": "管理用户",1592 "Manager Users": "管理用户",
1586 "New User": "新用户",1593 "New User": "新用户",
1587 "Status:": "地位:",1594 "Status:": "地位:",
public/locales/zh-tw.json+1 -1
@@ -391,7 +391,7 @@
391 "Alt Method": "替代方法",391 "Alt Method": "替代方法",
392 "AI21 API Key": "AI21 API 金鑰",392 "AI21 API Key": "AI21 API 金鑰",
393 "AI21 Model": "AI21 模型",393 "AI21 Model": "AI21 模型",
394 "MakerSuite API Key": "MakerSuite API 金鑰",394 "Google AI Studio API Key": "Google AI Studio API 金鑰",
395 "Google Model": "Google 模型",395 "Google Model": "Google 模型",
396 "MistralAI API Key": "MistralAI API 金鑰",396 "MistralAI API Key": "MistralAI API 金鑰",
397 "MistralAI Model": "MistralAI 模型",397 "MistralAI Model": "MistralAI 模型",
public/script.js+166 -85
@@ -84,6 +84,7 @@ import {
84 context_presets,84 context_presets,
85 resetMovableStyles,85 resetMovableStyles,
86 forceCharacterEditorTokenize,86 forceCharacterEditorTokenize,
87 applyPowerUserSettings,
87} from './scripts/power-user.js';88} from './scripts/power-user.js';
8889
89import {90import {
@@ -156,6 +157,7 @@ import {
156 ensureImageFormatSupported,157 ensureImageFormatSupported,
157 flashHighlight,158 flashHighlight,
158 isTrueBoolean,159 isTrueBoolean,
160 toggleDrawer,
159} from './scripts/utils.js';161} from './scripts/utils.js';
160import { debounce_timeout } from './scripts/constants.js';162import { debounce_timeout } from './scripts/constants.js';
161163
@@ -224,7 +226,7 @@ import {
224import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';226import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
225import { hideLoader, showLoader } from './scripts/loader.js';227import { hideLoader, showLoader } from './scripts/loader.js';
226import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';228import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
227import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js';229import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels } from './scripts/textgen-models.js';
228import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';230import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
229import { initPresetManager } from './scripts/preset-manager.js';231import { initPresetManager } from './scripts/preset-manager.js';
230import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros.js';232import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros.js';
@@ -241,7 +243,7 @@ import { DragAndDropHandler } from './scripts/dragdrop.js';
241import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';243import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
242import { initDynamicStyles } from './scripts/dynamic-styles.js';244import { initDynamicStyles } from './scripts/dynamic-styles.js';
243import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';245import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
244import { enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';246import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
245247
246//exporting functions and vars for mods248//exporting functions and vars for mods
247export {249export {
@@ -414,6 +416,7 @@ export const event_types = {
414 GENERATION_STOPPED: 'generation_stopped',416 GENERATION_STOPPED: 'generation_stopped',
415 GENERATION_ENDED: 'generation_ended',417 GENERATION_ENDED: 'generation_ended',
416 EXTENSIONS_FIRST_LOAD: 'extensions_first_load',418 EXTENSIONS_FIRST_LOAD: 'extensions_first_load',
419 EXTENSION_SETTINGS_LOADED: 'extension_settings_loaded',
417 SETTINGS_LOADED: 'settings_loaded',420 SETTINGS_LOADED: 'settings_loaded',
418 SETTINGS_UPDATED: 'settings_updated',421 SETTINGS_UPDATED: 'settings_updated',
419 GROUP_UPDATED: 'group_updated',422 GROUP_UPDATED: 'group_updated',
@@ -424,6 +427,8 @@ export const event_types = {
424 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',427 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
425 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',428 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
426 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',429 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',
427 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',432 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
428 WORLDINFO_UPDATED: 'worldinfo_updated',433 WORLDINFO_UPDATED: 'worldinfo_updated',
429 CHARACTER_EDITED: 'character_edited',434 CHARACTER_EDITED: 'character_edited',
@@ -456,6 +461,7 @@ export const event_types = {
456 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',461 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
457 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',462 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
458 ONLINE_STATUS_CHANGED: 'online_status_changed',463 ONLINE_STATUS_CHANGED: 'online_status_changed',
464 IMAGE_SWIPED: 'image_swiped',
459};465};
460466
461export const eventSource = new EventEmitter();467export const eventSource = new EventEmitter();
@@ -911,6 +917,7 @@ async function firstLoadInit() {
911 await readSecretState();917 await readSecretState();
912 initLocales();918 initLocales();
913 initDefaultSlashCommands();919 initDefaultSlashCommands();
920 initTextGenModels();
914 await getSystemMessages();921 await getSystemMessages();
915 sendSystemMessage(system_message_types.WELCOME);922 sendSystemMessage(system_message_types.WELCOME);
916 await getSettings();923 await getSettings();
@@ -1874,7 +1881,12 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
1874 }1881 }
18751882
1876 if (Number(messageId) === 0 && !isSystem && !isUser) {1883 if (Number(messageId) === 0 && !isSystem && !isUser) {
1884 const mesBeforeReplace = mes;
1885 const chatMessage = chat[messageId];
1877 mes = substituteParams(mes, undefined, ch_name);1886 mes = substituteParams(mes, undefined, ch_name);
1887 if (chatMessage && chatMessage.mes === mesBeforeReplace && chatMessage.extra?.display_text !== mesBeforeReplace) {
1888 chatMessage.mes = mes;
1889 }
1878 }1890 }
18791891
1880 mesForShowdownParse = mes;1892 mesForShowdownParse = mes;
@@ -2108,6 +2120,7 @@ export function updateMessageBlock(messageId, message) {
2108export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {2120export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
2109 // Add image to message2121 // Add image to message
2110 if (mes.extra?.image) {2122 if (mes.extra?.image) {
2123 const container = messageElement.find('.mes_img_container');
2111 const chatHeight = $('#chat').prop('scrollHeight');2124 const chatHeight = $('#chat').prop('scrollHeight');
2112 const image = messageElement.find('.mes_img');2125 const image = messageElement.find('.mes_img');
2113 const text = messageElement.find('.mes_text');2126 const text = messageElement.find('.mes_text');
@@ -2123,9 +2136,27 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
2123 });2136 });
2124 image.attr('src', mes.extra?.image);2137 image.attr('src', mes.extra?.image);
2125 image.attr('title', mes.extra?.title || mes.title || '');2138 image.attr('title', mes.extra?.title || mes.title || '');
2126 messageElement.find('.mes_img_container').addClass('img_extra');2139 container.addClass('img_extra');
2127 image.toggleClass('img_inline', isInline);2140 image.toggleClass('img_inline', isInline);
2128 text.toggleClass('displayNone', !isInline);2141 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 }
2129 }2160 }
21302161
2131 // Add file to message2162 // Add file to message
@@ -2492,8 +2523,8 @@ export function getStoppingStrings(isImpersonate, isContinue) {
2492 result.push(charString);2523 result.push(charString);
2493 }2524 }
24942525
2495 // Add other group members as the stopping strings2526 // 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)
2496 if (selected_group) {2527 if (selected_group && (name2 || isImpersonate)) {
2497 const group = groups.find(x => x.id === selected_group);2528 const group = groups.find(x => x.id === selected_group);
24982529
2499 if (group && Array.isArray(group.members)) {2530 if (group && Array.isArray(group.members)) {
@@ -2815,7 +2846,14 @@ function hideStopButton() {
2815}2846}
28162847
2817class StreamingProcessor {2848class 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) {
2819 this.result = '';2857 this.result = '';
2820 this.messageId = -1;2858 this.messageId = -1;
2821 this.messageDom = null;2859 this.messageDom = null;
@@ -2825,14 +2863,14 @@ class StreamingProcessor {
2825 /** @type {HTMLTextAreaElement} */2863 /** @type {HTMLTextAreaElement} */
2826 this.sendTextarea = document.querySelector('#send_textarea');2864 this.sendTextarea = document.querySelector('#send_textarea');
2827 this.type = type;2865 this.type = type;
2828 this.force_name2 = force_name2;2866 this.force_name2 = forceName2;
2829 this.isStopped = false;2867 this.isStopped = false;
2830 this.isFinished = false;2868 this.isFinished = false;
2831 this.generator = this.nullStreamingGeneration;2869 this.generator = this.nullStreamingGeneration;
2832 this.abortController = new AbortController();2870 this.abortController = new AbortController();
2833 this.firstMessageText = '...';2871 this.firstMessageText = '...';
2834 this.timeStarted = timeStarted;2872 this.timeStarted = timeStarted;
2835 this.messageAlreadyGenerated = messageAlreadyGenerated;2873 this.continueMessage = type === 'continue' ? continueMessage : '';
2836 this.swipes = [];2874 this.swipes = [];
2837 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */2875 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
2838 this.messageLogprobs = [];2876 this.messageLogprobs = [];
@@ -2985,8 +3023,7 @@ class StreamingProcessor {
2985 await eventSource.emit(event_types.IMPERSONATE_READY, text);3023 await eventSource.emit(event_types.IMPERSONATE_READY, text);
2986 }3024 }
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);
2990 await saveChatConditional();3027 await saveChatConditional();
2991 unblockGeneration();3028 unblockGeneration();
2992 generatedPromptCache = '';3029 generatedPromptCache = '';
@@ -3082,7 +3119,7 @@ class StreamingProcessor {
3082 if (logprobs) {3119 if (logprobs) {
3083 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3120 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3084 }3121 }
3085 await sw.tick(() => this.onProgressStreaming(this.messageId, this.messageAlreadyGenerated + text));3122 await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
3086 }3123 }
3087 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;3124 const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
3088 console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);3125 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
3275 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';3312 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
3276 const isImpersonate = type == 'impersonate';3313 const isImpersonate = type == 'impersonate';
32773314
3278 let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
3279
3280 if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {3315 if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
3281 const interruptedByCommand = await processCommands(String($('#send_textarea').val()));3316 const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
32823317
@@ -3715,7 +3750,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3715 let oaiMessageExamples = [];3750 let oaiMessageExamples = [];
37163751
3717 if (main_api === 'openai') {3752 if (main_api === 'openai') {
3718 message_already_generated = '';
3719 oaiMessages = setOpenAIMessages(coreChat);3753 oaiMessages = setOpenAIMessages(coreChat);
3720 oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray);3754 oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray);
3721 }3755 }
@@ -3858,7 +3892,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3858 cyclePrompt += oai_settings.continue_postfix;3892 cyclePrompt += oai_settings.continue_postfix;
3859 continue_mag += oai_settings.continue_postfix;3893 continue_mag += oai_settings.continue_postfix;
3860 }3894 }
3861 message_already_generated = continue_mag;
3862 }3895 }
38633896
3864 const originalType = type;3897 const originalType = type;
@@ -3943,7 +3976,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
39433976
3944 // Get instruct mode line3977 // Get instruct mode line
3945 if (isInstruct && !isContinue) {3978 if (isInstruct && !isContinue) {
3946 const name = (quiet_prompt && !quietToLoud) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);3979 const name = (quiet_prompt && !quietToLoud && !isImpersonate) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
3947 const isQuiet = quiet_prompt && type == 'quiet';3980 const isQuiet = quiet_prompt && type == 'quiet';
3948 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);3981 lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);
3949 }3982 }
@@ -4285,7 +4318,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4285 console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);4318 console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);
42864319
4287 if (isStreamingEnabled() && type !== 'quiet') {4320 if (isStreamingEnabled() && type !== 'quiet') {
4288 streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, message_already_generated);4321 streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, continue_mag);
4289 if (isContinue) {4322 if (isContinue) {
4290 // Save reply does add cycle text to the prompt, so it's not needed here4323 // Save reply does add cycle text to the prompt, so it's not needed here
4291 streamingProcessor.firstMessageText = '';4324 streamingProcessor.firstMessageText = '';
@@ -5313,17 +5346,10 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5313 // Regex uses vars, so add before formatting5346 // Regex uses vars, so add before formatting
5314 getMessage = getRegexedString(getMessage, isImpersonate ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT);5347 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
5320 if (power_user.collapse_newlines) {5349 if (power_user.collapse_newlines) {
5321 getMessage = collapseNewlines(getMessage);5350 getMessage = collapseNewlines(getMessage);
5322 }5351 }
53235352
5324 if (power_user.trim_spaces) {
5325 getMessage = getMessage.trim();
5326 }
5327 // trailing invisible whitespace before every newlines, on a multiline string5353 // trailing invisible whitespace before every newlines, on a multiline string
5328 // "trailing whitespace on newlines \nevery line of the string \n?sample text" ->5354 // "trailing whitespace on newlines \nevery line of the string \n?sample text" ->
5329 // "trailing whitespace on newlines\nevery line of the string\nsample text"5355 // "trailing whitespace on newlines\nevery line of the string\nsample text"
@@ -5402,9 +5428,11 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5402 getMessage = fixMarkdown(getMessage, false);5428 getMessage = fixMarkdown(getMessage, false);
5403 }5429 }
54045430
5405 const nameToTrim2 = isImpersonate ? name1 : name2;5431 const nameToTrim2 = isImpersonate
5432 ? (!power_user.allow_name1_display ? name1 : '')
5433 : (!power_user.allow_name2_display ? name2 : '');
54065434
5407 if (getMessage.startsWith(nameToTrim2 + ':')) {5435 if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) {
5408 getMessage = getMessage.replace(nameToTrim2 + ':', '');5436 getMessage = getMessage.replace(nameToTrim2 + ':', '');
5409 getMessage = getMessage.trimStart();5437 getMessage = getMessage.trimStart();
5410 }5438 }
@@ -5413,6 +5441,14 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
5413 getMessage = getMessage.trim();5441 getMessage = getMessage.trim();
5414 }5442 }
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
5416 return getMessage;5452 return getMessage;
5417}5453}
54185454
@@ -6426,6 +6462,8 @@ export async function getSettings() {
6426 // Load power user settings6462 // Load power user settings
6427 await loadPowerUserSettings(settings, data);6463 await loadPowerUserSettings(settings, data);
64286464
6465 applyPowerUserSettings();
6466
6429 // Load character tags6467 // Load character tags
6430 loadTagsSettings(settings);6468 loadTagsSettings(settings);
64316469
@@ -6480,9 +6518,10 @@ export async function getSettings() {
6480 selected_button = settings.selected_button;6518 selected_button = settings.selected_button;
64816519
6482 if (data.enable_extensions) {6520 if (data.enable_extensions) {
6521 const enableAutoUpdate = Boolean(data.enable_extensions_auto_update);
6483 const isVersionChanged = settings.currentVersion !== currentVersion;6522 const isVersionChanged = settings.currentVersion !== currentVersion;
6484 await loadExtensionSettings(settings, isVersionChanged);6523 await loadExtensionSettings(settings, isVersionChanged, enableAutoUpdate);
6485 eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);6524 await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
6486 }6525 }
64876526
6488 firstRun = !!settings.firstRun;6527 firstRun = !!settings.firstRun;
@@ -8353,6 +8392,12 @@ const CONNECT_API_MAP = {
8353 button: '#api_button_openai',8392 button: '#api_button_openai',
8354 source: chat_completion_sources.OPENAI,8393 source: chat_completion_sources.OPENAI,
8355 },8394 },
8395 // Google alias
8396 'google': {
8397 selected: 'openai',
8398 button: '#api_button_openai',
8399 source: chat_completion_sources.MAKERSUITE,
8400 },
8356 // OpenRouter special naming, to differentiate between chat comp and text comp8401 // OpenRouter special naming, to differentiate between chat comp and text comp
8357 'openrouter': {8402 'openrouter': {
8358 selected: 'openai',8403 selected: 'openai',
@@ -8366,6 +8411,9 @@ const CONNECT_API_MAP = {
8366 },8411 },
8367};8412};
83688413
8414// Collect all unique API names in an array
8415export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
8416
8369// Fill connections map from textgen_types and chat_completion_sources8417// Fill connections map from textgen_types and chat_completion_sources
8370for (const textGenType of Object.values(textgen_types)) {8418for (const textGenType of Object.values(textgen_types)) {
8371 if (CONNECT_API_MAP[textGenType]) continue;8419 if (CONNECT_API_MAP[textGenType]) continue;
@@ -8435,7 +8483,7 @@ async function disableInstructCallback() {
8435/**8483/**
8436 * @param {string} text API name8484 * @param {string} text API name
8437 */8485 */
8438async function connectAPISlash(_, text) {8486async function connectAPISlash(args, text) {
8439 if (!text.trim()) {8487 if (!text.trim()) {
8440 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {8488 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
8441 if (config.selected !== main_api) continue;8489 if (config.selected !== main_api) continue;
@@ -8458,12 +8506,15 @@ async function connectAPISlash(_, text) {
84588506
8459 return key;8507 return key;
8460 }8508 }
8509
8510 console.error('FIXME: The current API is not in the API map');
8511 return '';
8461 }8512 }
84628513
8463 const apiConfig = CONNECT_API_MAP[text.toLowerCase()];8514 const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
8464 if (!apiConfig) {8515 if (!apiConfig) {
8465 toastr.error(`Error: ${text} is not a valid API`);8516 toastr.error(`Error: ${text} is not a valid API`);
8466 return;8517 return '';
8467 }8518 }
84688519
8469 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);8520 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
@@ -8483,14 +8534,18 @@ async function connectAPISlash(_, text) {
8483 $(apiConfig.button).trigger('click');8534 $(apiConfig.button).trigger('click');
8484 }8535 }
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
8488 try {8540 try {
8489 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);8541 await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
8490 console.log('Connection successful');8542 console.log('Connection successful');
8491 } catch {8543 } catch {
8492 console.log('Could not connect after 5 seconds, skipping.');8544 console.log('Could not connect after 10 seconds, skipping.');
8493 }8545 }
8546
8547 toastr.clear(toast);
8548 return text;
8494}8549}
84958550
8496/**8551/**
@@ -8940,9 +8995,6 @@ jQuery(async function () {
8940 return '';8995 return '';
8941 }8996 }
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
8946 SlashCommandParser.addCommandObject(SlashCommand.fromProps({8998 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
8947 name: 'dupe',8999 name: 'dupe',
8948 callback: duplicateCharacter,9000 callback: duplicateCharacter,
@@ -8951,13 +9003,22 @@ jQuery(async function () {
8951 SlashCommandParser.addCommandObject(SlashCommand.fromProps({9003 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
8952 name: 'api',9004 name: 'api',
8953 callback: connectAPISlash,9005 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 ],
8954 unnamedArgumentList: [9016 unnamedArgumentList: [
8955 SlashCommandArgument.fromProps({9017 SlashCommandArgument.fromProps({
8956 description: 'API to connect to',9018 description: 'API to connect to',
8957 typeList: [ARGUMENT_TYPE.STRING],9019 typeList: [ARGUMENT_TYPE.STRING],
8958 isRequired: false,
8959 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>9020 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
8960 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIs.findIndex(x => x === selected)),9021 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
8961 selected[0].toUpperCase() ?? enumIcons.default)),9022 selected[0].toUpperCase() ?? enumIcons.default)),
8962 }),9023 }),
8963 ],9024 ],
@@ -10615,15 +10676,31 @@ jQuery(async function () {
10615 }10676 }
10616 });10677 });
1061710678
10618 $(document).on('click', '#OpenAllWIEntries', function () {10679 document.addEventListener('click', function (e) {
10619 $('#world_popup_entries_list').children().find('.down').click();10680 if (!(e.target instanceof HTMLElement)) return;
10620 });10681 if (e.target.matches('#OpenAllWIEntries')) {
10621 $(document).on('click', '#CloseAllWIEntries', function () {10682 document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10622 $('#world_popup_entries_list').children().find('.up').click();10683 toggleDrawer(drawer, true);
10684 });
10685 } else if (e.target.matches('#CloseAllWIEntries')) {
10686 document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => {
10687 toggleDrawer(drawer, false);
10688 });
10689 }
10623 });10690 });
10691
10624 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);10692 $(document).on('click', '.open_alternate_greetings', openAlternateGreetings);
10625 /* $('#set_character_world').on('click', openCharacterWorldPopup); */10693 /* $('#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
10627 $(document).keyup(function (e) {10704 $(document).keyup(function (e) {
10628 if (e.key === 'Escape') {10705 if (e.key === 'Escape') {
10629 const isEditVisible = $('#curEditTextarea').is(':visible');10706 const isEditVisible = $('#curEditTextarea').is(':visible');
@@ -10707,7 +10784,7 @@ jQuery(async function () {
10707 }10784 }
10708 } break;10785 } break;
10709 case 'import_tags': {10786 case 'import_tags': {
10710 await importTags(characters[this_chid], { forceShow: true });10787 await importTags(characters[this_chid], { importSetting: tag_import_setting.ASK });
10711 } break;10788 } break;
10712 /*case 'delete_button':10789 /*case 'delete_button':
10713 popup_type = "del_ch";10790 popup_type = "del_ch";
@@ -10736,62 +10813,66 @@ jQuery(async function () {
10736 var isManualInput = false;10813 var isManualInput = false;
10737 var valueBeforeManualInput;10814 var valueBeforeManualInput;
1073810815
10739 $('.range-block-counter input, .neo-range-input').on('click', function () {10816 $(document).on('input', '.range-block-counter input, .neo-range-input', function () {
10740 valueBeforeManualInput = $(this).val();10817 valueBeforeManualInput = $(this).val();
10741 console.log(valueBeforeManualInput);10818 console.log(valueBeforeManualInput);
10742 })10819 });
10743 .on('change', function (e) {10820
10744 e.target.focus();10821 $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {
10745 e.target.dispatchEvent(new Event('keyup'));10822 e.target.focus();
10746 })10823 e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
10747 .on('keydown', function (e) {10824 });
10748 const masterSelector = '#' + $(this).data('for');10825
10749 const masterElement = $(masterSelector);10826 $(document).on('keydown', '.range-block-counter input, .neo-range-input', function (e) {
10750 if (e.key === 'Enter') {10827 const masterSelector = '#' + $(this).data('for');
10751 let manualInput = Number($(this).val());10828 const masterElement = $(masterSelector);
10752 if (isManualInput) {10829 if (e.key === 'Enter') {
10753 //disallow manual inputs outside acceptable range
10754 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10755 //if value is ok, assign to slider and update handle text and position
10756 //newSlider.val(manualInput)
10757 //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
10758 valueBeforeManualInput = manualInput;
10759 $(masterElement).val($(this).val()).trigger('input', { forced: true });
10760 } else {
10761 //if value not ok, warn and reset to last known valid value
10762 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10763 console.log(valueBeforeManualInput);
10764 //newSlider.val(valueBeforeManualInput)
10765 $(this).val(valueBeforeManualInput);
10766 }
10767 }
10768 }
10769 })
10770 .on('keyup', function () {
10771 valueBeforeManualInput = $(this).val();
10772 console.log(valueBeforeManualInput);
10773 isManualInput = true;
10774 })
10775 //trigger slider changes when user clicks away
10776 .on('mouseup blur', function () {
10777 const masterSelector = '#' + $(this).data('for');
10778 const masterElement = $(masterSelector);
10779 let manualInput = Number($(this).val());10830 let manualInput = Number($(this).val());
10780 if (isManualInput) {10831 if (isManualInput) {
10781 //if value is between correct range for the slider10832 //disallow manual inputs outside acceptable range
10782 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {10833 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10834 //if value is ok, assign to slider and update handle text and position
10835 //newSlider.val(manualInput)
10836 //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual');
10783 valueBeforeManualInput = manualInput;10837 valueBeforeManualInput = manualInput;
10784 //set the slider value to input value
10785 $(masterElement).val($(this).val()).trigger('input', { forced: true });10838 $(masterElement).val($(this).val()).trigger('input', { forced: true });
10786 } else {10839 } else {
10787 //if value not ok, warn and reset to last known valid value10840 //if value not ok, warn and reset to last known valid value
10788 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);10841 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10789 console.log(valueBeforeManualInput);10842 console.log(valueBeforeManualInput);
10843 //newSlider.val(valueBeforeManualInput)
10790 $(this).val(valueBeforeManualInput);10844 $(this).val(valueBeforeManualInput);
10791 }10845 }
10792 }10846 }
10793 isManualInput = false;10847 }
10794 });10848 });
10849
10850 $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {
10851 valueBeforeManualInput = $(this).val();
10852 console.log(valueBeforeManualInput);
10853 isManualInput = true;
10854 });
10855
10856 //trigger slider changes when user clicks away
10857 $(document).on('mouseup blur', '.range-block-counter input, .neo-range-input', function () {
10858 const masterSelector = '#' + $(this).data('for');
10859 const masterElement = $(masterSelector);
10860 let manualInput = Number($(this).val());
10861 if (isManualInput) {
10862 //if value is between correct range for the slider
10863 if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) {
10864 valueBeforeManualInput = manualInput;
10865 //set the slider value to input value
10866 $(masterElement).val($(this).val()).trigger('input', { forced: true });
10867 } else {
10868 //if value not ok, warn and reset to last known valid value
10869 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
10870 console.log(valueBeforeManualInput);
10871 $(this).val(valueBeforeManualInput);
10872 }
10873 }
10874 isManualInput = false;
10875 });
1079510876
10796 $('.user_stats_button').on('click', function () {10877 $('.user_stats_button').on('click', function () {
10797 userStatsHandler();10878 userStatsHandler();
public/scripts/BulkEditOverlay.js+34 -4
@@ -18,7 +18,7 @@ import {
18import { favsToHotswap } from './RossAscends-mods.js';18import { favsToHotswap } from './RossAscends-mods.js';
19import { hideLoader, showLoader } from './loader.js';19import { hideLoader, showLoader } from './loader.js';
20import { convertCharacterToPersona } from './personas.js';20import { convertCharacterToPersona } from './personas.js';
21import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap } from './tags.js';21import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
2222
23/**23/**
24 * Static object representing the actions of the24 * Static object representing the actions of the
@@ -197,10 +197,10 @@ class BulkTagPopupHandler {
197 #getHtml = () => {197 #getHtml = () => {
198 const characterData = JSON.stringify({ characterIds: this.characterIds });198 const characterData = JSON.stringify({ characterIds: this.characterIds });
199 return `<div id="bulk_tag_shadow_popup">199 return `<div id="bulk_tag_shadow_popup">
200 <div id="bulk_tag_popup">200 <div id="bulk_tag_popup" class="wider_dialogue_popup">
201 <div id="bulk_tag_popup_holder">201 <div id="bulk_tag_popup_holder">
202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>202 <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters.</small>203 <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters. Import all or existing tags for all selected characters.</small>
204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>204 <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
205 <br>205 <br>
206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>206 <div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
@@ -219,6 +219,12 @@ class BulkTagPopupHandler {
219 <i class="fa-solid fa-trash-can margin-right-10px"></i>219 <i class="fa-solid fa-trash-can margin-right-10px"></i>
220 Mutual220 Mutual
221 </div>221 </div>
222 <div id="bulk_tag_popup_import_all_tags" class="menu_button" title="Import all tags from selected characters" data-i18n="[title]Import all tags from selected characters">
223 Import All
224 </div>
225 <div id="bulk_tag_popup_import_existing_tags" class="menu_button" title="Import existing tags from selected characters" data-i18n="[title]Import existing tags from selected characters">
226 Import Existing
227 </div>
222 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>228 <div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
223 </div>229 </div>
224 </div>230 </div>
@@ -254,6 +260,30 @@ class BulkTagPopupHandler {
254 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));260 document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
255 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));261 document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
256 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));262 document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
263 document.querySelector('#bulk_tag_popup_import_all_tags').addEventListener('click', this.importAllTags.bind(this));
264 document.querySelector('#bulk_tag_popup_import_existing_tags').addEventListener('click', this.importExistingTags.bind(this));
265 }
266
267 /**
268 * Import existing tags for all selected characters
269 */
270 async importExistingTags() {
271 for (const characterId of this.characterIds) {
272 await importTags(characters[characterId], { importSetting: tag_import_setting.ONLY_EXISTING });
273 }
274
275 $('#bulkTagList').empty();
276 }
277
278 /**
279 * Import all tags for all selected characters
280 */
281 async importAllTags() {
282 for (const characterId of this.characterIds) {
283 await importTags(characters[characterId], { importSetting: tag_import_setting.ALL });
284 }
285
286 $('#bulkTagList').empty();
257 }287 }
258288
259 /**289 /**
@@ -570,7 +600,7 @@ class BulkEditOverlay {
570 this.container.removeEventListener('mouseup', cancelHold);600 this.container.removeEventListener('mouseup', cancelHold);
571 this.container.removeEventListener('touchend', cancelHold);601 this.container.removeEventListener('touchend', cancelHold);
572 },602 },
573 BulkEditOverlay.longPressDelay);603 BulkEditOverlay.longPressDelay);
574 };604 };
575605
576 handleLongPressEnd = (event) => {606 handleLongPressEnd = (event) => {
public/scripts/RossAscends-mods.js+6 -4
@@ -380,6 +380,7 @@ function RA_autoconnect(PrevApi) {
380 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)380 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
381 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)381 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
382 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)382 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
383 || (secret_state[SECRET_KEYS.BLOCKENTROPY] && oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY)
383 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)384 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
384 ) {385 ) {
385 $('#api_button_openai').trigger('click');386 $('#api_button_openai').trigger('click');
@@ -953,6 +954,11 @@ export function initRossMods() {
953 * @param {KeyboardEvent} event954 * @param {KeyboardEvent} event
954 */955 */
955 async function processHotkeys(event) {956 async function processHotkeys(event) {
957 // Default hotkeys and shortcuts shouldn't work if any popup is currently open
958 if (Popup.util.isPopupOpen()) {
959 return;
960 }
961
956 //Enter to send when send_textarea in focus962 //Enter to send when send_textarea in focus
957 if (document.activeElement == hotkeyTargets['send_textarea']) {963 if (document.activeElement == hotkeyTargets['send_textarea']) {
958 const sendOnEnter = shouldSendOnEnter();964 const sendOnEnter = shouldSendOnEnter();
@@ -1106,10 +1112,6 @@ export function initRossMods() {
1106 }1112 }
11071113
1108 if (event.key == 'Escape') { //closes various panels1114 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
1113 //dont override Escape hotkey functions from script.js1115 //dont override Escape hotkey functions from script.js
1114 //"close edit box" and "cancel stream generation".1116 //"close edit box" and "cancel stream generation".
1115 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {1117 if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {
public/scripts/extensions.js+56 -28
@@ -21,6 +21,7 @@ const defaultUrl = 'http://localhost:5100';
21let saveMetadataTimeout = null;21let saveMetadataTimeout = null;
2222
23let requiresReload = false;23let requiresReload = false;
24let stateChanged = false;
2425
25export function saveMetadataDebounced() {26export function saveMetadataDebounced() {
26 const context = getContext();27 const context = getContext();
@@ -238,6 +239,7 @@ function onEnableExtensionClick() {
238239
239async function enableExtension(name, reload = true) {240async function enableExtension(name, reload = true) {
240 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);241 extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
242 stateChanged = true;
241 await saveSettings();243 await saveSettings();
242 if (reload) {244 if (reload) {
243 location.reload();245 location.reload();
@@ -248,6 +250,7 @@ async function enableExtension(name, reload = true) {
248250
249async function disableExtension(name, reload = true) {251async function disableExtension(name, reload = true) {
250 extension_settings.disabledExtensions.push(name);252 extension_settings.disabledExtensions.push(name);
253 stateChanged = true;
251 await saveSettings();254 await saveSettings();
252 if (reload) {255 if (reload) {
253 location.reload();256 location.reload();
@@ -304,7 +307,7 @@ async function activateExtensions() {
304307
305 if (!isDisabled) {308 if (!isDisabled) {
306 const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]);309 const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]);
307 promise310 await promise
308 .then(() => activeExtensions.add(name))311 .then(() => activeExtensions.add(name))
309 .catch(err => console.log('Could not activate extension: ' + name, err));312 .catch(err => console.log('Could not activate extension: ' + name, err));
310 promises.push(promise);313 promises.push(promise);
@@ -657,7 +660,29 @@ async function showExtensionsDetails() {
657 await oldPopup.complete(POPUP_RESULT.CANCELLED);660 await oldPopup.complete(POPUP_RESULT.CANCELLED);
658 }661 }
659662
660 const popup = new Popup(html, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton], allowVerticalScrolling: true });663 let waitingForSave = false;
664
665 const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
666 okButton: 'Close',
667 wide: true,
668 large: true,
669 customButtons: [updateAllButton],
670 allowVerticalScrolling: true,
671 onClosing: async () => {
672 if (waitingForSave) {
673 return false;
674 }
675 if (stateChanged) {
676 waitingForSave = true;
677 const toast = toastr.info('The page will be reloaded shortly...', 'Extensions state changed');
678 await saveSettings();
679 toastr.clear(toast);
680 waitingForSave = false;
681 requiresReload = true;
682 }
683 return true;
684 },
685 });
661 popupPromise = popup.show();686 popupPromise = popup.show();
662 } catch (error) {687 } catch (error) {
663 toastr.error('Error loading extensions. See browser console for details.');688 toastr.error('Error loading extensions. See browser console for details.');
@@ -797,16 +822,17 @@ export async function installExtension(url) {
797 const response = await request.json();822 const response = await request.json();
798 toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful');823 toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful');
799 console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);824 console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
800 await loadExtensionSettings({}, false);825 await loadExtensionSettings({}, false, false);
801 eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);826 await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
802}827}
803828
804/**829/**
805 * Loads extension settings from the app settings.830 * Loads extension settings from the app settings.
806 * @param {object} settings App Settings831 * @param {object} settings App Settings
807 * @param {boolean} versionChanged Is this a version change?832 * @param {boolean} versionChanged Is this a version change?
833 * @param {boolean} enableAutoUpdate Enable auto-update
808 */834 */
809async function loadExtensionSettings(settings, versionChanged) {835async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) {
810 if (settings.extension_settings) {836 if (settings.extension_settings) {
811 Object.assign(extension_settings, settings.extension_settings);837 Object.assign(extension_settings, settings.extension_settings);
812 }838 }
@@ -817,11 +843,11 @@ async function loadExtensionSettings(settings, versionChanged) {
817 $('#extensions_notify_updates').prop('checked', extension_settings.notifyUpdates);843 $('#extensions_notify_updates').prop('checked', extension_settings.notifyUpdates);
818844
819 // Activate offline extensions845 // Activate offline extensions
820 eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD);846 await eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD);
821 extensionNames = await discoverExtensions();847 extensionNames = await discoverExtensions();
822 manifests = await getManifests(extensionNames);848 manifests = await getManifests(extensionNames);
823849
824 if (versionChanged) {850 if (versionChanged && enableAutoUpdate) {
825 await autoUpdateExtensions(false);851 await autoUpdateExtensions(false);
826 }852 }
827853
@@ -989,6 +1015,28 @@ export async function writeExtensionField(characterId, key, value) {
989 }1015 }
990}1016}
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 */
1027export 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
992jQuery(async function () {1040jQuery(async function () {
993 await addExtensionsButtonAndMenu();1041 await addExtensionsButtonAndMenu();
994 $('#extensionsMenuButton').css('display', 'flex');1042 $('#extensionsMenuButton').css('display', 'flex');
@@ -1004,28 +1052,8 @@ jQuery(async function () {
10041052
1005 /**1053 /**
1006 * Handles the click event for the third-party extension import button.1054 * Handles the click event for the third-party extension import button.
1007 * Prompts the user to enter the Git URL of the extension to import.
1008 * After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
1009 * If the extension is imported successfully, a success message is displayed.
1010 * If the extension import fails, an error message is displayed and the error is logged to the console.
1011 * After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
1012 *1055 *
1013 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.1056 * @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
1014 */1057 */
1015 $('#third_party_extension_button').on('click', async () => {1058 $('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
1016 const html = `<h3>Enter the Git URL of the extension to install</h3>
1017 <br>
1018 <p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
1019 <br>
1020 <p>Example: <tt> https://github.com/author/extension-name </tt></p>`;
1021 const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '');
1022
1023 if (!input) {
1024 console.debug('Extension install cancelled');
1025 return;
1026 }
1027
1028 const url = String(input).trim();
1029 await installExtension(url);
1030 });
1031});1059});
public/scripts/extensions/caption/index.js+4 -0
@@ -169,7 +169,11 @@ async function sendCaptionedMessage(caption, image) {
169 },169 },
170 };170 };
171 context.chat.push(message);171 context.chat.push(message);
172 const messageId = context.chat.length - 1;
173 await eventSource.emit(event_types.MESSAGE_SENT, messageId);
172 context.addOneMessage(message);174 context.addOneMessage(message);
175 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
176 await context.saveChat();
173}177}
174178
175/**179/**
public/scripts/extensions/caption/settings.html+2 -1
@@ -20,7 +20,7 @@
20 <option value="zerooneai">01.AI (Yi)</option>20 <option value="zerooneai">01.AI (Yi)</option>
21 <option value="anthropic">Anthropic</option>21 <option value="anthropic">Anthropic</option>
22 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>22 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
23 <option value="google">Google MakerSuite</option>23 <option value="google">Google AI Studio</option>
24 <option value="koboldcpp">KoboldCpp</option>24 <option value="koboldcpp">KoboldCpp</option>
25 <option value="llamacpp">llama.cpp</option>25 <option value="llamacpp">llama.cpp</option>
26 <option value="ollama">Ollama</option>26 <option value="ollama">Ollama</option>
@@ -38,6 +38,7 @@
38 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>38 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
39 <option data-type="openai" value="gpt-4o">gpt-4o</option>39 <option data-type="openai" value="gpt-4o">gpt-4o</option>
40 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>40 <option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>
41 <option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
41 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>42 <option data-type="anthropic" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
42 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>43 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
43 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>44 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
public/scripts/extensions/memory/index.js+160 -31
@@ -1,4 +1,4 @@
1import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js';1import { getStringHash, debounce, waitUntilCondition, extractAllWords, isTrueBoolean } from '../../utils.js';
2import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';2import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
3import {3import {
4 activateSendButtons,4 activateSendButtons,
@@ -25,6 +25,8 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
25import { SlashCommand } from '../../slash-commands/SlashCommand.js';25import { SlashCommand } from '../../slash-commands/SlashCommand.js';
26import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';26import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
27import { MacrosParser } from '../../macros.js';27import { MacrosParser } from '../../macros.js';
28import { countWebLlmTokens, generateWebLlmChatPrompt, getWebLlmContextSize, isWebLlmSupported } from '../shared.js';
29import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
28export { MODULE_NAME };30export { MODULE_NAME };
2931
30const MODULE_NAME = '1_memory';32const MODULE_NAME = '1_memory';
@@ -36,6 +38,41 @@ let lastMessageHash = null;
36let lastMessageId = null;38let lastMessageId = null;
37let inApiCall = false;39let inApiCall = false;
3840
41/**
42 * Count the number of tokens in the provided text.
43 * @param {string} text Text to count tokens for
44 * @param {number} padding Number of additional tokens to add to the count
45 * @returns {Promise<number>} Number of tokens in the text
46 */
47async function countSourceTokens(text, padding = 0) {
48 if (extension_settings.memory.source === summary_sources.webllm) {
49 const count = await countWebLlmTokens(text);
50 return count + padding;
51 }
52
53 if (extension_settings.memory.source === summary_sources.extras) {
54 const count = getTextTokens(tokenizers.GPT2, text).length;
55 return count + padding;
56 }
57
58 return await getTokenCountAsync(text, padding);
59}
60
61async function getSourceContextSize() {
62 const overrideLength = extension_settings.memory.overrideResponseLength;
63
64 if (extension_settings.memory.source === summary_sources.webllm) {
65 const maxContext = await getWebLlmContextSize();
66 return overrideLength > 0 ? (maxContext - overrideLength) : Math.round(maxContext * 0.75);
67 }
68
69 if (extension_settings.source === summary_sources.extras) {
70 return 1024 - 64;
71 }
72
73 return getMaxContextSize(overrideLength);
74}
75
39const formatMemoryValue = function (value) {76const formatMemoryValue = function (value) {
40 if (!value) {77 if (!value) {
41 return '';78 return '';
@@ -55,6 +92,7 @@ const saveChatDebounced = debounce(() => getContext().saveChat(), debounce_timeo
55const summary_sources = {92const summary_sources = {
56 'extras': 'extras',93 'extras': 'extras',
57 'main': 'main',94 'main': 'main',
95 'webllm': 'webllm',
58};96};
5997
60const prompt_builders = {98const prompt_builders = {
@@ -130,12 +168,12 @@ function loadSettings() {
130168
131async function onPromptForceWordsAutoClick() {169async function onPromptForceWordsAutoClick() {
132 const context = getContext();170 const context = getContext();
133 const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength);171 const maxPromptLength = await getSourceContextSize();
134 const chat = context.chat;172 const chat = context.chat;
135 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);173 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
136 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;174 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
137 const averageMessageWordCount = messagesWordCount / allMessages.length;175 const averageMessageWordCount = messagesWordCount / allMessages.length;
138 const tokensPerWord = await getTokenCountAsync(allMessages.join('\n')) / messagesWordCount;176 const tokensPerWord = await countSourceTokens(allMessages.join('\n')) / messagesWordCount;
139 const wordsPerToken = 1 / tokensPerWord;177 const wordsPerToken = 1 / tokensPerWord;
140 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);178 const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);
141 // How many words should pass so that messages will start be dropped out of context;179 // How many words should pass so that messages will start be dropped out of context;
@@ -168,15 +206,15 @@ async function onPromptForceWordsAutoClick() {
168206
169async function onPromptIntervalAutoClick() {207async function onPromptIntervalAutoClick() {
170 const context = getContext();208 const context = getContext();
171 const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength);209 const maxPromptLength = await getSourceContextSize();
172 const chat = context.chat;210 const chat = context.chat;
173 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);211 const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
174 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;212 const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
175 const messagesTokenCount = await getTokenCountAsync(allMessages.join('\n'));213 const messagesTokenCount = await countSourceTokens(allMessages.join('\n'));
176 const tokensPerWord = messagesTokenCount / messagesWordCount;214 const tokensPerWord = messagesTokenCount / messagesWordCount;
177 const averageMessageTokenCount = messagesTokenCount / allMessages.length;215 const averageMessageTokenCount = messagesTokenCount / allMessages.length;
178 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);216 const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);
179 const promptTokens = await getTokenCountAsync(extension_settings.memory.prompt);217 const promptTokens = await countSourceTokens(extension_settings.memory.prompt);
180 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;218 const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;
181 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;219 const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
182 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);220 const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);
@@ -213,8 +251,8 @@ function onSummarySourceChange(event) {
213251
214function switchSourceControls(value) {252function switchSourceControls(value) {
215 $('#memory_settings [data-summary-source]').each((_, element) => {253 $('#memory_settings [data-summary-source]').each((_, element) => {
216 const source = $(element).data('summary-source');254 const source = element.dataset.summarySource.split(',').map(s => s.trim());
217 $(element).toggle(source === value);255 $(element).toggle(source.includes(value));
218 });256 });
219}257}
220258
@@ -353,10 +391,13 @@ function getIndexOfLatestChatSummary(chat) {
353391
354async function onChatEvent() {392async function onChatEvent() {
355 // Module not enabled393 // Module not enabled
356 if (extension_settings.memory.source === summary_sources.extras) {394 if (extension_settings.memory.source === summary_sources.extras && !modules.includes('summarize')) {
357 if (!modules.includes('summarize')) {395 return;
358 return;396 }
359 }397
398 // WebLLM is not supported
399 if (extension_settings.memory.source === summary_sources.webllm && !isWebLlmSupported()) {
400 return;
360 }401 }
361402
362 const context = getContext();403 const context = getContext();
@@ -416,7 +457,12 @@ async function onChatEvent() {
416 }457 }
417}458}
418459
419async function forceSummarizeChat() {460/**
461 * Forces a summary generation for the current chat.
462 * @param {boolean} quiet If an informational toast should be displayed
463 * @returns {Promise<string>} Summarized text
464 */
465async function forceSummarizeChat(quiet) {
420 if (extension_settings.memory.source === summary_sources.extras) {466 if (extension_settings.memory.source === summary_sources.extras) {
421 toastr.warning('Force summarization is not supported for Extras API');467 toastr.warning('Force summarization is not supported for Extras API');
422 return;468 return;
@@ -431,8 +477,12 @@ async function forceSummarizeChat() {
431 return '';477 return '';
432 }478 }
433479
434 toastr.info('Summarizing chat...', 'Please wait');480 const toast = quiet ? jQuery() : toastr.info('Summarizing chat...', 'Please wait', { timeOut: 0, extendedTimeOut: 0 });
435 const value = await summarizeChatMain(context, true, skipWIAN);481 const value = extension_settings.memory.source === summary_sources.main
482 ? await summarizeChatMain(context, true, skipWIAN)
483 : await summarizeChatWebLLM(context, true);
484
485 toastr.clear(toast);
436486
437 if (!value) {487 if (!value) {
438 toastr.warning('Failed to summarize chat');488 toastr.warning('Failed to summarize chat');
@@ -450,9 +500,10 @@ async function forceSummarizeChat() {
450async function summarizeCallback(args, text) {500async function summarizeCallback(args, text) {
451 text = text.trim();501 text = text.trim();
452502
453 // Using forceSummarizeChat to summarize the current chat503 // Summarize the current chat if no text provided
454 if (!text) {504 if (!text) {
455 return await forceSummarizeChat();505 const quiet = isTrueBoolean(args.quiet);
506 return await forceSummarizeChat(quiet);
456 }507 }
457508
458 const source = args.source || extension_settings.memory.source;509 const source = args.source || extension_settings.memory.source;
@@ -464,6 +515,11 @@ async function summarizeCallback(args, text) {
464 return await callExtrasSummarizeAPI(text);515 return await callExtrasSummarizeAPI(text);
465 case summary_sources.main:516 case summary_sources.main:
466 return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);517 return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
518 case summary_sources.webllm: {
519 const messages = [{ role: 'system', content: prompt }, { role: 'user', content: text }].filter(m => m.content);
520 const params = extension_settings.memory.overrideResponseLength > 0 ? { max_tokens: extension_settings.memory.overrideResponseLength } : {};
521 return await generateWebLlmChatPrompt(messages, params);
522 }
467 default:523 default:
468 toastr.warning('Invalid summarization source specified');524 toastr.warning('Invalid summarization source specified');
469 return '';525 return '';
@@ -484,16 +540,25 @@ async function summarizeChat(context) {
484 case summary_sources.main:540 case summary_sources.main:
485 await summarizeChatMain(context, false, skipWIAN);541 await summarizeChatMain(context, false, skipWIAN);
486 break;542 break;
543 case summary_sources.webllm:
544 await summarizeChatWebLLM(context, false);
545 break;
487 default:546 default:
488 break;547 break;
489 }548 }
490}549}
491550
492async function summarizeChatMain(context, force, skipWIAN) {551/**
493552 * Check if the chat should be summarized based on the current conditions.
553 * Return summary prompt if it should be summarized.
554 * @param {any} context ST context
555 * @param {boolean} force Summarize the chat regardless of the conditions
556 * @returns {Promise<string>} Summary prompt or empty string
557 */
558async function getSummaryPromptForNow(context, force) {
494 if (extension_settings.memory.promptInterval === 0 && !force) {559 if (extension_settings.memory.promptInterval === 0 && !force) {
495 console.debug('Prompt interval is set to 0, skipping summarization');560 console.debug('Prompt interval is set to 0, skipping summarization');
496 return;561 return '';
497 }562 }
498563
499 try {564 try {
@@ -505,17 +570,17 @@ async function summarizeChatMain(context, force, skipWIAN) {
505 waitUntilCondition(() => is_send_press === false, 30000, 100);570 waitUntilCondition(() => is_send_press === false, 30000, 100);
506 } catch {571 } catch {
507 console.debug('Timeout waiting for is_send_press');572 console.debug('Timeout waiting for is_send_press');
508 return;573 return '';
509 }574 }
510575
511 if (!context.chat.length) {576 if (!context.chat.length) {
512 console.debug('No messages in chat to summarize');577 console.debug('No messages in chat to summarize');
513 return;578 return '';
514 }579 }
515580
516 if (context.chat.length < extension_settings.memory.promptInterval && !force) {581 if (context.chat.length < extension_settings.memory.promptInterval && !force) {
517 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);582 console.debug(`Not enough messages in chat to summarize (chat: ${context.chat.length}, interval: ${extension_settings.memory.promptInterval})`);
518 return;583 return '';
519 }584 }
520585
521 let messagesSinceLastSummary = 0;586 let messagesSinceLastSummary = 0;
@@ -539,7 +604,7 @@ async function summarizeChatMain(context, force, skipWIAN) {
539604
540 if (!conditionSatisfied && !force) {605 if (!conditionSatisfied && !force) {
541 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);606 console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);
542 return;607 return '';
543 }608 }
544609
545 console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);610 console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);
@@ -547,6 +612,63 @@ async function summarizeChatMain(context, force, skipWIAN) {
547612
548 if (!prompt) {613 if (!prompt) {
549 console.debug('Summarization prompt is empty. Skipping summarization.');614 console.debug('Summarization prompt is empty. Skipping summarization.');
615 return '';
616 }
617
618 return prompt;
619}
620
621async function summarizeChatWebLLM(context, force) {
622 if (!isWebLlmSupported()) {
623 return;
624 }
625
626 const prompt = await getSummaryPromptForNow(context, force);
627
628 if (!prompt) {
629 return;
630 }
631
632 const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
633
634 if (lastUsedIndex === null || lastUsedIndex === -1) {
635 if (force) {
636 toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');
637 }
638
639 return null;
640 }
641
642 const messages = [
643 { role: 'system', content: prompt },
644 { role: 'user', content: rawPrompt },
645 ];
646
647 const params = {};
648
649 if (extension_settings.memory.overrideResponseLength > 0) {
650 params.max_tokens = extension_settings.memory.overrideResponseLength;
651 }
652
653 const summary = await generateWebLlmChatPrompt(messages, params);
654 const newContext = getContext();
655
656 // something changed during summarization request
657 if (newContext.groupId !== context.groupId ||
658 newContext.chatId !== context.chatId ||
659 (!newContext.groupId && (newContext.characterId !== context.characterId))) {
660 console.log('Context changed, summary discarded');
661 return;
662 }
663
664 setMemoryContext(summary, true, lastUsedIndex);
665 return summary;
666}
667
668async function summarizeChatMain(context, force, skipWIAN) {
669 const prompt = await getSummaryPromptForNow(context, force);
670
671 if (!prompt) {
550 return;672 return;
551 }673 }
552674
@@ -634,7 +756,7 @@ async function getRawSummaryPrompt(context, prompt) {
634 chat.pop(); // We always exclude the last message from the buffer756 chat.pop(); // We always exclude the last message from the buffer
635 const chatBuffer = [];757 const chatBuffer = [];
636 const PADDING = 64;758 const PADDING = 64;
637 const PROMPT_SIZE = getMaxContextSize(extension_settings.memory.overrideResponseLength);759 const PROMPT_SIZE = await getSourceContextSize();
638 let latestUsedMessage = null;760 let latestUsedMessage = null;
639761
640 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {762 for (let index = latestSummaryIndex + 1; index < chat.length; index++) {
@@ -651,7 +773,7 @@ async function getRawSummaryPrompt(context, prompt) {
651 const entry = `${message.name}:\n${message.mes}`;773 const entry = `${message.name}:\n${message.mes}`;
652 chatBuffer.push(entry);774 chatBuffer.push(entry);
653775
654 const tokens = await getTokenCountAsync(getMemoryString(true), PADDING);776 const tokens = await countSourceTokens(getMemoryString(true), PADDING);
655777
656 if (tokens > PROMPT_SIZE) {778 if (tokens > PROMPT_SIZE) {
657 chatBuffer.pop();779 chatBuffer.pop();
@@ -680,7 +802,7 @@ async function summarizeChatExtras(context) {
680 const reversedChat = chat.slice().reverse();802 const reversedChat = chat.slice().reverse();
681 reversedChat.shift();803 reversedChat.shift();
682 const memoryBuffer = [];804 const memoryBuffer = [];
683 const CONTEXT_SIZE = 1024 - 64;805 const CONTEXT_SIZE = await getSourceContextSize();
684806
685 for (const message of reversedChat) {807 for (const message of reversedChat) {
686 // we reached the point of latest memory808 // we reached the point of latest memory
@@ -698,14 +820,14 @@ async function summarizeChatExtras(context) {
698 memoryBuffer.push(entry);820 memoryBuffer.push(entry);
699821
700 // check if token limit was reached822 // check if token limit was reached
701 const tokens = getTextTokens(tokenizers.GPT2, getMemoryString()).length;823 const tokens = await countSourceTokens(getMemoryString());
702 if (tokens >= CONTEXT_SIZE) {824 if (tokens >= CONTEXT_SIZE) {
703 break;825 break;
704 }826 }
705 }827 }
706828
707 const resultingString = getMemoryString();829 const resultingString = getMemoryString();
708 const resultingTokens = getTextTokens(tokenizers.GPT2, resultingString).length;830 const resultingTokens = await countSourceTokens(resultingString);
709831
710 if (!resultingString || resultingTokens < CONTEXT_SIZE) {832 if (!resultingString || resultingTokens < CONTEXT_SIZE) {
711 console.debug('Not enough context to summarize');833 console.debug('Not enough context to summarize');
@@ -890,7 +1012,7 @@ function setupListeners() {
890 $('#memory_prompt_words').off('click').on('input', onMemoryPromptWordsInput);1012 $('#memory_prompt_words').off('click').on('input', onMemoryPromptWordsInput);
891 $('#memory_prompt_interval').off('click').on('input', onMemoryPromptIntervalInput);1013 $('#memory_prompt_interval').off('click').on('input', onMemoryPromptIntervalInput);
892 $('#memory_prompt').off('click').on('input', onMemoryPromptInput);1014 $('#memory_prompt').off('click').on('input', onMemoryPromptInput);
893 $('#memory_force_summarize').off('click').on('click', forceSummarizeChat);1015 $('#memory_force_summarize').off('click').on('click', () => forceSummarizeChat(false));
894 $('#memory_template').off('click').on('input', onMemoryTemplateInput);1016 $('#memory_template').off('click').on('input', onMemoryTemplateInput);
895 $('#memory_depth').off('click').on('input', onMemoryDepthInput);1017 $('#memory_depth').off('click').on('input', onMemoryDepthInput);
896 $('#memory_role').off('click').on('input', onMemoryRoleInput);1018 $('#memory_role').off('click').on('input', onMemoryRoleInput);
@@ -933,13 +1055,20 @@ jQuery(async function () {
933 name: 'summarize',1055 name: 'summarize',
934 callback: summarizeCallback,1056 callback: summarizeCallback,
935 namedArgumentList: [1057 namedArgumentList: [
936 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']),1058 new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', Object.values(summary_sources)),
937 SlashCommandNamedArgument.fromProps({1059 SlashCommandNamedArgument.fromProps({
938 name: 'prompt',1060 name: 'prompt',
939 description: 'prompt to use for summarization',1061 description: 'prompt to use for summarization',
940 typeList: [ARGUMENT_TYPE.STRING],1062 typeList: [ARGUMENT_TYPE.STRING],
941 defaultValue: '',1063 defaultValue: '',
942 }),1064 }),
1065 SlashCommandNamedArgument.fromProps({
1066 name: 'quiet',
1067 description: 'suppress the toast message when summarizing the chat',
1068 typeList: [ARGUMENT_TYPE.BOOLEAN],
1069 defaultValue: 'false',
1070 enumList: commonEnumProviders.boolean('trueFalse')(),
1071 }),
943 ],1072 ],
944 unnamedArgumentList: [1073 unnamedArgumentList: [
945 new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),1074 new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
public/scripts/extensions/memory/settings.html+4 -3
@@ -13,6 +13,7 @@
13 <select id="summary_source">13 <select id="summary_source">
14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>14 <option value="main" data-i18n="ext_sum_main_api">Main API</option>
15 <option value="extras">Extras API</option>15 <option value="extras">Extras API</option>
16 <option value="webllm" data-i18n="ext_sum_webllm">WebLLM Extension</option>
16 </select><br>17 </select><br>
1718
18 <div class="flex-container justifyspacebetween alignitemscenter">19 <div class="flex-container justifyspacebetween alignitemscenter">
@@ -24,7 +25,7 @@
2425
25 <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>26 <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" data-i18n="[placeholder]ext_sum_memory_placeholder" placeholder="Summary will be generated here..."></textarea>
26 <div class="memory_contents_controls">27 <div class="memory_contents_controls">
27 <div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="[title]ext_sum_force_tip">28 <div id="memory_force_summarize" data-summary-source="main,webllm" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="[title]ext_sum_force_tip">
28 <i class="fa-solid fa-database"></i>29 <i class="fa-solid fa-database"></i>
29 <span data-i18n="ext_sum_force_text">Summarize now</span>30 <span data-i18n="ext_sum_force_text">Summarize now</span>
30 </div>31 </div>
@@ -58,7 +59,7 @@
58 <span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>59 <span data-i18n="ext_sum_prompt_builder_3">Classic, blocking</span>
59 </label>60 </label>
60 </div>61 </div>
61 <div data-summary-source="main">62 <div data-summary-source="main,webllm">
62 <label for="memory_prompt" class="title_restorable">63 <label for="memory_prompt" class="title_restorable">
63 <span data-i18n="Summary Prompt">Summary Prompt</span>64 <span data-i18n="Summary Prompt">Summary Prompt</span>
64 <div id="memory_prompt_restore" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">65 <div id="memory_prompt_restore" data-i18n="[title]ext_sum_restore_default_prompt_tip" title="Restore default prompt" class="right_menu_button">
@@ -74,7 +75,7 @@
74 </label>75 </label>
75 <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />76 <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />
76 <label for="memory_max_messages_per_request">77 <label for="memory_max_messages_per_request">
77 <span data-i18n="ext_sum_raw_max_msg">[Raw] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)78 <span data-i18n="ext_sum_raw_max_msg">[Raw/WebLLM] Max messages per request</span> (<span id="memory_max_messages_per_request_value"></span>)
78 <small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>79 <small class="memory_disabled_hint" data-i18n="ext_sum_0_unlimited">0 = unlimited</small>
79 </label>80 </label>
80 <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />81 <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />
public/scripts/extensions/quick-reply/api/QuickReplyApi.js+6 -0
@@ -204,6 +204,7 @@ export class QuickReplyApi {
204 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message204 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
205 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded205 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
206 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected206 * @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
207 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated208 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
208 * @returns {QuickReply} the new quick reply209 * @returns {QuickReply} the new quick reply
209 */210 */
@@ -218,6 +219,7 @@ export class QuickReplyApi {
218 executeOnAi,219 executeOnAi,
219 executeOnChatChange,220 executeOnChatChange,
220 executeOnGroupMemberDraft,221 executeOnGroupMemberDraft,
222 executeOnNewChat,
221 automationId,223 automationId,
222 } = {}) {224 } = {}) {
223 const set = this.getSetByName(setName);225 const set = this.getSetByName(setName);
@@ -236,6 +238,7 @@ export class QuickReplyApi {
236 qr.executeOnAi = executeOnAi ?? false;238 qr.executeOnAi = executeOnAi ?? false;
237 qr.executeOnChatChange = executeOnChatChange ?? false;239 qr.executeOnChatChange = executeOnChatChange ?? false;
238 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;240 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
241 qr.executeOnNewChat = executeOnNewChat ?? false;
239 qr.automationId = automationId ?? '';242 qr.automationId = automationId ?? '';
240 qr.onUpdate();243 qr.onUpdate();
241 return qr;244 return qr;
@@ -258,6 +261,7 @@ export class QuickReplyApi {
258 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message261 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
259 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded262 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
260 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected263 * @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
261 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated265 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
262 * @returns {QuickReply} the altered quick reply266 * @returns {QuickReply} the altered quick reply
263 */267 */
@@ -273,6 +277,7 @@ export class QuickReplyApi {
273 executeOnAi,277 executeOnAi,
274 executeOnChatChange,278 executeOnChatChange,
275 executeOnGroupMemberDraft,279 executeOnGroupMemberDraft,
280 executeOnNewChat,
276 automationId,281 automationId,
277 } = {}) {282 } = {}) {
278 const qr = this.getQrByLabel(setName, label);283 const qr = this.getQrByLabel(setName, label);
@@ -290,6 +295,7 @@ export class QuickReplyApi {
290 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;295 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
291 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;296 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
292 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;297 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
298 qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
293 qr.automationId = automationId ?? qr.automationId;299 qr.automationId = automationId ?? qr.automationId;
294 qr.onUpdate();300 qr.onUpdate();
295 return qr;301 return qr;
public/scripts/extensions/quick-reply/html/qrEditor.html+4 -0
@@ -109,6 +109,10 @@
109 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>109 <span><i class="fa-solid fa-fw fa-message"></i><span data-i18n="Execute on chat change">Execute on chat change</span></span>
110 </label>110 </label>
111 <label class="checkbox_label">111 <label class="checkbox_label">
112 <input type="checkbox" id="qr--executeOnNewChat">
113 <span><i class="fa-solid fa-fw fa-comments"></i><span data-i18n="Execute on new chat">Execute on new chat</span></span>
114 </label>
115 <label class="checkbox_label">
112 <input type="checkbox" id="qr--executeOnGroupMemberDraft">116 <input type="checkbox" id="qr--executeOnGroupMemberDraft">
113 <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>117 <span><i class="fa-solid fa-fw fa-people-group"></i><span data-i18n="Execute on group member draft">Execute on group member draft</span></span>
114 </label>118 </label>
public/scripts/extensions/quick-reply/html/settings.html+3 -0
@@ -11,6 +11,9 @@
11 <label class="flex-container">11 <label class="flex-container">
12 <input type="checkbox" id="qr--isCombined"><span data-i18n="Combine Quick Replies">Combine Quick Replies</span>12 <input type="checkbox" id="qr--isCombined"><span data-i18n="Combine Quick Replies">Combine Quick Replies</span>
13 </label>13 </label>
14 <label class="flex-container">
15 <input type="checkbox" id="qr--showPopoutButton"><span data-i18n="Show Popout Button">Show Popout Button</span>
16 </label>
1417
15 <hr>18 <hr>
1619
public/scripts/extensions/quick-reply/index.js+6 -0
@@ -105,6 +105,7 @@ const loadSets = async () => {
105 qr.executeOnAi = slot.autoExecute_botMessage ?? false;105 qr.executeOnAi = slot.autoExecute_botMessage ?? false;
106 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;106 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
107 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;107 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
108 qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
108 qr.automationId = slot.automationId ?? '';109 qr.automationId = slot.automationId ?? '';
109 qr.contextList = (slot.contextMenu ?? []).map(it=>({110 qr.contextList = (slot.contextMenu ?? []).map(it=>({
110 set: it.preset,111 set: it.preset,
@@ -260,3 +261,8 @@ const onWIActivation = async (entries) => {
260 await autoExec.handleWIActivation(entries);261 await autoExec.handleWIActivation(entries);
261};262};
262eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));263eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));
264
265const onNewChat = async () => {
266 await autoExec.handleNewChat();
267};
268eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+9 -0
@@ -83,6 +83,15 @@ export class AutoExecuteHandler {
83 await this.performAutoExecute(qrList);83 await this.performAutoExecute(qrList);
84 }84 }
8585
86 async handleNewChat() {
87 if (!this.checkExecute()) return;
88 const qrList = [
89 ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat)).flat(),
90 ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat))?.flat() ?? []),
91 ];
92 await this.performAutoExecute(qrList);
93 }
94
86 /**95 /**
87 * @param {any[]} entries Set of activated entries96 * @param {any[]} entries Set of activated entries
88 */97 */
public/scripts/extensions/quick-reply/src/QuickReply.js+8 -0
@@ -44,6 +44,7 @@ export class QuickReply {
44 /**@type {boolean}*/ executeOnAi = false;44 /**@type {boolean}*/ executeOnAi = false;
45 /**@type {boolean}*/ executeOnChatChange = false;45 /**@type {boolean}*/ executeOnChatChange = false;
46 /**@type {boolean}*/ executeOnGroupMemberDraft = false;46 /**@type {boolean}*/ executeOnGroupMemberDraft = false;
47 /**@type {boolean}*/ executeOnNewChat = false;
47 /**@type {string}*/ automationId = '';48 /**@type {string}*/ automationId = '';
4849
49 /**@type {function}*/ onExecute;50 /**@type {function}*/ onExecute;
@@ -1061,6 +1062,13 @@ export class QuickReply {
1061 this.updateContext();1062 this.updateContext();
1062 });1063 });
1063 /**@type {HTMLInputElement}*/1064 /**@type {HTMLInputElement}*/
1065 const executeOnNewChat = dom.querySelector('#qr--executeOnNewChat');
1066 executeOnNewChat.checked = this.executeOnNewChat;
1067 executeOnNewChat.addEventListener('click', ()=>{
1068 this.executeOnNewChat = executeOnNewChat.checked;
1069 this.updateContext();
1070 });
1071 /**@type {HTMLInputElement}*/
1064 const automationId = dom.querySelector('#qr--automationId');1072 const automationId = dom.querySelector('#qr--automationId');
1065 automationId.value = this.automationId;1073 automationId.value = this.automationId;
1066 automationId.addEventListener('input', () => {1074 automationId.addEventListener('input', () => {
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+2 -0
@@ -16,6 +16,7 @@ export class QuickReplySettings {
16 /**@type {Boolean}*/ isEnabled = false;16 /**@type {Boolean}*/ isEnabled = false;
17 /**@type {Boolean}*/ isCombined = false;17 /**@type {Boolean}*/ isCombined = false;
18 /**@type {Boolean}*/ isPopout = false;18 /**@type {Boolean}*/ isPopout = false;
19 /**@type {Boolean}*/ showPopoutButton = true;
19 /**@type {QuickReplyConfig}*/ config;20 /**@type {QuickReplyConfig}*/ config;
20 /**@type {QuickReplyConfig}*/ _chatConfig;21 /**@type {QuickReplyConfig}*/ _chatConfig;
21 get chatConfig() {22 get chatConfig() {
@@ -79,6 +80,7 @@ export class QuickReplySettings {
79 isEnabled: this.isEnabled,80 isEnabled: this.isEnabled,
80 isCombined: this.isCombined,81 isCombined: this.isCombined,
81 isPopout: this.isPopout,82 isPopout: this.isPopout,
83 showPopoutButton: this.showPopoutButton,
82 config: this.config,84 config: this.config,
83 };85 };
84 }86 }
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js+14 -11
@@ -69,17 +69,20 @@ export class ButtonUi {
69 root.id = 'qr--bar';69 root.id = 'qr--bar';
70 root.classList.add('flex-container');70 root.classList.add('flex-container');
71 root.classList.add('flexGap5');71 root.classList.add('flexGap5');
72 const popout = document.createElement('div'); {72 if (this.settings.showPopoutButton) {
73 popout.id = 'qr--popoutTrigger';73 root.classList.add('popoutVisible');
74 popout.classList.add('menu_button');74 const popout = document.createElement('div'); {
75 popout.classList.add('fa-solid');75 popout.id = 'qr--popoutTrigger';
76 popout.classList.add('fa-window-restore');76 popout.classList.add('menu_button');
77 popout.addEventListener('click', ()=>{77 popout.classList.add('fa-solid');
78 this.settings.isPopout = true;78 popout.classList.add('fa-window-restore');
79 this.refresh();79 popout.addEventListener('click', ()=>{
80 this.settings.save();80 this.settings.isPopout = true;
81 });81 this.refresh();
82 root.append(popout);82 this.settings.save();
83 });
84 root.append(popout);
85 }
83 }86 }
84 if (this.settings.isCombined) {87 if (this.settings.isCombined) {
85 const buttons = document.createElement('div'); {88 const buttons = document.createElement('div'); {
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+10 -0
@@ -14,6 +14,7 @@ export class SettingsUi {
1414
15 /**@type {HTMLInputElement}*/ isEnabled;15 /**@type {HTMLInputElement}*/ isEnabled;
16 /**@type {HTMLInputElement}*/ isCombined;16 /**@type {HTMLInputElement}*/ isCombined;
17 /**@type {HTMLInputElement}*/ showPopoutButton;
1718
18 /**@type {HTMLElement}*/ globalSetList;19 /**@type {HTMLElement}*/ globalSetList;
1920
@@ -79,6 +80,10 @@ export class SettingsUi {
79 this.isCombined = this.dom.querySelector('#qr--isCombined');80 this.isCombined = this.dom.querySelector('#qr--isCombined');
80 this.isCombined.checked = this.settings.isCombined;81 this.isCombined.checked = this.settings.isCombined;
81 this.isCombined.addEventListener('click', ()=>this.onIsCombined());82 this.isCombined.addEventListener('click', ()=>this.onIsCombined());
83
84 this.showPopoutButton = this.dom.querySelector('#qr--showPopoutButton');
85 this.showPopoutButton.checked = this.settings.showPopoutButton;
86 this.showPopoutButton.addEventListener('click', ()=>this.onShowPopoutButton());
82 }87 }
8388
84 prepareGlobalSetList() {89 prepareGlobalSetList() {
@@ -235,6 +240,11 @@ export class SettingsUi {
235 this.settings.save();240 this.settings.save();
236 }241 }
237242
243 async onShowPopoutButton() {
244 this.settings.showPopoutButton = this.showPopoutButton.checked;
245 this.settings.save();
246 }
247
238 async onGlobalSetListSort() {248 async onGlobalSetListSort() {
239 this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{249 this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{
240 const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];250 const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
public/scripts/extensions/quick-reply/style.css+3 -1
@@ -27,7 +27,6 @@
27 max-width: 100%;27 max-width: 100%;
28 overflow-x: auto;28 overflow-x: auto;
29 order: 1;29 order: 1;
30 padding-right: 2.5em;
31 position: relative;30 position: relative;
32}31}
33#qr--bar > #qr--popoutTrigger {32#qr--bar > #qr--popoutTrigger {
@@ -35,6 +34,9 @@
35 right: 0.25em;34 right: 0.25em;
36 top: 0;35 top: 0;
37}36}
37#qr--bar.popoutVisible {
38 padding-right: 2.5em;
39}
38#qr--popout {40#qr--popout {
39 display: flex;41 display: flex;
40 flex-direction: column;42 flex-direction: column;
public/scripts/extensions/quick-reply/style.less+3 -1
@@ -25,7 +25,6 @@
25 max-width: 100%;25 max-width: 100%;
26 overflow-x: auto;26 overflow-x: auto;
27 order: 1;27 order: 1;
28 padding-right: 2.5em;
29 position: relative;28 position: relative;
3029
31 >#qr--popoutTrigger {30 >#qr--popoutTrigger {
@@ -34,6 +33,9 @@
34 top: 0;33 top: 0;
35 }34 }
36}35}
36#qr--bar.popoutVisible {
37 padding-right: 2.5em;
38}
3739
38#qr--popout {40#qr--popout {
39 display: flex;41 display: flex;
public/scripts/extensions/regex/editor.html+15 -22
@@ -54,12 +54,7 @@
54 <small data-i18n="Replace With">Replace With</small>54 <small data-i18n="Replace With">Replace With</small>
55 </label>55 </label>
56 <div>56 <div>
57 <textarea57 <textarea class="regex_replace_string text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_replace_string_placeholder" placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups." rows="2"></textarea>
58 class="regex_replace_string text_pole wide100p textarea_compact"
59 data-i18n="[placeholder]ext_regex_replace_string_placeholder"
60 placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
61 rows="2"
62 ></textarea>
63 </div>58 </div>
64 </div>59 </div>
65 <div class="flex1">60 <div class="flex1">
@@ -67,11 +62,7 @@
67 <small data-i18n="Trim Out">Trim Out</small>62 <small data-i18n="Trim Out">Trim Out</small>
68 </label>63 </label>
69 <div>64 <div>
70 <textarea65 <textarea class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder" placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter." rows="3"></textarea>
71 class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder"
72 placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter."
73 rows="3"
74 ></textarea>
75 </div>66 </div>
76 </div>67 </div>
77 </div>68 </div>
@@ -126,17 +117,6 @@
126 <input type="checkbox" name="disabled" />117 <input type="checkbox" name="disabled" />
127 <span data-i18n="Disabled">Disabled</span>118 <span data-i18n="Disabled">Disabled</span>
128 </label>119 </label>
129 <label class="checkbox flex-container" title="Chat history won't change, only the message rendered in the UI.">
130 <input type="checkbox" name="only_format_display" />
131 <span data-i18n="Only Format Display">Only Format Display</span>
132 </label>
133 <label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_prompt_desc" title="Chat history won't change, only the prompt as the request is sent (on generation).">
134 <input type="checkbox" name="only_format_prompt"/>
135 <span>
136 <span data-i18n="Only Format Prompt (?)">Only Format Prompt</span>
137 <span class="fa-solid fa-circle-question note-link-span"></span>
138 </span>
139 </label>
140 <label class="checkbox flex-container">120 <label class="checkbox flex-container">
141 <input type="checkbox" name="run_on_edit" />121 <input type="checkbox" name="run_on_edit" />
142 <span data-i18n="Run On Edit">Run On Edit</span>122 <span data-i18n="Run On Edit">Run On Edit</span>
@@ -148,6 +128,19 @@
148 <span class="fa-solid fa-circle-question note-link-span"></span>128 <span class="fa-solid fa-circle-question note-link-span"></span>
149 </span>129 </span>
150 </label>130 </label>
131 <span>
132 <small data-i18n="ext_regex_other_options" data-i18n="Ephemerality">Ephemerality</small>
133 <span class="fa-solid fa-circle-question note-link-span" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
134 </span>
135 <label class="checkbox flex-container" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
136 <input type="checkbox" name="only_format_display" />
137 <span data-i18n="Only Format Display">Alter Chat Display</span>
138 </label>
139 <label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_prompt_desc" title="Chat history file contents won't change, but regex will be applied to the outgoing prompt before it is sent to the LLM.">
140 <input type="checkbox" name="only_format_prompt" />
141 <span data-i18n="Only Format Prompt (?)">Alter Outgoing Prompt</span>
142 </label>
143
151 </div>144 </div>
152 </div>145 </div>
153 </div>146 </div>
public/scripts/extensions/shared.js+85 -2
@@ -1,5 +1,5 @@
1import { getRequestHeaders } from '../../script.js';1import { getRequestHeaders } from '../../script.js';
2import { extension_settings } from '../extensions.js';2import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
3import { oai_settings } from '../openai.js';3import { oai_settings } from '../openai.js';
4import { SECRET_KEYS, secret_state } from '../secrets.js';4import { SECRET_KEYS, secret_state } from '../secrets.js';
5import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';5import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
@@ -141,7 +141,7 @@ function throwIfInvalidModel(useReverseProxy) {
141 }141 }
142142
143 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {143 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
144 throw new Error('MakerSuite API key is not set.');144 throw new Error('Google AI Studio API key is not set.');
145 }145 }
146146
147 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {147 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
@@ -176,3 +176,86 @@ function throwIfInvalidModel(useReverseProxy) {
176 throw new Error('Custom API URL is not set.');176 throw new Error('Custom API URL is not set.');
177 }177 }
178}178}
179
180/**
181 * Check if the WebLLM extension is installed and supported.
182 * @returns {boolean} Whether the extension is installed and supported
183 */
184export function isWebLlmSupported() {
185 if (!('gpu' in navigator)) {
186 const warningKey = 'webllm_browser_warning_shown';
187 if (!sessionStorage.getItem(warningKey)) {
188 toastr.error('Your browser does not support the WebGPU API. Please use a different browser.', 'WebLLM', {
189 preventDuplicates: true,
190 timeOut: 0,
191 extendedTimeOut: 0,
192 });
193 sessionStorage.setItem(warningKey, '1');
194 }
195 return false;
196 }
197
198 if (!('llm' in SillyTavern)) {
199 const warningKey = 'webllm_extension_warning_shown';
200 if (!sessionStorage.getItem(warningKey)) {
201 toastr.error('WebLLM extension is not installed. Click here to install it.', 'WebLLM', {
202 timeOut: 0,
203 extendedTimeOut: 0,
204 preventDuplicates: true,
205 onclick: () => openThirdPartyExtensionMenu('https://github.com/SillyTavern/Extension-WebLLM'),
206 });
207 sessionStorage.setItem(warningKey, '1');
208 }
209 return false;
210 }
211
212 return true;
213}
214
215/**
216 * Generates text in response to a chat prompt using WebLLM.
217 * @param {any[]} messages Messages to use for generating
218 * @param {object} params Additional parameters
219 * @returns {Promise<string>} Generated response
220 */
221export async function generateWebLlmChatPrompt(messages, params = {}) {
222 if (!isWebLlmSupported()) {
223 throw new Error('WebLLM extension is not installed.');
224 }
225
226 console.debug('WebLLM chat completion request:', messages, params);
227 const engine = SillyTavern.llm;
228 const response = await engine.generateChatPrompt(messages, params);
229 console.debug('WebLLM chat completion response:', response);
230 return response;
231}
232
233/**
234 * Counts the number of tokens in the provided text using WebLLM's default model.
235 * @param {string} text Text to count tokens in
236 * @returns {Promise<number>} Number of tokens in the text
237 */
238export async function countWebLlmTokens(text) {
239 if (!isWebLlmSupported()) {
240 throw new Error('WebLLM extension is not installed.');
241 }
242
243 const engine = SillyTavern.llm;
244 const response = await engine.countTokens(text);
245 return response;
246}
247
248/**
249 * Gets the size of the context in the WebLLM's default model.
250 * @returns {Promise<number>} Size of the context in the WebLLM model
251 */
252export async function getWebLlmContextSize() {
253 if (!isWebLlmSupported()) {
254 throw new Error('WebLLM extension is not installed.');
255 }
256
257 const engine = SillyTavern.llm;
258 await engine.loadModel();
259 const model = await engine.getCurrentModelInfo();
260 return model?.context_size;
261}
public/scripts/extensions/stable-diffusion/index.js+272 -48
@@ -30,7 +30,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
30import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';30import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
31import { debounce_timeout } from '../../constants.js';31import { debounce_timeout } from '../../constants.js';
32import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';32import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
33import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';33import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
34export { MODULE_NAME };34export { MODULE_NAME };
3535
36const MODULE_NAME = 'sd';36const MODULE_NAME = 'sd';
@@ -51,6 +51,8 @@ const sources = {
51 drawthings: 'drawthings',51 drawthings: 'drawthings',
52 pollinations: 'pollinations',52 pollinations: 'pollinations',
53 stability: 'stability',53 stability: 'stability',
54 blockentropy: 'blockentropy',
55 huggingface: 'huggingface',
54};56};
5557
56const initiators = {58const initiators = {
@@ -58,6 +60,7 @@ const initiators = {
58 action: 'action',60 action: 'action',
59 interactive: 'interactive',61 interactive: 'interactive',
60 wand: 'wand',62 wand: 'wand',
63 swipe: 'swipe',
61};64};
6265
63const generationMode = {66const generationMode = {
@@ -452,6 +455,7 @@ async function loadSettings() {
452 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);455 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
453 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);456 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
454 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);457 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
458 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
455459
456 for (const style of extension_settings.sd.styles) {460 for (const style of extension_settings.sd.styles) {
457 const option = document.createElement('option');461 const option = document.createElement('option');
@@ -1089,6 +1093,11 @@ function onComfyUrlInput() {
1089 saveSettingsDebounced();1093 saveSettingsDebounced();
1090}1094}
10911095
1096function onHFModelInput() {
1097 extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();
1098 saveSettingsDebounced();
1099}
1100
1092function onComfyWorkflowChange() {1101function onComfyWorkflowChange() {
1093 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();1102 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();
1094 saveSettingsDebounced();1103 saveSettingsDebounced();
@@ -1096,7 +1105,18 @@ function onComfyWorkflowChange() {
10961105
1097async function onStabilityKeyClick() {1106async function onStabilityKeyClick() {
1098 const popupText = 'Stability AI API Key:';1107 const popupText = 'Stability AI API Key:';
1099 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT);1108 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {
1109 customButtons: [{
1110 text: 'Remove Key',
1111 appendAtEnd: true,
1112 result: POPUP_RESULT.NEGATIVE,
1113 action: async () => {
1114 await writeSecret(SECRET_KEYS.STABILITY, '');
1115 toastr.success('API Key removed');
1116 await loadSettingOptions();
1117 },
1118 }],
1119 });
11001120
1101 if (!key) {1121 if (!key) {
1102 return;1122 return;
@@ -1222,7 +1242,16 @@ async function onModelChange() {
1222 extension_settings.sd.model = $('#sd_model').find(':selected').val();1242 extension_settings.sd.model = $('#sd_model').find(':selected').val();
1223 saveSettingsDebounced();1243 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
1227 if (cloudSources.includes(extension_settings.sd.source)) {1256 if (cloudSources.includes(extension_settings.sd.source)) {
1228 return;1257 return;
@@ -1434,6 +1463,12 @@ async function loadSamplers() {
1434 case sources.stability:1463 case sources.stability:
1435 samplers = ['N/A'];1464 samplers = ['N/A'];
1436 break;1465 break;
1466 case sources.blockentropy:
1467 samplers = ['N/A'];
1468 break;
1469 case sources.huggingface:
1470 samplers = ['N/A'];
1471 break;
1437 }1472 }
14381473
1439 for (const sampler of samplers) {1474 for (const sampler of samplers) {
@@ -1620,6 +1655,12 @@ async function loadModels() {
1620 case sources.stability:1655 case sources.stability:
1621 models = await loadStabilityModels();1656 models = await loadStabilityModels();
1622 break;1657 break;
1658 case sources.blockentropy:
1659 models = await loadBlockEntropyModels();
1660 break;
1661 case sources.huggingface:
1662 models = [{ value: '', text: '<Enter Model ID above>' }];
1663 break;
1623 }1664 }
16241665
1625 for (const model of models) {1666 for (const model of models) {
@@ -1649,49 +1690,13 @@ async function loadStabilityModels() {
1649async function loadPollinationsModels() {1690async function loadPollinationsModels() {
1650 return [1691 return [
1651 {1692 {
1652 value: 'pixart',1693 value: 'flux',
1653 text: 'PixArt-αlpha',1694 text: 'FLUX.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',
1686 },1695 },
1687 {1696 {
1688 value: 'turbo',1697 value: 'turbo',
1689 text: 'SDXL Turbo',1698 text: 'SDXL Turbo',
1690 },1699 },
1691 {
1692 value: 'realvis',
1693 text: 'Realistic Vision',
1694 },
1695 ];1700 ];
1696}1701}
16971702
@@ -1714,6 +1719,26 @@ async function loadTogetherAIModels() {
1714 return [];1719 return [];
1715}1720}
17161721
1722async function loadBlockEntropyModels() {
1723 if (!secret_state[SECRET_KEYS.BLOCKENTROPY]) {
1724 console.debug('Block Entropy API key is not set.');
1725 return [];
1726 }
1727
1728 const result = await fetch('/api/sd/blockentropy/models', {
1729 method: 'POST',
1730 headers: getRequestHeaders(),
1731 });
1732 console.log(result);
1733 if (result.ok) {
1734 const data = await result.json();
1735 console.log(data);
1736 return data;
1737 }
1738
1739 return [];
1740}
1741
1717async function loadHordeModels() {1742async function loadHordeModels() {
1718 const result = await fetch('/api/horde/sd-models', {1743 const result = await fetch('/api/horde/sd-models', {
1719 method: 'POST',1744 method: 'POST',
@@ -1980,6 +2005,12 @@ async function loadSchedulers() {
1980 case sources.stability:2005 case sources.stability:
1981 schedulers = ['N/A'];2006 schedulers = ['N/A'];
1982 break;2007 break;
2008 case sources.blockentropy:
2009 schedulers = ['N/A'];
2010 break;
2011 case sources.huggingface:
2012 schedulers = ['N/A'];
2013 break;
1983 }2014 }
19842015
1985 for (const scheduler of schedulers) {2016 for (const scheduler of schedulers) {
@@ -2056,6 +2087,12 @@ async function loadVaes() {
2056 case sources.stability:2087 case sources.stability:
2057 vaes = ['N/A'];2088 vaes = ['N/A'];
2058 break;2089 break;
2090 case sources.blockentropy:
2091 vaes = ['N/A'];
2092 break;
2093 case sources.huggingface:
2094 vaes = ['N/A'];
2095 break;
2059 }2096 }
20602097
2061 for (const vae of vaes) {2098 for (const vae of vaes) {
@@ -2267,9 +2304,9 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2267 const quietPrompt = getQuietPrompt(generationType, trigger);2304 const quietPrompt = getQuietPrompt(generationType, trigger);
2268 const context = getContext();2305 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].id2307 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 it2308 ? 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
2274 if (generationType == generationMode.BACKGROUND) {2311 if (generationType == generationMode.BACKGROUND) {
2275 const callbackOriginal = callback;2312 const callbackOriginal = callback;
@@ -2584,6 +2621,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2584 case sources.stability:2621 case sources.stability:
2585 result = await generateStabilityImage(prefixedPrompt, negativePrompt, signal);2622 result = await generateStabilityImage(prefixedPrompt, negativePrompt, signal);
2586 break;2623 break;
2624 case sources.blockentropy:
2625 result = await generateBlockEntropyImage(prefixedPrompt, negativePrompt, signal);
2626 break;
2627 case sources.huggingface:
2628 result = await generateHuggingFaceImage(prefixedPrompt, signal);
2629 break;
2587 }2630 }
25882631
2589 if (!result.data) {2632 if (!result.data) {
@@ -2639,6 +2682,40 @@ async function generateTogetherAIImage(prompt, negativePrompt, signal) {
2639 }2682 }
2640}2683}
26412684
2685async function generateBlockEntropyImage(prompt, negativePrompt, signal) {
2686 const result = await fetch('/api/sd/blockentropy/generate', {
2687 method: 'POST',
2688 headers: getRequestHeaders(),
2689 signal: signal,
2690 body: JSON.stringify({
2691 prompt: prompt,
2692 negative_prompt: negativePrompt,
2693 model: extension_settings.sd.model,
2694 steps: extension_settings.sd.steps,
2695 width: extension_settings.sd.width,
2696 height: extension_settings.sd.height,
2697 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
2698 }),
2699 });
2700
2701 if (result.ok) {
2702 const data = await result.json();
2703
2704 // Default format is 'jpg'
2705 let format = 'jpg';
2706
2707 // Check if a format is specified in the result
2708 if (data.format) {
2709 format = data.format.toLowerCase();
2710 }
2711
2712 return { format: format, data: data.images[0] };
2713 } else {
2714 const text = await result.text();
2715 throw new Error(text);
2716 }
2717}
2718
2642/**2719/**
2643 * Generates an image using the Pollinations API.2720 * Generates an image using the Pollinations API.
2644 * @param {string} prompt - The main instruction used to guide the image generation.2721 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3183,6 +3260,34 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3183 return { format: 'png', data: await promptResult.text() };3260 return { format: 'png', data: await promptResult.text() };
3184}3261}
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 */
3270async function generateHuggingFaceImage(prompt, signal) {
3271 const result = await fetch('/api/sd/huggingface/generate', {
3272 method: 'POST',
3273 headers: getRequestHeaders(),
3274 signal: signal,
3275 body: JSON.stringify({
3276 model: extension_settings.sd.huggingface_model_id,
3277 prompt: prompt,
3278 }),
3279 });
3280
3281 if (result.ok) {
3282 const data = await result.json();
3283 return { format: 'jpg', data: data.image };
3284 } else {
3285 const text = await result.text();
3286 throw new Error(text);
3287 }
3288}
3289
3290
3186async function onComfyOpenWorkflowEditorClick() {3291async function onComfyOpenWorkflowEditorClick() {
3187 let workflow = await (await fetch('/api/sd/comfy/workflow', {3292 let workflow = await (await fetch('/api/sd/comfy/workflow', {
3188 method: 'POST',3293 method: 'POST',
@@ -3348,11 +3453,15 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
3348 generationType: generationType,3453 generationType: generationType,
3349 negative: additionalNegativePrefix,3454 negative: additionalNegativePrefix,
3350 inline_image: false,3455 inline_image: false,
3456 image_swipes: [image],
3351 },3457 },
3352 };3458 };
3353 context.chat.push(message);3459 context.chat.push(message);
3460 const messageId = context.chat.length - 1;
3461 await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
3354 context.addOneMessage(message);3462 context.addOneMessage(message);
3355 context.saveChat();3463 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
3464 await context.saveChat();
3356}3465}
33573466
3358/**3467/**
@@ -3396,7 +3505,7 @@ async function addSDGenButtons() {
3396 $(document).on('click touchend', function (e) {3505 $(document).on('click touchend', function (e) {
3397 const target = $(e.target);3506 const target = $(e.target);
3398 if (target.is(dropdown) || target.closest(dropdown).length) return;3507 if (target.is(dropdown) || target.closest(dropdown).length) return;
3399 if ((target.is(button) || target.closest(button).length) && !dropdown.is(':visible') && $('#send_but').is(':visible')) {3508 if ((target.is(button) || target.closest(button).length) && !dropdown.is(':visible')) {
3400 e.preventDefault();3509 e.preventDefault();
34013510
3402 dropdown.fadeIn(animation_duration);3511 dropdown.fadeIn(animation_duration);
@@ -3456,6 +3565,10 @@ function isValidState() {
3456 return true;3565 return true;
3457 case sources.stability:3566 case sources.stability:
3458 return secret_state[SECRET_KEYS.STABILITY];3567 return secret_state[SECRET_KEYS.STABILITY];
3568 case sources.blockentropy:
3569 return secret_state[SECRET_KEYS.BLOCKENTROPY];
3570 case sources.huggingface:
3571 return secret_state[SECRET_KEYS.HUGGINGFACE];
3459 }3572 }
3460}3573}
34613574
@@ -3485,7 +3598,9 @@ async function sdMessageButton(e) {
3485 const $mes = $icon.closest('.mes');3598 const $mes = $icon.closest('.mes');
3486 const message_id = $mes.attr('mesid');3599 const message_id = $mes.attr('mesid');
3487 const message = context.chat[message_id];3600 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;
3489 const messageText = message?.mes;3604 const messageText = message?.mes;
3490 const hasSavedImage = message?.extra?.image && message?.extra?.title;3605 const hasSavedImage = message?.extra?.image && message?.extra?.title;
3491 const hasSavedNegative = message?.extra?.negative;3606 const hasSavedNegative = message?.extra?.negative;
@@ -3529,10 +3644,23 @@ async function sdMessageButton(e) {
35293644
3530 function saveGeneratedImage(prompt, image, generationType, negative) {3645 function saveGeneratedImage(prompt, image, generationType, negative) {
3531 // Some message sources may not create the extra object3646 // Some message sources may not create the extra object
3532 if (typeof message.extra !== 'object') {3647 if (typeof message.extra !== 'object' || message.extra === null) {
3533 message.extra = {};3648 message.extra = {};
3534 }3649 }
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
3536 // If already contains an image and it's not inline - leave it as is3664 // If already contains an image and it's not inline - leave it as is
3537 message.extra.inline_image = message.extra.image && !message.extra.inline_image ? false : true;3665 message.extra.inline_image = message.extra.image && !message.extra.inline_image ? false : true;
3538 message.extra.image = image;3666 message.extra.image = image;
@@ -3571,6 +3699,99 @@ async function writePromptFields(characterId) {
3571 await writeExtensionField(characterId, 'sd_character_prompt', promptObject);3699 await writeExtensionField(characterId, 'sd_character_prompt', promptObject);
3572}3700}
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 */
3710async function onImageSwiped({ message, element, direction }) {
3711 const context = getContext();
3712 const animationClass = 'fa-fade';
3713 const messageImg = element.find('.mes_img');
3714
3715 // Current image is already animating
3716 if (messageImg.hasClass(animationClass)) {
3717 return;
3718 }
3719
3720 const swipes = message?.extra?.image_swipes;
3721
3722 if (!Array.isArray(swipes)) {
3723 console.warn('No image swipes found in the message');
3724 return;
3725 }
3726
3727 const currentIndex = swipes.indexOf(message.extra.image);
3728
3729 if (currentIndex === -1) {
3730 console.warn('Current image not found in the swipes');
3731 return;
3732 }
3733
3734 // Switch to previous image or wrap around if at the beginning
3735 if (direction === 'left') {
3736 const newIndex = currentIndex === 0 ? swipes.length - 1 : currentIndex - 1;
3737 message.extra.image = swipes[newIndex];
3738
3739 // Update the image in the message
3740 appendMediaToMessage(message, element, false);
3741 }
3742
3743 // Switch to next image or generate a new one if at the end
3744 if (direction === 'right') {
3745 const newIndex = currentIndex === swipes.length - 1 ? swipes.length : currentIndex + 1;
3746
3747 if (newIndex === swipes.length) {
3748 const abortController = new AbortController();
3749 const swipeControls = element.find('.mes_img_swipes');
3750 const stopButton = document.getElementById('sd_stop_gen');
3751 const stopListener = () => abortController.abort('Aborted by user');
3752 const generationType = message?.extra?.generationType ?? generationMode.FREE;
3753 const dimensions = setTypeSpecificDimensions(generationType);
3754 const originalSeed = extension_settings.sd.seed;
3755 extension_settings.sd.seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
3756 let imagePath = '';
3757
3758 try {
3759 $(stopButton).show();
3760 eventSource.once(CUSTOM_STOP_EVENT, stopListener);
3761 const callback = () => { };
3762 const hasNegative = message.extra.negative;
3763 const prompt = await refinePrompt(message.extra.title, false, false);
3764 const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, false, true) : '';
3765 const characterName = context.groupId
3766 ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
3767 : context.characters[context.characterId]?.name;
3768
3769 messageImg.addClass(animationClass);
3770 swipeControls.hide();
3771 imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiators.swipe, abortController.signal);
3772 } finally {
3773 $(stopButton).hide();
3774 messageImg.removeClass(animationClass);
3775 swipeControls.show();
3776 eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
3777 restoreOriginalDimensions(dimensions);
3778 extension_settings.sd.seed = originalSeed;
3779 }
3780
3781 if (!imagePath) {
3782 return;
3783 }
3784
3785 swipes.push(imagePath);
3786 }
3787
3788 message.extra.image = swipes[newIndex];
3789 appendMediaToMessage(message, element, false);
3790 }
3791
3792 await context.saveChat();
3793}
3794
3574jQuery(async () => {3795jQuery(async () => {
3575 await addSDGenButtons();3796 await addSDGenButtons();
35763797
@@ -3688,6 +3909,7 @@ jQuery(async () => {
3688 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);3909 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
3689 $('#sd_stability_key').on('click', onStabilityKeyClick);3910 $('#sd_stability_key').on('click', onStabilityKeyClick);
3690 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);3911 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
3912 $('#sd_huggingface_model_id').on('input', onHFModelInput);
36913913
3692 $('.sd_settings .inline-drawer-toggle').on('click', function () {3914 $('.sd_settings .inline-drawer-toggle').on('click', function () {
3693 initScrollHeight($('#sd_prompt_prefix'));3915 initScrollHeight($('#sd_prompt_prefix'));
@@ -3709,6 +3931,8 @@ jQuery(async () => {
3709 }3931 }
3710 });3932 });
37113933
3934 eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped);
3935
3712 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);3936 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
37133937
3714 await loadSettings();3938 await loadSettings();
public/scripts/extensions/stable-diffusion/settings.html+10 -2
@@ -29,7 +29,8 @@
29 </label>29 </label>
30 <label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">30 <label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">
31 <input id="sd_expand" type="checkbox" />31 <input id="sd_expand" type="checkbox" />
32 <span data-i18n="sd_expand_txt">Auto-enhance prompts</span>32 <span data-i18n="sd_expand_txt">Auto-extend prompts</span>
33 <span class="right_menu_button fa-solid fa-triangle-exclamation" data-i18n="[title]sd_expand_warning" title="May produce unexpected results. Manual prompt editing is recommended."></span>
33 </label>34 </label>
34 <label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">35 <label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
35 <input id="sd_snap" type="checkbox" />36 <input id="sd_snap" type="checkbox" />
@@ -37,9 +38,11 @@
37 </label>38 </label>
38 <label for="sd_source" data-i18n="Source">Source</label>39 <label for="sd_source" data-i18n="Source">Source</label>
39 <select id="sd_source">40 <select id="sd_source">
41 <option value="blockentropy">Block Entropy</option>
40 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
41 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
42 <option value="extras">Extras API (local / remote)</option>44 <option value="extras">Extras API (local / remote)</option>
45 <option value="huggingface">HuggingFace Inference API (serverless)</option>
43 <option value="novel">NovelAI Diffusion</option>46 <option value="novel">NovelAI Diffusion</option>
44 <option value="openai">OpenAI (DALL-E)</option>47 <option value="openai">OpenAI (DALL-E)</option>
45 <option value="pollinations">Pollinations</option>48 <option value="pollinations">Pollinations</option>
@@ -81,6 +84,11 @@
81 <!-- (Original Text)<b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine. -->84 <!-- (Original Text)<b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine. -->
82 <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_drawthings_auth_txt"> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i>85 <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_drawthings_auth_txt"> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i>
83 </div>86 </div>
87 <div data-sd-source="huggingface">
88 <i>Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.</i>
89 <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
90 <input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" />
91 </div>
84 <div data-sd-source="vlad">92 <div data-sd-source="vlad">
85 <label for="sd_vlad_url">SD.Next API URL</label>93 <label for="sd_vlad_url">SD.Next API URL</label>
86 <div class="flex-container flexnowrap">94 <div class="flex-container flexnowrap">
@@ -378,7 +386,7 @@
378 </label>386 </label>
379 </div>387 </div>
380388
381 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability" class="marginTop5">389 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,blockentropy" class="marginTop5">
382 <label for="sd_seed">390 <label for="sd_seed">
383 <span data-i18n="Seed">Seed</span>391 <span data-i18n="Seed">Seed</span>
384 <small data-i18n="(-1 for random)">(-1 for random)</small>392 <small data-i18n="(-1 for random)">(-1 for random)</small>
public/scripts/extensions/translate/index.js+13 -2
@@ -10,7 +10,7 @@ import {
10 updateMessageBlock,10 updateMessageBlock,
11} from '../../../script.js';11} from '../../../script.js';
12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
13import { POPUP_TYPE, callGenericPopup } from '../../popup.js';13import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
14import { findSecret, secret_state, writeSecret } from '../../secrets.js';14import { findSecret, secret_state, writeSecret } from '../../secrets.js';
15import { SlashCommand } from '../../slash-commands/SlashCommand.js';15import { SlashCommand } from '../../slash-commands/SlashCommand.js';
16import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';16import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@@ -621,7 +621,18 @@ jQuery(async () => {
621 const secretKey = extension_settings.translate.provider + '_url';621 const secretKey = extension_settings.translate.provider + '_url';
622 const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';622 const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
623623
624 const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl);624 const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl,{
625 customButtons: [{
626 text: 'Remove URL',
627 appendAtEnd: true,
628 result: POPUP_RESULT.NEGATIVE,
629 action: async () => {
630 await writeSecret(secretKey, '');
631 toastr.success('API URL removed');
632 $('#translate_url_button').toggleClass('success', !!secret_state[secretKey]);
633 },
634 }],
635 });
625636
626 if (url == false || url == '') {637 if (url == false || url == '') {
627 return;638 return;
public/scripts/extensions/tts/azure.js+14 -2
@@ -1,5 +1,5 @@
1import { getRequestHeaders } from '../../../script.js';1import { getRequestHeaders } from '../../../script.js';
2import { POPUP_TYPE, callGenericPopup } from '../../popup.js';2import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
3import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';3import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';
4import { getPreviewString, saveTtsProviderSettings } from './index.js';4import { getPreviewString, saveTtsProviderSettings } from './index.js';
5export { AzureTtsProvider };5export { AzureTtsProvider };
@@ -70,7 +70,19 @@ class AzureTtsProvider {
70 const popupText = 'Azure TTS API Key';70 const popupText = 'Azure TTS API Key';
71 const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : '';71 const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : '';
7272
73 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey);73 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
75 if (key == false || key == '') {87 if (key == false || key == '') {
76 return;88 return;
public/scripts/extensions/tts/index.js+16 -9
@@ -9,6 +9,7 @@ import { SystemTtsProvider } from './system.js';
9import { NovelTtsProvider } from './novel.js';9import { NovelTtsProvider } from './novel.js';
10import { power_user } from '../../power-user.js';10import { power_user } from '../../power-user.js';
11import { OpenAITtsProvider } from './openai.js';11import { OpenAITtsProvider } from './openai.js';
12import { OpenAICompatibleTtsProvider } from './openai-compatible.js';
12import { XTTSTtsProvider } from './xtts.js';13import { XTTSTtsProvider } from './xtts.js';
13import { VITSTtsProvider } from './vits.js';14import { VITSTtsProvider } from './vits.js';
14import { GSVITtsProvider } from './gsvi.js';15import { GSVITtsProvider } from './gsvi.js';
@@ -82,20 +83,21 @@ export function getPreviewString(lang) {
82}83}
8384
84const ttsProviders = {85const ttsProviders = {
85 ElevenLabs: ElevenLabsTtsProvider,86 AllTalk: AllTalkTtsProvider,
86 Silero: SileroTtsProvider,87 Azure: AzureTtsProvider,
87 XTTSv2: XTTSTtsProvider,
88 VITS: VITSTtsProvider,
89 GSVI: GSVITtsProvider,
90 SBVits2: SBVits2TtsProvider,
91 System: SystemTtsProvider,
92 Coqui: CoquiTtsProvider,88 Coqui: CoquiTtsProvider,
93 Edge: EdgeTtsProvider,89 Edge: EdgeTtsProvider,
90 ElevenLabs: ElevenLabsTtsProvider,
91 GSVI: GSVITtsProvider,
94 Novel: NovelTtsProvider,92 Novel: NovelTtsProvider,
95 OpenAI: OpenAITtsProvider,93 OpenAI: OpenAITtsProvider,
96 AllTalk: AllTalkTtsProvider,94 'OpenAI Compatible': OpenAICompatibleTtsProvider,
95 SBVits2: SBVits2TtsProvider,
96 Silero: SileroTtsProvider,
97 SpeechT5: SpeechT5TtsProvider,97 SpeechT5: SpeechT5TtsProvider,
98 Azure: AzureTtsProvider,98 System: SystemTtsProvider,
99 VITS: VITSTtsProvider,
100 XTTSv2: XTTSTtsProvider,
99};101};
100let ttsProvider;102let ttsProvider;
101let ttsProviderName;103let ttsProviderName;
@@ -753,6 +755,11 @@ async function onMessageEvent(messageId, lastCharIndex) {
753 const message = structuredClone(context.chat[messageId]);755 const message = structuredClone(context.chat[messageId]);
754 const hashNew = getStringHash(message?.mes ?? '');756 const hashNew = getStringHash(message?.mes ?? '');
755757
758 // Ignore prompt-hidden messages
759 if (message.is_system) {
760 return;
761 }
762
756 // if no new messages, or same message, or same message hash, do nothing763 // if no new messages, or same message, or same message hash, do nothing
757 if (hashNew === lastMessageHash) {764 if (hashNew === lastMessageHash) {
758 return;765 return;
public/scripts/extensions/tts/openai-compatible.js+193 -0
@@ -0,0 +1,193 @@
1import { getRequestHeaders } from '../../../script.js';
2import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
3import { findSecret, SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
4import { getPreviewString, saveTtsProviderSettings } from './index.js';
5
6export { OpenAICompatibleTtsProvider };
7
8class 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 {
124 if (hasEnabledVoice) {124 if (hasEnabledVoice) {
125 return;125 return;
126 }126 }
127 const utterance = new SpeechSynthesisUtterance('hi');127 const utterance = new SpeechSynthesisUtterance(' . ');
128 utterance.volume = 0;128 utterance.volume = 0;
129 speechSynthesis.speak(utterance);129 speechSynthesis.speak(utterance);
130 hasEnabledVoice = true;130 hasEnabledVoice = true;
public/scripts/extensions/vectors/index.js+75 -13
@@ -30,6 +30,13 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
30import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';30import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
31import { SlashCommand } from '../../slash-commands/SlashCommand.js';31import { SlashCommand } from '../../slash-commands/SlashCommand.js';
32import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';32import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
33import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
34import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
35
36/**
37 * @typedef {object} HashedMessage
38 * @property {string} text - The hashed message text
39 */
3340
34const MODULE_NAME = 'vectors';41const MODULE_NAME = 'vectors';
3542
@@ -191,6 +198,11 @@ function splitByChunks(items) {
191 return chunkedItems;198 return chunkedItems;
192}199}
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 */
194async function summarizeExtra(hashedMessages) {206async function summarizeExtra(hashedMessages) {
195 for (const element of hashedMessages) {207 for (const element of hashedMessages) {
196 try {208 try {
@@ -222,6 +234,11 @@ async function summarizeExtra(hashedMessages) {
222 return hashedMessages;234 return hashedMessages;
223}235}
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 */
225async function summarizeMain(hashedMessages) {242async function summarizeMain(hashedMessages) {
226 for (const element of hashedMessages) {243 for (const element of hashedMessages) {
227 element.text = await generateRaw(element.text, '', false, false, settings.summary_prompt);244 element.text = await generateRaw(element.text, '', false, false, settings.summary_prompt);
@@ -230,12 +247,39 @@ async function summarizeMain(hashedMessages) {
230 return hashedMessages;247 return hashedMessages;
231}248}
232249
250/**
251 * Summarizes messages using WebLLM.
252 * @param {HashedMessage[]} hashedMessages Array of hashed messages
253 * @returns {Promise<HashedMessage[]>} Summarized messages
254 */
255async 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 */
233async function summarize(hashedMessages, endpoint = 'main') {275async function summarize(hashedMessages, endpoint = 'main') {
234 switch (endpoint) {276 switch (endpoint) {
235 case 'main':277 case 'main':
236 return await summarizeMain(hashedMessages);278 return await summarizeMain(hashedMessages);
237 case 'extras':279 case 'extras':
238 return await summarizeExtra(hashedMessages);280 return await summarizeExtra(hashedMessages);
281 case 'webllm':
282 return await summarizeWebLLM(hashedMessages);
239 default:283 default:
240 console.error('Unsupported endpoint', endpoint);284 console.error('Unsupported endpoint', endpoint);
241 }285 }
@@ -357,7 +401,7 @@ async function processFiles(chat) {
357 const dataBankCollectionIds = await ingestDataBankAttachments();401 const dataBankCollectionIds = await ingestDataBankAttachments();
358402
359 if (dataBankCollectionIds.length) {403 if (dataBankCollectionIds.length) {
360 const queryText = await getQueryText(chat);404 const queryText = await getQueryText(chat, 'file');
361 await injectDataBankChunks(queryText, dataBankCollectionIds);405 await injectDataBankChunks(queryText, dataBankCollectionIds);
362 }406 }
363407
@@ -391,7 +435,7 @@ async function processFiles(chat) {
391 await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size, settings.overlap_percent);435 await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size, settings.overlap_percent);
392 }436 }
393437
394 const queryText = await getQueryText(chat);438 const queryText = await getQueryText(chat, 'file');
395 const fileChunks = await retrieveFileChunks(queryText, collectionId);439 const fileChunks = await retrieveFileChunks(queryText, collectionId);
396440
397 message.mes = `${fileChunks}\n\n${message.mes}`;441 message.mes = `${fileChunks}\n\n${message.mes}`;
@@ -552,7 +596,7 @@ async function rearrangeChat(chat) {
552 return;596 return;
553 }597 }
554598
555 const queryText = await getQueryText(chat);599 const queryText = await getQueryText(chat, 'chat');
556600
557 if (queryText.length === 0) {601 if (queryText.length === 0) {
558 console.debug('Vectors: No text to query');602 console.debug('Vectors: No text to query');
@@ -639,15 +683,16 @@ const onChatEvent = debounce(async () => await moduleWorker.update(), debounce_t
639/**683/**
640 * Gets the text to query from the chat684 * Gets the text to query from the chat
641 * @param {object[]} chat Chat messages685 * @param {object[]} chat Chat messages
686 * @param {'file'|'chat'|'world-info'} initiator Initiator of the query
642 * @returns {Promise<string>} Text to query687 * @returns {Promise<string>} Text to query
643 */688 */
644async function getQueryText(chat) {689async function getQueryText(chat, initiator) {
645 let queryText = '';690 let queryText = '';
646 let i = 0;691 let i = 0;
647692
648 let hashedMessages = chat.map(x => ({ text: String(substituteParams(x.mes)) }));693 let hashedMessages = chat.map(x => ({ text: String(substituteParams(x.mes)) }));
649694
650 if (settings.summarize && settings.summarize_sent) {695 if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) {
651 hashedMessages = await summarize(hashedMessages, settings.summary_source);696 hashedMessages = await summarize(hashedMessages, settings.summary_source);
652 }697 }
653698
@@ -1235,7 +1280,7 @@ async function activateWorldInfo(chat) {
1235 }1280 }
12361281
1237 // Perform a multi-query1282 // Perform a multi-query
1238 const queryText = await getQueryText(chat);1283 const queryText = await getQueryText(chat, 'world-info');
12391284
1240 if (queryText.length === 0) {1285 if (queryText.length === 0) {
1241 console.debug('Vectors: No text to query for WI');1286 console.debug('Vectors: No text to query for WI');
@@ -1299,11 +1344,30 @@ jQuery(async () => {
1299 saveSettingsDebounced();1344 saveSettingsDebounced();
1300 toggleSettings();1345 toggleSettings();
1301 });1346 });
1302 $('#api_key_nomicai').on('change', () => {1347 $('#api_key_nomicai').on('click', 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;
1306 }1365 }
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');
1307 saveSettingsDebounced();1371 saveSettingsDebounced();
1308 });1372 });
1309 $('#vectors_togetherai_model').val(settings.togetherai_model).on('change', () => {1373 $('#vectors_togetherai_model').val(settings.togetherai_model).on('change', () => {
@@ -1531,9 +1595,7 @@ jQuery(async () => {
1531 $('#dialogue_popup_input').val(presetModel);1595 $('#dialogue_popup_input').val(presetModel);
1532 });1596 });
15331597
1534 const validSecret = !!secret_state[SECRET_KEYS.NOMICAI];1598 $('#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
1538 toggleSettings();1600 toggleSettings();
1539 eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);1601 eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);
public/scripts/extensions/vectors/settings.html+8 -11
@@ -12,7 +12,7 @@
12 <select id="vectors_source" class="text_pole">12 <select id="vectors_source" class="text_pole">
13 <option value="cohere">Cohere</option>13 <option value="cohere">Cohere</option>
14 <option value="extras">Extras</option>14 <option value="extras">Extras</option>
15 <option value="palm">Google MakerSuite</option>15 <option value="palm">Google AI Studio</option>
16 <option value="llamacpp">llama.cpp</option>16 <option value="llamacpp">llama.cpp</option>
17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>17 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
18 <option value="mistral">MistralAI</option>18 <option value="mistral">MistralAI</option>
@@ -103,17 +103,13 @@
103 </span>103 </span>
104 </small>104 </small>
105105
106 <div class="flex-container flexFlowColumn" id="nomicai_apiKey">106 <div class="flex-container alignItemsCenter" id="nomicai_apiKey">
107 <label for="api_key_nomicai">107 <label for="api_key_nomicai" class="flex1">
108 <span data-i18n="NomicAI API Key">NomicAI API Key</span>108 <span data-i18n="NomicAI API Key">NomicAI API Key</span>
109 </label>109 </label>
110 <div class="flex-container">110 <div id="api_key_nomicai" class="menu_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.
117 </div>113 </div>
118 </div>114 </div>
119115
@@ -378,10 +374,11 @@
378 <select id="vectors_summary_source" class="text_pole">374 <select id="vectors_summary_source" class="text_pole">
379 <option value="main" data-i18n="Main API">Main API</option>375 <option value="main" data-i18n="Main API">Main API</option>
380 <option value="extras" data-i18n="Extras API">Extras API</option>376 <option value="extras" data-i18n="Extras API">Extras API</option>
377 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
381 </select>378 </select>
382379
383 <label for="vectors_summary_prompt" title="Summary Prompt:">Summary Prompt:</label>380 <label for="vectors_summary_prompt" title="Summary Prompt:">Summary Prompt:</label>
384 <small data-i18n="Only used when Main API is selected.">Only used when Main API is selected.</small>381 <small data-i18n="Only used when Main API or WebLLM Extension is selected.">Only used when Main API or WebLLM Extension is selected.</small>
385 <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>382 <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>
386 </div>383 </div>
387 </div>384 </div>
public/scripts/openai.js+83 -5
@@ -120,6 +120,7 @@ const default_bias_presets = {
120const max_2k = 2047;120const max_2k = 2047;
121const max_4k = 4095;121const max_4k = 4095;
122const max_8k = 8191;122const max_8k = 8191;
123const max_12k = 12287;
123const max_16k = 16383;124const max_16k = 16383;
124const max_32k = 32767;125const max_32k = 32767;
125const max_64k = 65535;126const max_64k = 65535;
@@ -186,6 +187,7 @@ export const chat_completion_sources = {
186 PERPLEXITY: 'perplexity',187 PERPLEXITY: 'perplexity',
187 GROQ: 'groq',188 GROQ: 'groq',
188 ZEROONEAI: '01ai',189 ZEROONEAI: '01ai',
190 BLOCKENTROPY: 'blockentropy',
189};191};
190192
191const character_names_behavior = {193const character_names_behavior = {
@@ -238,7 +240,7 @@ const default_settings = {
238 top_p_openai: 1.0,240 top_p_openai: 1.0,
239 top_k_openai: 0,241 top_k_openai: 0,
240 min_p_openai: 0,242 min_p_openai: 0,
241 top_a_openai: 1,243 top_a_openai: 0,
242 repetition_penalty_openai: 1,244 repetition_penalty_openai: 1,
243 stream_openai: false,245 stream_openai: false,
244 websearch_cohere: false,246 websearch_cohere: false,
@@ -268,6 +270,7 @@ const default_settings = {
268 perplexity_model: 'llama-3.1-70b-instruct',270 perplexity_model: 'llama-3.1-70b-instruct',
269 groq_model: 'llama-3.1-70b-versatile',271 groq_model: 'llama-3.1-70b-versatile',
270 zerooneai_model: 'yi-large',272 zerooneai_model: 'yi-large',
273 blockentropy_model: 'be-70b-base-llama3.1',
271 custom_model: '',274 custom_model: '',
272 custom_url: '',275 custom_url: '',
273 custom_include_body: '',276 custom_include_body: '',
@@ -318,7 +321,7 @@ const oai_settings = {
318 top_p_openai: 1.0,321 top_p_openai: 1.0,
319 top_k_openai: 0,322 top_k_openai: 0,
320 min_p_openai: 0,323 min_p_openai: 0,
321 top_a_openai: 1,324 top_a_openai: 0,
322 repetition_penalty_openai: 1,325 repetition_penalty_openai: 1,
323 stream_openai: false,326 stream_openai: false,
324 websearch_cohere: false,327 websearch_cohere: false,
@@ -348,6 +351,7 @@ const oai_settings = {
348 perplexity_model: 'llama-3.1-70b-instruct',351 perplexity_model: 'llama-3.1-70b-instruct',
349 groq_model: 'llama-3.1-70b-versatile',352 groq_model: 'llama-3.1-70b-versatile',
350 zerooneai_model: 'yi-large',353 zerooneai_model: 'yi-large',
354 blockentropy_model: 'be-70b-base-llama3.1',
351 custom_model: '',355 custom_model: '',
352 custom_url: '',356 custom_url: '',
353 custom_include_body: '',357 custom_include_body: '',
@@ -804,7 +808,8 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
804808
805 // Reserve budget for group nudge809 // Reserve budget for group nudge
806 let groupNudgeMessage = null;810 let groupNudgeMessage = null;
807 if (selected_group) {811 const noGroupNudgeTypes = ['impersonate'];
812 if (selected_group && prompts.has('groupNudge') && !noGroupNudgeTypes.includes(type)) {
808 groupNudgeMessage = Message.fromPrompt(prompts.get('groupNudge'));813 groupNudgeMessage = Message.fromPrompt(prompts.get('groupNudge'));
809 chatCompletion.reserveBudget(groupNudgeMessage);814 chatCompletion.reserveBudget(groupNudgeMessage);
810 }815 }
@@ -1542,6 +1547,8 @@ function getChatCompletionModel() {
1542 return oai_settings.groq_model;1547 return oai_settings.groq_model;
1543 case chat_completion_sources.ZEROONEAI:1548 case chat_completion_sources.ZEROONEAI:
1544 return oai_settings.zerooneai_model;1549 return oai_settings.zerooneai_model;
1550 case chat_completion_sources.BLOCKENTROPY:
1551 return oai_settings.blockentropy_model;
1545 default:1552 default:
1546 throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`);1553 throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`);
1547 }1554 }
@@ -1655,6 +1662,23 @@ function saveModelList(data) {
16551662
1656 $('#model_01ai_select').val(oai_settings.zerooneai_model).trigger('change');1663 $('#model_01ai_select').val(oai_settings.zerooneai_model).trigger('change');
1657 }1664 }
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 }
1658}1682}
16591683
1660function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {1684function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {
@@ -3015,6 +3039,7 @@ function loadOpenAISettings(data, settings) {
3015 oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;3039 oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;
3016 oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model;3040 oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model;
3017 oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;3041 oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;
3042 oai_settings.blockentropy_model = settings.blockentropy_model ?? default_settings.blockentropy_model;
3018 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;3043 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
3019 oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;3044 oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
3020 oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;3045 oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
@@ -3048,6 +3073,7 @@ function loadOpenAISettings(data, settings) {
3048 oai_settings.names_behavior = settings.names_behavior ?? default_settings.names_behavior;3073 oai_settings.names_behavior = settings.names_behavior ?? default_settings.names_behavior;
3049 oai_settings.continue_postfix = settings.continue_postfix ?? default_settings.continue_postfix;3074 oai_settings.continue_postfix = settings.continue_postfix ?? default_settings.continue_postfix;
3050 oai_settings.function_calling = settings.function_calling ?? default_settings.function_calling;3075 oai_settings.function_calling = settings.function_calling ?? default_settings.function_calling;
3076 oai_settings.openrouter_providers = settings.openrouter_providers ?? default_settings.openrouter_providers;
30513077
3052 // Migrate from old settings3078 // Migrate from old settings
3053 if (settings.names_in_completion === true) {3079 if (settings.names_in_completion === true) {
@@ -3093,6 +3119,7 @@ function loadOpenAISettings(data, settings) {
3093 $('#model_groq_select').val(oai_settings.groq_model);3119 $('#model_groq_select').val(oai_settings.groq_model);
3094 $(`#model_groq_select option[value="${oai_settings.groq_model}"`).attr('selected', true);3120 $(`#model_groq_select option[value="${oai_settings.groq_model}"`).attr('selected', true);
3095 $('#model_01ai_select').val(oai_settings.zerooneai_model);3121 $('#model_01ai_select').val(oai_settings.zerooneai_model);
3122 $('#model_blockentropy_select').val(oai_settings.blockentropy_model);
3096 $('#custom_model_id').val(oai_settings.custom_model);3123 $('#custom_model_id').val(oai_settings.custom_model);
3097 $('#custom_api_url_text').val(oai_settings.custom_url);3124 $('#custom_api_url_text').val(oai_settings.custom_url);
3098 $('#openai_max_context').val(oai_settings.openai_max_context);3125 $('#openai_max_context').val(oai_settings.openai_max_context);
@@ -3354,6 +3381,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3354 perplexity_model: settings.perplexity_model,3381 perplexity_model: settings.perplexity_model,
3355 groq_model: settings.groq_model,3382 groq_model: settings.groq_model,
3356 zerooneai_model: settings.zerooneai_model,3383 zerooneai_model: settings.zerooneai_model,
3384 blockentropy_model: settings.blockentropy_model,
3357 custom_model: settings.custom_model,3385 custom_model: settings.custom_model,
3358 custom_url: settings.custom_url,3386 custom_url: settings.custom_url,
3359 custom_include_body: settings.custom_include_body,3387 custom_include_body: settings.custom_include_body,
@@ -3596,6 +3624,8 @@ async function onPresetImportFileChange(e) {
3596 }3624 }
3597 }3625 }
35983626
3627 await eventSource.emit(event_types.OAI_PRESET_IMPORT_READY, { data: presetBody, presetName: name });
3628
3599 const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, {3629 const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, {
3600 method: 'POST',3630 method: 'POST',
3601 headers: getRequestHeaders(),3631 headers: getRequestHeaders(),
@@ -3651,6 +3681,7 @@ async function onExportPresetClick() {
3651 sensitiveFields.forEach(field => delete preset[field]);3681 sensitiveFields.forEach(field => delete preset[field]);
3652 }3682 }
36533683
3684 await eventSource.emit(event_types.OAI_PRESET_EXPORT_READY, preset);
3654 const presetJsonString = JSON.stringify(preset, null, 4);3685 const presetJsonString = JSON.stringify(preset, null, 4);
3655 const presetFileName = `${oai_settings.preset_settings_openai}.json`;3686 const presetFileName = `${oai_settings.preset_settings_openai}.json`;
3656 download(presetJsonString, presetFileName, 'application/json');3687 download(presetJsonString, presetFileName, 'application/json');
@@ -3791,6 +3822,7 @@ function onSettingsPresetChange() {
3791 perplexity_model: ['#model_perplexity_select', 'perplexity_model', false],3822 perplexity_model: ['#model_perplexity_select', 'perplexity_model', false],
3792 groq_model: ['#model_groq_select', 'groq_model', false],3823 groq_model: ['#model_groq_select', 'groq_model', false],
3793 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false],3824 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false],
3825 blockentropy_model: ['#model_blockentropy_select', 'blockentropy_model', false],
3794 custom_model: ['#custom_model_id', 'custom_model', false],3826 custom_model: ['#custom_model_id', 'custom_model', false],
3795 custom_url: ['#custom_api_url_text', 'custom_url', false],3827 custom_url: ['#custom_api_url_text', 'custom_url', false],
3796 custom_include_body: ['#custom_include_body', 'custom_include_body', false],3828 custom_include_body: ['#custom_include_body', 'custom_include_body', false],
@@ -3889,7 +3921,7 @@ function getMaxContextOpenAI(value) {
3889 if (oai_settings.max_context_unlocked) {3921 if (oai_settings.max_context_unlocked) {
3890 return unlocked_max;3922 return unlocked_max;
3891 }3923 }
3892 else if (value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {3924 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')) {
3893 return max_128k;3925 return max_128k;
3894 }3926 }
3895 else if (value.includes('gpt-3.5-turbo-1106')) {3927 else if (value.includes('gpt-3.5-turbo-1106')) {
@@ -4038,6 +4070,12 @@ async function onModelChange() {
4038 oai_settings.zerooneai_model = value;4070 oai_settings.zerooneai_model = value;
4039 }4071 }
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
4041 if (value && $(this).is('#model_custom_select')) {4079 if (value && $(this).is('#model_custom_select')) {
4042 console.log('Custom model changed to', value);4080 console.log('Custom model changed to', value);
4043 oai_settings.custom_model = value;4081 oai_settings.custom_model = value;
@@ -4326,6 +4364,29 @@ async function onModelChange() {
4326 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);4364 oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
4327 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');4365 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
4328 }4366 }
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
4330 $('#openai_max_context_counter').attr('max', Number($('#openai_max_context').attr('max')));4391 $('#openai_max_context_counter').attr('max', Number($('#openai_max_context').attr('max')));
43314392
@@ -4412,7 +4473,7 @@ async function onConnectButtonClick(e) {
4412 }4473 }
44134474
4414 if (!secret_state[SECRET_KEYS.MAKERSUITE] && !oai_settings.reverse_proxy) {4475 if (!secret_state[SECRET_KEYS.MAKERSUITE] && !oai_settings.reverse_proxy) {
4415 console.log('No secret key saved for MakerSuite');4476 console.log('No secret key saved for Google AI Studio');
4416 return;4477 return;
4417 }4478 }
4418 }4479 }
@@ -4533,6 +4594,18 @@ async function onConnectButtonClick(e) {
4533 return;4594 return;
4534 }4595 }
4535 }4596 }
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
4537 startStatusLoading();4610 startStatusLoading();
4538 saveSettingsDebounced();4611 saveSettingsDebounced();
@@ -4584,6 +4657,9 @@ function toggleChatCompletionForms() {
4584 else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {4657 else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
4585 $('#model_custom_select').trigger('change');4658 $('#model_custom_select').trigger('change');
4586 }4659 }
4660 else if (oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY) {
4661 $('#model_blockentropy_select').trigger('change');
4662 }
4587 $('[data-source]').each(function () {4663 $('[data-source]').each(function () {
4588 const validSources = $(this).data('source').split(',');4664 const validSources = $(this).data('source').split(',');
4589 $(this).toggle(validSources.includes(oai_settings.chat_completion_source));4665 $(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -4687,6 +4763,7 @@ export function isImageInliningSupported() {
4687 'gpt-4-turbo',4763 'gpt-4-turbo',
4688 'gpt-4o',4764 'gpt-4o',
4689 'gpt-4o-mini',4765 'gpt-4o-mini',
4766 'chatgpt-4o-latest',
4690 'yi-vision',4767 'yi-vision',
4691 ];4768 ];
46924769
@@ -5313,6 +5390,7 @@ $(document).ready(async function () {
5313 $('#model_perplexity_select').on('change', onModelChange);5390 $('#model_perplexity_select').on('change', onModelChange);
5314 $('#model_groq_select').on('change', onModelChange);5391 $('#model_groq_select').on('change', onModelChange);
5315 $('#model_01ai_select').on('change', onModelChange);5392 $('#model_01ai_select').on('change', onModelChange);
5393 $('#model_blockentropy_select').on('change', onModelChange);
5316 $('#model_custom_select').on('change', onModelChange);5394 $('#model_custom_select').on('change', onModelChange);
5317 $('#settings_preset_openai').on('change', onSettingsPresetChange);5395 $('#settings_preset_openai').on('change', onSettingsPresetChange);
5318 $('#new_oai_preset').on('click', onNewPresetClick);5396 $('#new_oai_preset').on('click', onNewPresetClick);
public/scripts/popup.js+8 -8
@@ -40,8 +40,8 @@ export const POPUP_RESULT = {
40 * @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`.40 * @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`.
41 * @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.41 * @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.
42 * @property {CustomPopupInput[]?} [customInputs=null] - Custom inputs to add to the popup. The display below the content and the input box, one by one.42 * @property {CustomPopupInput[]?} [customInputs=null] - Custom inputs to add to the popup. The display below the content and the input box, one by one.
43 * @property {(popup: Popup) => boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close43 * @property {(popup: Popup) => Promise<boolean?>|boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close
44 * @property {(popup: Popup) => void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up44 * @property {(popup: Popup) => Promise<void?>|void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up
45 * @property {number?} [cropAspect=null] - Aspect ratio for the crop popup45 * @property {number?} [cropAspect=null] - Aspect ratio for the crop popup
46 * @property {string?} [cropImage=null] - Image URL to display in the crop popup46 * @property {string?} [cropImage=null] - Image URL to display in the crop popup
47 */47 */
@@ -138,8 +138,8 @@ export class Popup {
138 /** @readonly @type {CustomPopupButton[]|string[]?} */ customButtons;138 /** @readonly @type {CustomPopupButton[]|string[]?} */ customButtons;
139 /** @readonly @type {CustomPopupInput[]} */ customInputs;139 /** @readonly @type {CustomPopupInput[]} */ customInputs;
140140
141 /** @type {(popup: Popup) => boolean?} */ onClosing;141 /** @type {(popup: Popup) => Promise<boolean?>|boolean?} */ onClosing;
142 /** @type {(popup: Popup) => void?} */ onClose;142 /** @type {(popup: Popup) => Promise<void?>|void?} */ onClose;
143143
144 /** @type {POPUP_RESULT|number} */ result;144 /** @type {POPUP_RESULT|number} */ result;
145 /** @type {any} */ value;145 /** @type {any} */ value;
@@ -509,7 +509,7 @@ export class Popup {
509 this.result = result;509 this.result = result;
510510
511 if (this.onClosing) {511 if (this.onClosing) {
512 const shouldClose = this.onClosing(this);512 const shouldClose = await this.onClosing(this);
513 if (!shouldClose) {513 if (!shouldClose) {
514 this.#isClosingPrevented = true;514 this.#isClosingPrevented = true;
515 // Set values back if we cancel out of closing the popup515 // Set values back if we cancel out of closing the popup
@@ -547,13 +547,13 @@ export class Popup {
547 fixToastrForDialogs();547 fixToastrForDialogs();
548548
549 // After the dialog is actually completely closed, remove it from the DOM549 // After the dialog is actually completely closed, remove it from the DOM
550 runAfterAnimation(this.dlg, () => {550 runAfterAnimation(this.dlg, async () => {
551 // Call the close on the dialog551 // Call the close on the dialog
552 this.dlg.close();552 this.dlg.close();
553553
554 // Run a possible custom handler right before DOM removal554 // Run a possible custom handler right before DOM removal
555 if (this.onClose) {555 if (this.onClose) {
556 this.onClose(this);556 await this.onClose(this);
557 }557 }
558558
559 // Remove it from the dom559 // Remove it from the dom
@@ -596,7 +596,7 @@ export class Popup {
596596
597 /** @returns {boolean} Checks if any modal popup dialog is open */597 /** @returns {boolean} Checks if any modal popup dialog is open */
598 isPopupOpen() {598 isPopupOpen() {
599 return Popup.util.popups.length > 0;599 return Popup.util.popups.filter(x => x.dlg.hasAttribute('open')).length > 0;
600 },600 },
601601
602 /**602 /**
public/scripts/power-user.js+58 -194
@@ -60,6 +60,7 @@ export {
60 power_user,60 power_user,
61 send_on_enter_options,61 send_on_enter_options,
62 getContextSettings,62 getContextSettings,
63 applyPowerUserSettings,
63};64};
6465
65export const MAX_CONTEXT_DEFAULT = 8192;66export const MAX_CONTEXT_DEFAULT = 8192;
@@ -202,6 +203,7 @@ let power_user = {
202 trim_spaces: true,203 trim_spaces: true,
203 relaxed_api_urls: false,204 relaxed_api_urls: false,
204 world_import_dialog: true,205 world_import_dialog: true,
206 enable_auto_select_input: false,
205 tag_import_setting: tag_import_setting.ASK,207 tag_import_setting: tag_import_setting.ASK,
206 disable_group_trimming: false,208 disable_group_trimming: false,
207 single_line: false,209 single_line: false,
@@ -300,45 +302,9 @@ let movingUIPresets = [];
300export let context_presets = [];302export let context_presets = [];
301303
302const storage_keys = {304const 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',
340 auto_connect_legacy: 'AutoConnectEnabled',305 auto_connect_legacy: 'AutoConnectEnabled',
341 auto_load_chat_legacy: 'AutoLoadChatEnabled',306 auto_load_chat_legacy: 'AutoLoadChatEnabled',
307 hideChatAvatars_legacy: 'hideChatAvatarsEnabled',
342308
343 storyStringValidationCache: 'StoryStringValidationCache',309 storyStringValidationCache: 'StoryStringValidationCache',
344};310};
@@ -458,73 +424,47 @@ function fixMarkdown(text, forDisplay) {
458}424}
459425
460function switchHotswap() {426function switchHotswap() {
461 const value = localStorage.getItem(storage_keys.hotswap_enabled);
462 power_user.hotswap_enabled = value === null ? true : value == 'true';
463 $('body').toggleClass('no-hotswap', !power_user.hotswap_enabled);427 $('body').toggleClass('no-hotswap', !power_user.hotswap_enabled);
464 $('#hotswapEnabled').prop('checked', power_user.hotswap_enabled);428 $('#hotswapEnabled').prop('checked', power_user.hotswap_enabled);
465}429}
466430
467function switchTimer() {431function switchTimer() {
468 const value = localStorage.getItem(storage_keys.timer_enabled);
469 power_user.timer_enabled = value === null ? true : value == 'true';
470 $('body').toggleClass('no-timer', !power_user.timer_enabled);432 $('body').toggleClass('no-timer', !power_user.timer_enabled);
471 $('#messageTimerEnabled').prop('checked', power_user.timer_enabled);433 $('#messageTimerEnabled').prop('checked', power_user.timer_enabled);
472}434}
473435
474function switchTimestamps() {436function switchTimestamps() {
475 const value = localStorage.getItem(storage_keys.timestamps_enabled);
476 power_user.timestamps_enabled = value === null ? true : value == 'true';
477 $('body').toggleClass('no-timestamps', !power_user.timestamps_enabled);437 $('body').toggleClass('no-timestamps', !power_user.timestamps_enabled);
478 $('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);438 $('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
479}439}
480440
481function switchIcons() {441function switchIcons() {
482 const value = localStorage.getItem(storage_keys.timestamp_model_icon);
483 power_user.timestamp_model_icon = value === null ? true : value == 'true';
484 $('body').toggleClass('no-modelIcons', !power_user.timestamp_model_icon);442 $('body').toggleClass('no-modelIcons', !power_user.timestamp_model_icon);
485 $('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);443 $('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
486}444}
487445
488function switchTokenCount() {446function switchTokenCount() {
489 const value = localStorage.getItem(storage_keys.message_token_count_enabled);
490 power_user.message_token_count_enabled = value === null ? false : value == 'true';
491 $('body').toggleClass('no-tokenCount', !power_user.message_token_count_enabled);447 $('body').toggleClass('no-tokenCount', !power_user.message_token_count_enabled);
492 $('#messageTokensEnabled').prop('checked', power_user.message_token_count_enabled);448 $('#messageTokensEnabled').prop('checked', power_user.message_token_count_enabled);
493}449}
494450
495function switchMesIDDisplay() {451function 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}`) */
502 $('body').toggleClass('no-mesIDDisplay', !power_user.mesIDDisplay_enabled);452 $('body').toggleClass('no-mesIDDisplay', !power_user.mesIDDisplay_enabled);
503 $('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);453 $('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
504}454}
505455
506function switchHideChatAvatars() {456function 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 */
513 $('body').toggleClass('hideChatAvatars', power_user.hideChatAvatars_enabled);457 $('body').toggleClass('hideChatAvatars', power_user.hideChatAvatars_enabled);
514 $('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);458 $('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
515}459}
516460
517function switchMessageActions() {461function switchMessageActions() {
518 const value = localStorage.getItem(storage_keys.expand_message_actions);
519 power_user.expand_message_actions = value === null ? false : value == 'true';
520 $('body').toggleClass('expandMessageActions', power_user.expand_message_actions);462 $('body').toggleClass('expandMessageActions', power_user.expand_message_actions);
521 $('#expandMessageActions').prop('checked', power_user.expand_message_actions);463 $('#expandMessageActions').prop('checked', power_user.expand_message_actions);
522 $('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');464 $('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
523}465}
524466
525function switchReducedMotion() {467function switchReducedMotion() {
526 const value = localStorage.getItem(storage_keys.reduced_motion);
527 power_user.reduced_motion = value === null ? false : value == 'true';
528 jQuery.fx.off = power_user.reduced_motion;468 jQuery.fx.off = power_user.reduced_motion;
529 const overrideDuration = power_user.reduced_motion ? 0 : ANIMATION_DURATION_DEFAULT;469 const overrideDuration = power_user.reduced_motion ? 0 : ANIMATION_DURATION_DEFAULT;
530 setAnimationDuration(overrideDuration);470 setAnimationDuration(overrideDuration);
@@ -533,8 +473,6 @@ function switchReducedMotion() {
533}473}
534474
535function switchCompactInputArea() {475function switchCompactInputArea() {
536 const value = localStorage.getItem(storage_keys.compact_input_area);
537 power_user.compact_input_area = value === null ? true : value == 'true';
538 $('#send_form').toggleClass('compact', power_user.compact_input_area);476 $('#send_form').toggleClass('compact', power_user.compact_input_area);
539 $('#compact_input_area').prop('checked', power_user.compact_input_area);477 $('#compact_input_area').prop('checked', power_user.compact_input_area);
540}478}
@@ -550,8 +488,6 @@ async function switchLabMode() {
550 }488 }
551 */489 */
552 await delay(100);490 await delay(100);
553 const value = localStorage.getItem(storage_keys.enableLabMode);
554 power_user.enableLabMode = value === null ? false : value == 'true';
555 $('body').toggleClass('enableLabMode', power_user.enableLabMode);491 $('body').toggleClass('enableLabMode', power_user.enableLabMode);
556 $('#enableLabMode').prop('checked', power_user.enableLabMode);492 $('#enableLabMode').prop('checked', power_user.enableLabMode);
557493
@@ -598,8 +534,6 @@ async function switchLabMode() {
598534
599async function switchZenSliders() {535async function switchZenSliders() {
600 await delay(100);536 await delay(100);
601 const value = localStorage.getItem(storage_keys.enableZenSliders);
602 power_user.enableZenSliders = value === null ? false : value == 'true';
603 $('body').toggleClass('enableZenSliders', power_user.enableZenSliders);537 $('body').toggleClass('enableZenSliders', power_user.enableZenSliders);
604 $('#enableZenSliders').prop('checked', power_user.enableZenSliders);538 $('#enableZenSliders').prop('checked', power_user.enableZenSliders);
605539
@@ -971,8 +905,6 @@ async function CreateZenSliders(elmnt) {
971 }905 }
972}906}
973function switchUiMode() {907function switchUiMode() {
974 const fastUi = localStorage.getItem(storage_keys.fast_ui_mode);
975 power_user.fast_ui_mode = fastUi === null ? true : fastUi == 'true';
976 $('body').toggleClass('no-blur', power_user.fast_ui_mode);908 $('body').toggleClass('no-blur', power_user.fast_ui_mode);
977 $('#fast_ui_mode').prop('checked', power_user.fast_ui_mode);909 $('#fast_ui_mode').prop('checked', power_user.fast_ui_mode);
978 if (power_user.fast_ui_mode) {910 if (power_user.fast_ui_mode) {
@@ -1022,8 +954,6 @@ function switchMovingUI() {
1022 $('.drawer-content.maximized').each(function () {954 $('.drawer-content.maximized').each(function () {
1023 $(this).find('.inline-drawer-maximize').trigger('click');955 $(this).find('.inline-drawer-maximize').trigger('click');
1024 });956 });
1025 const movingUI = localStorage.getItem(storage_keys.movingUI);
1026 power_user.movingUI = movingUI === null ? false : movingUI == 'true';
1027 $('body').toggleClass('movingUI', power_user.movingUI);957 $('body').toggleClass('movingUI', power_user.movingUI);
1028 if (power_user.movingUI === true) {958 if (power_user.movingUI === true) {
1029 initMovingUI();959 initMovingUI();
@@ -1039,9 +969,7 @@ function switchMovingUI() {
1039 }969 }
1040}970}
1041971
1042function noShadows() {972function applyNoShadows() {
1043 const noShadows = localStorage.getItem(storage_keys.noShadows);
1044 power_user.noShadows = noShadows === null ? false : noShadows == 'true';
1045 $('body').toggleClass('noShadows', power_user.noShadows);973 $('body').toggleClass('noShadows', power_user.noShadows);
1046 $('#noShadowsmode').prop('checked', power_user.noShadows);974 $('#noShadowsmode').prop('checked', power_user.noShadows);
1047 if (power_user.noShadows) {975 if (power_user.noShadows) {
@@ -1055,12 +983,9 @@ function noShadows() {
1055}983}
1056984
1057function applyAvatarStyle() {985function applyAvatarStyle() {
1058 power_user.avatar_style = Number(localStorage.getItem(storage_keys.avatar_style) ?? avatar_styles.ROUND);
1059 $('body').toggleClass('big-avatars', power_user.avatar_style === avatar_styles.RECTANGULAR);986 $('body').toggleClass('big-avatars', power_user.avatar_style === avatar_styles.RECTANGULAR);
1060 $('body').toggleClass('square-avatars', power_user.avatar_style === avatar_styles.SQUARE);987 $('body').toggleClass('square-avatars', power_user.avatar_style === avatar_styles.SQUARE);
1061 $('#avatar_style').val(power_user.avatar_style).prop('selected', true);988 $('#avatar_style').val(power_user.avatar_style).prop('selected', true);
1062 //$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop("checked", true);
1063
1064}989}
1065990
1066function applyChatDisplay() {991function applyChatDisplay() {
@@ -1095,8 +1020,6 @@ function applyChatDisplay() {
1095}1020}
10961021
1097function applyChatWidth(type) {1022function applyChatWidth(type) {
1098 power_user.chat_width = Number(localStorage.getItem(storage_keys.chat_width) ?? 50);
1099
1100 if (type === 'forced') {1023 if (type === 'forced') {
1101 let r = document.documentElement;1024 let r = document.documentElement;
1102 r.style.setProperty('--sheldWidth', `${power_user.chat_width}vw`);1025 r.style.setProperty('--sheldWidth', `${power_user.chat_width}vw`);
@@ -1158,8 +1081,6 @@ async function applyThemeColor(type) {
1158}1081}
11591082
1160async function applyCustomCSS() {1083async function applyCustomCSS() {
1161 power_user.custom_css = String(localStorage.getItem(storage_keys.custom_css) ?? '');
1162
1163 $('#customCSS').val(power_user.custom_css);1084 $('#customCSS').val(power_user.custom_css);
1164 var styleId = 'custom-style';1085 var styleId = 'custom-style';
1165 var style = document.getElementById(styleId);1086 var style = document.getElementById(styleId);
@@ -1173,32 +1094,26 @@ async function applyCustomCSS() {
1173}1094}
11741095
1175async function applyBlurStrength() {1096async 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);
1178 $('#blur_strength_counter').val(power_user.blur_strength);1098 $('#blur_strength_counter').val(power_user.blur_strength);
1179 $('#blur_strength').val(power_user.blur_strength);1099 $('#blur_strength').val(power_user.blur_strength);
1180
1181
1182}1100}
11831101
1184async function applyShadowWidth() {1102async 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);
1187 $('#shadow_width_counter').val(power_user.shadow_width);1104 $('#shadow_width_counter').val(power_user.shadow_width);
1188 $('#shadow_width').val(power_user.shadow_width);1105 $('#shadow_width').val(power_user.shadow_width);
11891106
1190}1107}
11911108
1192async function applyFontScale(type) {1109async function applyFontScale(type) {
1193
1194 power_user.font_scale = Number(localStorage.getItem(storage_keys.font_scale) ?? 1);
1195 //this is to allow forced setting on page load, theme swap, etc1110 //this is to allow forced setting on page load, theme swap, etc
1196 if (type === 'forced') {1111 if (type === 'forced') {
1197 document.documentElement.style.setProperty('--fontScale', power_user.font_scale);1112 document.documentElement.style.setProperty('--fontScale', String(power_user.font_scale));
1198 } else {1113 } else {
1199 //this is to prevent the slider from updating page in real time1114 //this is to prevent the slider from updating page in real time
1200 $('#font_scale').off('mouseup touchend').on('mouseup touchend', () => {1115 $('#font_scale').off('mouseup touchend').on('mouseup touchend', () => {
1201 document.documentElement.style.setProperty('--fontScale', power_user.font_scale);1116 document.documentElement.style.setProperty('--fontScale', String(power_user.font_scale));
1202 });1117 });
1203 }1118 }
12041119
@@ -1227,64 +1142,55 @@ async function applyTheme(name) {
1227 {1142 {
1228 key: 'blur_strength',1143 key: 'blur_strength',
1229 action: async () => {1144 action: async () => {
1230 localStorage.setItem(storage_keys.blur_strength, power_user.blur_strength);
1231 await applyBlurStrength();1145 await applyBlurStrength();
1232 },1146 },
1233 },1147 },
1234 {1148 {
1235 key: 'custom_css',1149 key: 'custom_css',
1236 action: async () => {1150 action: async () => {
1237 localStorage.setItem(storage_keys.custom_css, power_user.custom_css);
1238 await applyCustomCSS();1151 await applyCustomCSS();
1239 },1152 },
1240 },1153 },
1241 {1154 {
1242 key: 'shadow_width',1155 key: 'shadow_width',
1243 action: async () => {1156 action: async () => {
1244 localStorage.setItem(storage_keys.shadow_width, power_user.shadow_width);
1245 await applyShadowWidth();1157 await applyShadowWidth();
1246 },1158 },
1247 },1159 },
1248 {1160 {
1249 key: 'font_scale',1161 key: 'font_scale',
1250 action: async () => {1162 action: async () => {
1251 localStorage.setItem(storage_keys.font_scale, power_user.font_scale);
1252 await applyFontScale('forced');1163 await applyFontScale('forced');
1253 },1164 },
1254 },1165 },
1255 {1166 {
1256 key: 'fast_ui_mode',1167 key: 'fast_ui_mode',
1257 action: async () => {1168 action: async () => {
1258 localStorage.setItem(storage_keys.fast_ui_mode, power_user.fast_ui_mode);
1259 switchUiMode();1169 switchUiMode();
1260 },1170 },
1261 },1171 },
1262 {1172 {
1263 key: 'waifuMode',1173 key: 'waifuMode',
1264 action: async () => {1174 action: async () => {
1265 localStorage.setItem(storage_keys.waifuMode, power_user.waifuMode);
1266 switchWaifuMode();1175 switchWaifuMode();
1267 },1176 },
1268 },1177 },
1269 {1178 {
1270 key: 'chat_display',1179 key: 'chat_display',
1271 action: async () => {1180 action: async () => {
1272 localStorage.setItem(storage_keys.chat_display, power_user.chat_display);
1273 applyChatDisplay();1181 applyChatDisplay();
1274 },1182 },
1275 },1183 },
1276 {1184 {
1277 key: 'avatar_style',1185 key: 'avatar_style',
1278 action: async () => {1186 action: async () => {
1279 localStorage.setItem(storage_keys.avatar_style, power_user.avatar_style);
1280 applyAvatarStyle();1187 applyAvatarStyle();
1281 },1188 },
1282 },1189 },
1283 {1190 {
1284 key: 'noShadows',1191 key: 'noShadows',
1285 action: async () => {1192 action: async () => {
1286 localStorage.setItem(storage_keys.noShadows, power_user.noShadows);1193 applyNoShadows();
1287 noShadows();
1288 },1194 },
1289 },1195 },
1290 {1196 {
@@ -1294,78 +1200,66 @@ async function applyTheme(name) {
1294 if (!power_user.chat_width) {1200 if (!power_user.chat_width) {
1295 power_user.chat_width = 50;1201 power_user.chat_width = 50;
1296 }1202 }
1297
1298 localStorage.setItem(storage_keys.chat_width, String(power_user.chat_width));
1299 applyChatWidth('forced');1203 applyChatWidth('forced');
1300 },1204 },
1301 },1205 },
1302 {1206 {
1303 key: 'timer_enabled',1207 key: 'timer_enabled',
1304 action: async () => {1208 action: async () => {
1305 localStorage.setItem(storage_keys.timer_enabled, Boolean(power_user.timer_enabled));
1306 switchTimer();1209 switchTimer();
1307 },1210 },
1308 },1211 },
1309 {1212 {
1310 key: 'timestamps_enabled',1213 key: 'timestamps_enabled',
1311 action: async () => {1214 action: async () => {
1312 localStorage.setItem(storage_keys.timestamps_enabled, Boolean(power_user.timestamps_enabled));
1313 switchTimestamps();1215 switchTimestamps();
1314 },1216 },
1315 },1217 },
1316 {1218 {
1317 key: 'timestamp_model_icon',1219 key: 'timestamp_model_icon',
1318 action: async () => {1220 action: async () => {
1319 localStorage.setItem(storage_keys.timestamp_model_icon, Boolean(power_user.timestamp_model_icon));
1320 switchIcons();1221 switchIcons();
1321 },1222 },
1322 },1223 },
1323 {1224 {
1324 key: 'message_token_count_enabled',1225 key: 'message_token_count_enabled',
1325 action: async () => {1226 action: async () => {
1326 localStorage.setItem(storage_keys.message_token_count_enabled, Boolean(power_user.message_token_count_enabled));
1327 switchTokenCount();1227 switchTokenCount();
1328 },1228 },
1329 },1229 },
1330 {1230 {
1331 key: 'mesIDDisplay_enabled',1231 key: 'mesIDDisplay_enabled',
1332 action: async () => {1232 action: async () => {
1333 localStorage.setItem(storage_keys.mesIDDisplay_enabled, Boolean(power_user.mesIDDisplay_enabled));
1334 switchMesIDDisplay();1233 switchMesIDDisplay();
1335 },1234 },
1336 },1235 },
1337 {1236 {
1338 key: 'hideChatAvatars_enabled',1237 key: 'hideChatAvatars_enabled',
1339 action: async () => {1238 action: async () => {
1340 localStorage.setItem(storage_keys.hideChatAvatars_enabled, Boolean(power_user.hideChatAvatars_enabled));
1341 switchHideChatAvatars();1239 switchHideChatAvatars();
1342 },1240 },
1343 },1241 },
1344 {1242 {
1345 key: 'expand_message_actions',1243 key: 'expand_message_actions',
1346 action: async () => {1244 action: async () => {
1347 localStorage.setItem(storage_keys.expand_message_actions, Boolean(power_user.expand_message_actions));
1348 switchMessageActions();1245 switchMessageActions();
1349 },1246 },
1350 },1247 },
1351 {1248 {
1352 key: 'enableZenSliders',1249 key: 'enableZenSliders',
1353 action: async () => {1250 action: async () => {
1354 localStorage.setItem(storage_keys.enableZenSliders, Boolean(power_user.enableZenSliders));
1355 switchMessageActions();1251 switchMessageActions();
1356 },1252 },
1357 },1253 },
1358 {1254 {
1359 key: 'enableLabMode',1255 key: 'enableLabMode',
1360 action: async () => {1256 action: async () => {
1361 localStorage.setItem(storage_keys.enableLabMode, Boolean(power_user.enableLabMode));
1362 switchMessageActions();1257 switchMessageActions();
1363 },1258 },
1364 },1259 },
1365 {1260 {
1366 key: 'hotswap_enabled',1261 key: 'hotswap_enabled',
1367 action: async () => {1262 action: async () => {
1368 localStorage.setItem(storage_keys.hotswap_enabled, Boolean(power_user.hotswap_enabled));
1369 switchHotswap();1263 switchHotswap();
1370 },1264 },
1371 },1265 },
@@ -1386,7 +1280,6 @@ async function applyTheme(name) {
1386 {1280 {
1387 key: 'reduced_motion',1281 key: 'reduced_motion',
1388 action: async () => {1282 action: async () => {
1389 localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
1390 $('#reduced_motion').prop('checked', power_user.reduced_motion);1283 $('#reduced_motion').prop('checked', power_user.reduced_motion);
1391 switchReducedMotion();1284 switchReducedMotion();
1392 },1285 },
@@ -1394,7 +1287,6 @@ async function applyTheme(name) {
1394 {1287 {
1395 key: 'compact_input_area',1288 key: 'compact_input_area',
1396 action: async () => {1289 action: async () => {
1397 localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
1398 $('#compact_input_area').prop('checked', power_user.compact_input_area);1290 $('#compact_input_area').prop('checked', power_user.compact_input_area);
1399 switchCompactInputArea();1291 switchCompactInputArea();
1400 },1292 },
@@ -1449,24 +1341,26 @@ async function showDebugMenu() {
1449 callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true });1341 callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true });
1450}1342}
14511343
1452switchUiMode();1344function applyPowerUserSettings() {
1453applyFontScale('forced');1345 switchUiMode();
1454applyThemeColor();1346 applyFontScale('forced');
1455applyChatWidth('forced');1347 applyThemeColor();
1456applyAvatarStyle();1348 applyChatWidth('forced');
1457applyBlurStrength();1349 applyAvatarStyle();
1458applyShadowWidth();1350 applyBlurStrength();
1459applyCustomCSS();1351 applyShadowWidth();
1460switchMovingUI();1352 applyCustomCSS();
1461noShadows();1353 switchMovingUI();
1462switchHotswap();1354 applyNoShadows();
1463switchTimer();1355 switchHotswap();
1464switchTimestamps();1356 switchTimer();
1465switchIcons();1357 switchTimestamps();
1466switchMesIDDisplay();1358 switchIcons();
1467switchHideChatAvatars();1359 switchMesIDDisplay();
1468switchTokenCount();1360 switchHideChatAvatars();
1469switchMessageActions();1361 switchTokenCount();
1362 switchMessageActions();
1363}
14701364
1471function getExampleMessagesBehavior() {1365function getExampleMessagesBehavior() {
1472 if (power_user.strip_examples) {1366 if (power_user.strip_examples) {
@@ -1529,20 +1423,10 @@ async function loadPowerUserSettings(settings, data) {
1529 context_presets = data.context;1423 context_presets = data.context;
1530 }1424 }
15311425
1532 // These are still local storage1426 // 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);
1544 const autoLoadChat = localStorage.getItem(storage_keys.auto_load_chat_legacy);1427 const autoLoadChat = localStorage.getItem(storage_keys.auto_load_chat_legacy);
1545 const autoConnect = localStorage.getItem(storage_keys.auto_connect_legacy);1428 const autoConnect = localStorage.getItem(storage_keys.auto_connect_legacy);
1429 const hideChatAvatars = localStorage.getItem(storage_keys.hideChatAvatars_legacy);
15461430
1547 if (autoLoadChat) {1431 if (autoLoadChat) {
1548 power_user.auto_load_chat = autoLoadChat === 'true';1432 power_user.auto_load_chat = autoLoadChat === 'true';
@@ -1554,22 +1438,10 @@ async function loadPowerUserSettings(settings, data) {
1554 localStorage.removeItem(storage_keys.auto_connect_legacy);1438 localStorage.removeItem(storage_keys.auto_connect_legacy);
1555 }1439 }
15561440
1557 power_user.fast_ui_mode = fastUi === null ? true : fastUi == 'true';1441 if (hideChatAvatars) {
1558 power_user.movingUI = movingUI === null ? false : movingUI == 'true';1442 power_user.hideChatAvatars_enabled = hideChatAvatars === '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
1574 if (power_user.chat_display === '') {1446 if (power_user.chat_display === '') {
1575 power_user.chat_display = chat_styles.DEFAULT;1447 power_user.chat_display = chat_styles.DEFAULT;
@@ -1596,6 +1468,7 @@ async function loadPowerUserSettings(settings, data) {
1596 $('#single_line').prop('checked', power_user.single_line);1468 $('#single_line').prop('checked', power_user.single_line);
1597 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);1469 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
1598 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);1470 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
1471 $('#enable_auto_select_input').prop('checked', power_user.enable_auto_select_input);
1599 $('#trim_spaces').prop('checked', power_user.trim_spaces);1472 $('#trim_spaces').prop('checked', power_user.trim_spaces);
1600 $('#continue_on_send').prop('checked', power_user.continue_on_send);1473 $('#continue_on_send').prop('checked', power_user.continue_on_send);
1601 $('#quick_continue').prop('checked', power_user.quick_continue);1474 $('#quick_continue').prop('checked', power_user.quick_continue);
@@ -1655,7 +1528,7 @@ async function loadPowerUserSettings(settings, data) {
1655 $('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);1528 $('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
1656 $('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);1529 $('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
1657 $('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);1530 $('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
1658 $('#hideChatAvatarsEndabled').prop('checked', power_user.hideChatAvatars_enabled);1531 $('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
1659 $('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);1532 $('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);
1660 $('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);1533 $('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);
1661 $('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');1534 $('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');
@@ -3298,10 +3171,8 @@ $(document).ready(() => {
3298 saveSettingsDebounced();3171 saveSettingsDebounced();
3299 });3172 });
33003173
3301 // Settings that go to local storage
3302 $('#fast_ui_mode').change(function () {3174 $('#fast_ui_mode').change(function () {
3303 power_user.fast_ui_mode = $(this).prop('checked');3175 power_user.fast_ui_mode = $(this).prop('checked');
3304 localStorage.setItem(storage_keys.fast_ui_mode, power_user.fast_ui_mode);
3305 switchUiMode();3176 switchUiMode();
3306 saveSettingsDebounced();3177 saveSettingsDebounced();
3307 });3178 });
@@ -3312,24 +3183,21 @@ $(document).ready(() => {
3312 saveSettingsDebounced();3183 saveSettingsDebounced();
3313 });3184 });
33143185
3315 $('#customCSS').on('change', () => {3186 $('#customCSS').on('input', () => {
3316 power_user.custom_css = $('#customCSS').val();3187 power_user.custom_css = String($('#customCSS').val());
3317 localStorage.setItem(storage_keys.custom_css, power_user.custom_css);
3318 saveSettingsDebounced();3188 saveSettingsDebounced();
3319 applyCustomCSS();3189 applyCustomCSS();
3320 });3190 });
33213191
3322 $('#movingUImode').change(function () {3192 $('#movingUImode').change(function () {
3323 power_user.movingUI = $(this).prop('checked');3193 power_user.movingUI = $(this).prop('checked');
3324 localStorage.setItem(storage_keys.movingUI, power_user.movingUI);
3325 switchMovingUI();3194 switchMovingUI();
3326 saveSettingsDebounced();3195 saveSettingsDebounced();
3327 });3196 });
33283197
3329 $('#noShadowsmode').change(function () {3198 $('#noShadowsmode').change(function () {
3330 power_user.noShadows = $(this).prop('checked');3199 power_user.noShadows = $(this).prop('checked');
3331 localStorage.setItem(storage_keys.noShadows, power_user.noShadows);3200 applyNoShadows();
3332 noShadows();
3333 saveSettingsDebounced();3201 saveSettingsDebounced();
3334 });3202 });
33353203
@@ -3338,7 +3206,6 @@ $(document).ready(() => {
3338 $('#avatar_style').on('change', function () {3206 $('#avatar_style').on('change', function () {
3339 const value = $(this).find(':selected').val();3207 const value = $(this).find(':selected').val();
3340 power_user.avatar_style = Number(value);3208 power_user.avatar_style = Number(value);
3341 localStorage.setItem(storage_keys.avatar_style, power_user.avatar_style);
3342 applyAvatarStyle();3209 applyAvatarStyle();
3343 saveSettingsDebounced();3210 saveSettingsDebounced();
3344 });3211 });
@@ -3346,17 +3213,15 @@ $(document).ready(() => {
3346 $('#chat_display').on('change', function () {3213 $('#chat_display').on('change', function () {
3347 const value = $(this).find(':selected').val();3214 const value = $(this).find(':selected').val();
3348 power_user.chat_display = Number(value);3215 power_user.chat_display = Number(value);
3349 localStorage.setItem(storage_keys.chat_display, power_user.chat_display);
3350 applyChatDisplay();3216 applyChatDisplay();
3351 saveSettingsDebounced();3217 saveSettingsDebounced();
3352
3353 });3218 });
33543219
3355 $('#chat_width_slider').on('input', function (e, data) {3220 $('#chat_width_slider').on('input', function (e, data) {
3356 const applyMode = data?.forced ? 'forced' : 'normal';3221 const applyMode = data?.forced ? 'forced' : 'normal';
3357 power_user.chat_width = Number(e.target.value);3222 power_user.chat_width = Number(e.target.value);
3358 localStorage.setItem(storage_keys.chat_width, power_user.chat_width);
3359 applyChatWidth(applyMode);3223 applyChatWidth(applyMode);
3224 saveSettingsDebounced();
3360 setHotswapsDebounced();3225 setHotswapsDebounced();
3361 });3226 });
33623227
@@ -3386,7 +3251,6 @@ $(document).ready(() => {
3386 const applyMode = data?.forced ? 'forced' : 'normal';3251 const applyMode = data?.forced ? 'forced' : 'normal';
3387 power_user.font_scale = Number(e.target.value);3252 power_user.font_scale = Number(e.target.value);
3388 $('#font_scale_counter').val(power_user.font_scale);3253 $('#font_scale_counter').val(power_user.font_scale);
3389 localStorage.setItem(storage_keys.font_scale, power_user.font_scale);
3390 await applyFontScale(applyMode);3254 await applyFontScale(applyMode);
3391 saveSettingsDebounced();3255 saveSettingsDebounced();
3392 });3256 });
@@ -3394,7 +3258,6 @@ $(document).ready(() => {
3394 $('input[name="blur_strength"]').on('input', async function (e) {3258 $('input[name="blur_strength"]').on('input', async function (e) {
3395 power_user.blur_strength = Number(e.target.value);3259 power_user.blur_strength = Number(e.target.value);
3396 $('#blur_strength_counter').val(power_user.blur_strength);3260 $('#blur_strength_counter').val(power_user.blur_strength);
3397 localStorage.setItem(storage_keys.blur_strength, power_user.blur_strength);
3398 await applyBlurStrength();3261 await applyBlurStrength();
3399 saveSettingsDebounced();3262 saveSettingsDebounced();
3400 });3263 });
@@ -3402,7 +3265,6 @@ $(document).ready(() => {
3402 $('input[name="shadow_width"]').on('input', async function (e) {3265 $('input[name="shadow_width"]').on('input', async function (e) {
3403 power_user.shadow_width = Number(e.target.value);3266 power_user.shadow_width = Number(e.target.value);
3404 $('#shadow_width_counter').val(power_user.shadow_width);3267 $('#shadow_width_counter').val(power_user.shadow_width);
3405 localStorage.setItem(storage_keys.shadow_width, power_user.shadow_width);
3406 await applyShadowWidth();3268 await applyShadowWidth();
3407 saveSettingsDebounced();3269 saveSettingsDebounced();
3408 });3270 });
@@ -3643,36 +3505,36 @@ $(document).ready(() => {
3643 $('#messageTimerEnabled').on('input', function () {3505 $('#messageTimerEnabled').on('input', function () {
3644 const value = !!$(this).prop('checked');3506 const value = !!$(this).prop('checked');
3645 power_user.timer_enabled = value;3507 power_user.timer_enabled = value;
3646 localStorage.setItem(storage_keys.timer_enabled, Boolean(power_user.timer_enabled));
3647 switchTimer();3508 switchTimer();
3509 saveSettingsDebounced();
3648 });3510 });
36493511
3650 $('#messageTimestampsEnabled').on('input', function () {3512 $('#messageTimestampsEnabled').on('input', function () {
3651 const value = !!$(this).prop('checked');3513 const value = !!$(this).prop('checked');
3652 power_user.timestamps_enabled = value;3514 power_user.timestamps_enabled = value;
3653 localStorage.setItem(storage_keys.timestamps_enabled, Boolean(power_user.timestamps_enabled));
3654 switchTimestamps();3515 switchTimestamps();
3516 saveSettingsDebounced();
3655 });3517 });
36563518
3657 $('#messageModelIconEnabled').on('input', function () {3519 $('#messageModelIconEnabled').on('input', function () {
3658 const value = !!$(this).prop('checked');3520 const value = !!$(this).prop('checked');
3659 power_user.timestamp_model_icon = value;3521 power_user.timestamp_model_icon = value;
3660 localStorage.setItem(storage_keys.timestamp_model_icon, Boolean(power_user.timestamp_model_icon));
3661 switchIcons();3522 switchIcons();
3523 saveSettingsDebounced();
3662 });3524 });
36633525
3664 $('#messageTokensEnabled').on('input', function () {3526 $('#messageTokensEnabled').on('input', function () {
3665 const value = !!$(this).prop('checked');3527 const value = !!$(this).prop('checked');
3666 power_user.message_token_count_enabled = value;3528 power_user.message_token_count_enabled = value;
3667 localStorage.setItem(storage_keys.message_token_count_enabled, Boolean(power_user.message_token_count_enabled));
3668 switchTokenCount();3529 switchTokenCount();
3530 saveSettingsDebounced();
3669 });3531 });
36703532
3671 $('#expandMessageActions').on('input', function () {3533 $('#expandMessageActions').on('input', function () {
3672 const value = !!$(this).prop('checked');3534 const value = !!$(this).prop('checked');
3673 power_user.expand_message_actions = value;3535 power_user.expand_message_actions = value;
3674 localStorage.setItem(storage_keys.expand_message_actions, Boolean(power_user.expand_message_actions));
3675 switchMessageActions();3536 switchMessageActions();
3537 saveSettingsDebounced();
3676 });3538 });
36773539
3678 $('#enableZenSliders').on('input', function () {3540 $('#enableZenSliders').on('input', function () {
@@ -3684,9 +3546,8 @@ $(document).ready(() => {
3684 return;3546 return;
3685 }3547 }
3686 power_user.enableZenSliders = value;3548 power_user.enableZenSliders = value;
3687 localStorage.setItem(storage_keys.enableZenSliders, Boolean(power_user.enableZenSliders));
3688 saveSettingsDebounced();
3689 switchZenSliders();3549 switchZenSliders();
3550 saveSettingsDebounced();
3690 });3551 });
36913552
3692 $('#enableLabMode').on('input', function () {3553 $('#enableLabMode').on('input', function () {
@@ -3699,30 +3560,29 @@ $(document).ready(() => {
3699 }3560 }
37003561
3701 power_user.enableLabMode = value;3562 power_user.enableLabMode = value;
3702 localStorage.setItem(storage_keys.enableLabMode, Boolean(power_user.enableLabMode));
3703 saveSettingsDebounced();
3704 switchLabMode();3563 switchLabMode();
3564 saveSettingsDebounced();
3705 });3565 });
37063566
3707 $('#mesIDDisplayEnabled').on('input', function () {3567 $('#mesIDDisplayEnabled').on('input', function () {
3708 const value = !!$(this).prop('checked');3568 const value = !!$(this).prop('checked');
3709 power_user.mesIDDisplay_enabled = value;3569 power_user.mesIDDisplay_enabled = value;
3710 localStorage.setItem(storage_keys.mesIDDisplay_enabled, Boolean(power_user.mesIDDisplay_enabled));
3711 switchMesIDDisplay();3570 switchMesIDDisplay();
3571 saveSettingsDebounced();
3712 });3572 });
37133573
3714 $('#hideChatAvatarsEnabled').on('input', function () {3574 $('#hideChatAvatarsEnabled').on('input', function () {
3715 const value = !!$(this).prop('checked');3575 const value = !!$(this).prop('checked');
3716 power_user.hideChatAvatars_enabled = value;3576 power_user.hideChatAvatars_enabled = value;
3717 localStorage.setItem(storage_keys.hideChatAvatars_enabled, Boolean(power_user.hideChatAvatars_enabled));
3718 switchHideChatAvatars();3577 switchHideChatAvatars();
3578 saveSettingsDebounced();
3719 });3579 });
37203580
3721 $('#hotswapEnabled').on('input', function () {3581 $('#hotswapEnabled').on('input', function () {
3722 const value = !!$(this).prop('checked');3582 const value = !!$(this).prop('checked');
3723 power_user.hotswap_enabled = value;3583 power_user.hotswap_enabled = value;
3724 localStorage.setItem(storage_keys.hotswap_enabled, Boolean(power_user.hotswap_enabled));
3725 switchHotswap();3584 switchHotswap();
3585 saveSettingsDebounced();
3726 });3586 });
37273587
3728 $('#prefer_character_prompt').on('input', function () {3588 $('#prefer_character_prompt').on('input', function () {
@@ -3775,6 +3635,12 @@ $(document).ready(() => {
3775 saveSettingsDebounced();3635 saveSettingsDebounced();
3776 });3636 });
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
3778 $('#spoiler_free_mode').on('input', function () {3644 $('#spoiler_free_mode').on('input', function () {
3779 power_user.spoiler_free_mode = !!$(this).prop('checked');3645 power_user.spoiler_free_mode = !!$(this).prop('checked');
3780 switchSpoilerMode();3646 switchSpoilerMode();
@@ -3824,8 +3690,8 @@ $(document).ready(() => {
3824 $('#ui_mode_select').on('change', function () {3690 $('#ui_mode_select').on('change', function () {
3825 const value = $(this).find(':selected').val();3691 const value = $(this).find(':selected').val();
3826 power_user.ui_mode = Number(value);3692 power_user.ui_mode = Number(value);
3827 saveSettingsDebounced();
3828 switchSimpleMode();3693 switchSimpleMode();
3694 saveSettingsDebounced();
3829 });3695 });
38303696
3831 $('#bogus_folders').on('input', function () {3697 $('#bogus_folders').on('input', function () {
@@ -3929,14 +3795,12 @@ $(document).ready(() => {
39293795
3930 $('#reduced_motion').on('input', function () {3796 $('#reduced_motion').on('input', function () {
3931 power_user.reduced_motion = !!$(this).prop('checked');3797 power_user.reduced_motion = !!$(this).prop('checked');
3932 localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
3933 switchReducedMotion();3798 switchReducedMotion();
3934 saveSettingsDebounced();3799 saveSettingsDebounced();
3935 });3800 });
39363801
3937 $('#compact_input_area').on('input', function () {3802 $('#compact_input_area').on('input', function () {
3938 power_user.compact_input_area = !!$(this).prop('checked');3803 power_user.compact_input_area = !!$(this).prop('checked');
3939 localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
3940 switchCompactInputArea();3804 switchCompactInputArea();
3941 saveSettingsDebounced();3805 saveSettingsDebounced();
3942 });3806 });
public/scripts/secrets.js+4 -1
@@ -32,6 +32,8 @@ export const SECRET_KEYS = {
32 ZEROONEAI: 'api_key_01ai',32 ZEROONEAI: 'api_key_01ai',
33 HUGGINGFACE: 'api_key_huggingface',33 HUGGINGFACE: 'api_key_huggingface',
34 STABILITY: 'api_key_stability',34 STABILITY: 'api_key_stability',
35 BLOCKENTROPY: 'api_key_blockentropy',
36 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
35};37};
3638
37const INPUT_MAP = {39const INPUT_MAP = {
@@ -63,6 +65,7 @@ const INPUT_MAP = {
63 [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',65 [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',
64 [SECRET_KEYS.ZEROONEAI]: '#api_key_01ai',66 [SECRET_KEYS.ZEROONEAI]: '#api_key_01ai',
65 [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',67 [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
68 [SECRET_KEYS.BLOCKENTROPY]: '#api_key_blockentropy',
66};69};
6770
68async function clearSecret() {71async function clearSecret() {
@@ -125,7 +128,7 @@ export async function writeSecret(key, value) {
125 const text = await response.text();128 const text = await response.text();
126129
127 if (text == 'ok') {130 if (text == 'ok') {
128 secret_state[key] = true;131 secret_state[key] = !!value;
129 updateSecretDisplay();132 updateSecretDisplay();
130 }133 }
131 }134 }
public/scripts/showdown-underscore.js+13 -3
@@ -7,9 +7,19 @@ export const markdownUnderscoreExt = () => {
7 }7 }
88
9 return [{9 return [{
10 type: 'lang',10 type: 'output',
11 regex: new RegExp('\\b(?<!_)_(?!_)(.*?)(?<!_)_(?!_)\\b', 'g'),11 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 },
13 }];23 }];
14 } catch (e) {24 } catch (e) {
15 console.error('Error in Showdown-underscore extension:', e);25 console.error('Error in Showdown-underscore extension:', e);
public/scripts/slash-commands.js+253 -38
@@ -1,7 +1,9 @@
1import {1import {
2 Generate,2 Generate,
3 UNIQUE_APIS,
3 activateSendButtons,4 activateSendButtons,
4 addOneMessage,5 addOneMessage,
6 api_server,
5 callPopup,7 callPopup,
6 characters,8 characters,
7 chat,9 chat,
@@ -49,8 +51,8 @@ import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSel
49import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';51import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
50import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';52import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
51import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';53import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
52import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';54import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
53import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';55import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
54import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';56import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
55import { registerVariableCommands, resolveVariable } from './variables.js';57import { registerVariableCommands, resolveVariable } from './variables.js';
56import { background_settings } from './backgrounds.js';58import { background_settings } from './backgrounds.js';
@@ -717,6 +719,7 @@ export function initDefaultSlashCommands() {
717 SlashCommandParser.addCommandObject(SlashCommand.fromProps({719 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
718 name: 'delswipe',720 name: 'delswipe',
719 callback: deleteSwipeCallback,721 callback: deleteSwipeCallback,
722 returns: 'the new, currently selected swipe id',
720 aliases: ['swipedel'],723 aliases: ['swipedel'],
721 unnamedArgumentList: [724 unnamedArgumentList: [
722 SlashCommandArgument.fromProps({725 SlashCommandArgument.fromProps({
@@ -912,13 +915,28 @@ export function initDefaultSlashCommands() {
912 SlashCommandParser.addCommandObject(SlashCommand.fromProps({915 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
913 name: 'addswipe',916 name: 'addswipe',
914 callback: addSwipeCallback,917 callback: addSwipeCallback,
918 returns: 'the new swipe id',
915 aliases: ['swipeadd'],919 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 ],
916 unnamedArgumentList: [928 unnamedArgumentList: [
917 new SlashCommandArgument(929 new SlashCommandArgument(
918 'text', [ARGUMENT_TYPE.STRING], true,930 'text', [ARGUMENT_TYPE.STRING], true,
919 ),931 ),
920 ],932 ],
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>`,
922 }));940 }));
923 SlashCommandParser.addCommandObject(SlashCommand.fromProps({941 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
924 name: 'stop',942 name: 'stop',
@@ -1480,8 +1498,9 @@ export function initDefaultSlashCommands() {
1480 ],1498 ],
1481 helpString: 'Sets the specified prompt manager entry/entries on or off.',1499 helpString: 'Sets the specified prompt manager entry/entries on or off.',
1482 }));1500 }));
1483 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',1501 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1484 callback: async()=>((await showFontAwesomePicker()) ?? false).toString(),1502 name: 'pick-icon',
1503 callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
1485 returns: 'The chosen icon name or false if cancelled.',1504 returns: 'The chosen icon name or false if cancelled.',
1486 helpString: `1505 helpString: `
1487 <div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>1506 <div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>
@@ -1495,6 +1514,72 @@ export function initDefaultSlashCommands() {
1495 </div>1514 </div>
1496 `,1515 `,
1497 }));1516 }));
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
1499 registerVariableCommands();1584 registerVariableCommands();
1500}1585}
@@ -1772,7 +1857,7 @@ async function popupCallback(args, value) {
1772 return String(value);1857 return String(value);
1773}1858}
17741859
1775function getMessagesCallback(args, value) {1860async function getMessagesCallback(args, value) {
1776 const includeNames = !isFalseBoolean(args?.names);1861 const includeNames = !isFalseBoolean(args?.names);
1777 const includeHidden = isTrueBoolean(args?.hidden);1862 const includeHidden = isTrueBoolean(args?.hidden);
1778 const role = args?.role;1863 const role = args?.role;
@@ -1805,33 +1890,34 @@ function getMessagesCallback(args, value) {
1805 throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);1890 throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
1806 };1891 };
18071892
1808 const messages = [];1893 const processMessage = async (mesId) => {
18091894 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;
1815 }1898 }
18161899
1817 if (role && !filterByRole(message)) {1900 if (role && !filterByRole(msg)) {
1818 console.debug(`/messages: Skipping message with ID ${messageId} due to role filter`);1901 console.debug(`/messages: Skipping message with ID ${mesId} due to role filter`);
1819 continue;1902 return null;
1820 }1903 }
18211904
1822 if (!includeHidden && message.is_system) {1905 if (!includeHidden && msg.is_system) {
1823 console.debug(`/messages: Skipping hidden message with ID ${messageId}`);1906 console.debug(`/messages: Skipping hidden message with ID ${mesId}`);
1824 continue;1907 return null;
1825 }1908 }
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');
1835}1921}
18361922
1837async function runCallback(args, name) {1923async function runCallback(args, name) {
@@ -2061,12 +2147,13 @@ async function generateRawCallback(args, value) {
2061 }2147 }
2062}2148}
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 */
2064async function generateCallback(args, value) {2156async function generateCallback(args, value) {
2065 if (!value) {
2066 console.warn('WARN: No argument provided for /gen command');
2067 return;
2068 }
2069
2070 // Prevent generate recursion2157 // Prevent generate recursion
2071 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));2158 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
2072 const lock = isTrueBoolean(args?.lock);2159 const lock = isTrueBoolean(args?.lock);
@@ -2154,8 +2241,11 @@ async function echoCallback(args, value) {
2154 }2241 }
2155}2242}
21562243
21572244/**
2158async function addSwipeCallback(_, arg) {2245 * @param {{switch?: string}} args - named arguments
2246 * @param {string} value - The swipe text to add (unnamed argument)
2247 */
2248async function addSwipeCallback(args, value) {
2159 const lastMessage = chat[chat.length - 1];2249 const lastMessage = chat[chat.length - 1];
21602250
2161 if (!lastMessage) {2251 if (!lastMessage) {
@@ -2163,7 +2253,7 @@ async function addSwipeCallback(_, arg) {
2163 return '';2253 return '';
2164 }2254 }
21652255
2166 if (!arg) {2256 if (!value) {
2167 console.warn('WARN: No argument provided for /addswipe command');2257 console.warn('WARN: No argument provided for /addswipe command');
2168 return '';2258 return '';
2169 }2259 }
@@ -2192,23 +2282,30 @@ async function addSwipeCallback(_, arg) {
2192 lastMessage.swipe_info = lastMessage.swipes.map(() => ({}));2282 lastMessage.swipe_info = lastMessage.swipes.map(() => ({}));
2193 }2283 }
21942284
2195 lastMessage.swipes.push(arg);2285 lastMessage.swipes.push(value);
2196 lastMessage.swipe_info.push({2286 lastMessage.swipe_info.push({
2197 send_date: getMessageTimeStamp(),2287 send_date: getMessageTimeStamp(),
2198 gen_started: null,2288 gen_started: null,
2199 gen_finished: null,2289 gen_finished: null,
2200 extra: {2290 extra: {
2201 bias: extractMessageBias(arg),2291 bias: extractMessageBias(value),
2202 gen_id: Date.now(),2292 gen_id: Date.now(),
2203 api: 'manual',2293 api: 'manual',
2204 model: 'slash command',2294 model: 'slash command',
2205 },2295 },
2206 });2296 });
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
2208 await saveChatConditional();2305 await saveChatConditional();
2209 await reloadCurrentChat();2306 await reloadCurrentChat();
22102307
2211 return '';2308 return String(newSwipeId);
2212}2309}
22132310
2214async function deleteSwipeCallback(_, arg) {2311async function deleteSwipeCallback(_, arg) {
@@ -2244,7 +2341,7 @@ async function deleteSwipeCallback(_, arg) {
2244 await saveChatConditional();2341 await saveChatConditional();
2245 await reloadCurrentChat();2342 await reloadCurrentChat();
22462343
2247 return '';2344 return String(newSwipeId);
2248}2345}
22492346
2250async function askCharacter(args, text) {2347async function askCharacter(args, text) {
@@ -3223,6 +3320,7 @@ function getModelOptions() {
3223 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },3320 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
3224 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },3321 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
3225 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },3322 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
3323 { id: 'model_blockentropy_select', api: 'openai', type: chat_completion_sources.BLOCKENTROPY },
3226 { id: 'model_novel_select', api: 'novel', type: null },3324 { id: 'model_novel_select', api: 'novel', type: null },
3227 { id: 'horde_model', api: 'koboldhorde', type: null },3325 { id: 'horde_model', api: 'koboldhorde', type: null },
3228 ];3326 ];
@@ -3391,6 +3489,123 @@ function setPromptEntryCallback(args, targetState) {
3391 return '';3489 return '';
3392}3490}
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 */
3501async 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
3588async 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
3394export let isExecutingCommandsFromChatInput = false;3609export let isExecutingCommandsFromChatInput = false;
3395export let commandsFromChatInputAbortController;3610export let commandsFromChatInputAbortController;
33963611
public/scripts/tags.js+18 -10
@@ -445,7 +445,11 @@ export function getTagKeyForEntity(entityOrKey) {
445 }445 }
446446
447 // Next lets check if its a valid character or character id, so we can swith it to its tag447 // 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
449 if (character) {453 if (character) {
450 x = character.avatar;454 x = character.avatar;
451 }455 }
@@ -708,12 +712,12 @@ const ANTI_TROLL_MAX_TAGS = 15;
708 *712 *
709 * @param {Character} character - The character713 * @param {Character} character - The character
710 * @param {object} [options] - Options714 * @param {object} [options] - Options
711 * @param {boolean} [options.forceShow=false] - Whether to force showing the import dialog715 * @param {tag_import_setting} [options.importSetting=null] - Force a tag import setting
712 * @returns {Promise<boolean>} Boolean indicating whether any tag was imported716 * @returns {Promise<boolean>} Boolean indicating whether any tag was imported
713 */717 */
714async function importTags(character, { forceShow = false } = {}) {718async function importTags(character, { importSetting = null } = {}) {
715 // Gather the tags to import based on the selected setting719 // Gather the tags to import based on the selected setting
716 const tagNamesToImport = await handleTagImport(character, { forceShow });720 const tagNamesToImport = await handleTagImport(character, { importSetting });
717 if (!tagNamesToImport?.length) {721 if (!tagNamesToImport?.length) {
718 console.debug('No tags to import');722 console.debug('No tags to import');
719 return;723 return;
@@ -722,7 +726,11 @@ async function importTags(character, { forceShow = false } = {}) {
722 const tagsToImport = tagNamesToImport.map(tag => getTag(tag, { createNew: true }));726 const tagsToImport = tagNamesToImport.map(tag => getTag(tag, { createNew: true }));
723 const added = addTagsToEntity(tagsToImport, character.avatar);727 const added = addTagsToEntity(tagsToImport, character.avatar);
724728
725 toastr.success(`Imported tags:<br />${tagsToImport.map(x => x.name).join(', ')}`, 'Importing Tags', { escapeHtml: false });729 if (added) {
730 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
727 return added;735 return added;
728}736}
@@ -732,10 +740,10 @@ async function importTags(character, { forceShow = false } = {}) {
732 *740 *
733 * @param {Character} character - The character741 * @param {Character} character - The character
734 * @param {object} [options] - Options742 * @param {object} [options] - Options
735 * @param {boolean} [options.forceShow=false] - Whether to force showing the import dialog743 * @param {tag_import_setting} [options.importSetting=null] - Force a tag import setting
736 * @returns {Promise<string[]>} Array of strings representing the tags to import744 * @returns {Promise<string[]>} Array of strings representing the tags to import
737 */745 */
738async function handleTagImport(character, { forceShow = false } = {}) {746async function handleTagImport(character, { importSetting = null } = {}) {
739 /** @type {string[]} */747 /** @type {string[]} */
740 const importTags = character.tags.map(t => t.trim()).filter(t => t)748 const importTags = character.tags.map(t => t.trim()).filter(t => t)
741 .filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))749 .filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))
@@ -745,9 +753,9 @@ async function handleTagImport(character, { forceShow = false } = {}) {
745 .map(newTag);753 .map(newTag);
746 const folderTags = getOpenBogusFolders();754 const folderTags = getOpenBogusFolders();
747755
748 // Choose the setting for this dialog. If from settings, verify the setting really exists, otherwise take "ASK".756 // Choose the setting for this dialog. First check override, then saved setting or finally use "ASK".
749 const setting = forceShow ? tag_import_setting.ASK757 const setting = importSetting ? importSetting :
750 : Object.values(tag_import_setting).find(setting => setting === power_user.tag_import_setting) ?? tag_import_setting.ASK;758 Object.values(tag_import_setting).find(setting => setting === power_user.tag_import_setting) ?? tag_import_setting.ASK;
751759
752 switch (setting) {760 switch (setting) {
753 case tag_import_setting.ALL:761 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() {
599 return tokenizers.YI;599 return tokenizers.YI;
600 case 'Mistral':600 case 'Mistral':
601 return tokenizers.MISTRAL;601 return tokenizers.MISTRAL;
602 case 'Gemini':
603 return tokenizers.GEMMA;
604 case 'Claude':
605 return tokenizers.CLAUDE;
602 default:606 default:
603 return tokenizers.OPENAI;607 return tokenizers.OPENAI;
604 }608 }
@@ -618,7 +622,7 @@ export function getCurrentDreamGenModelTokenizer() {
618 }622 }
619}623}
620624
621jQuery(function () {625export function initTextGenModels() {
622 $('#mancer_model').on('change', onMancerModelSelect);626 $('#mancer_model').on('change', onMancerModelSelect);
623 $('#model_togetherai_select').on('change', onTogetherModelSelect);627 $('#model_togetherai_select').on('change', onTogetherModelSelect);
624 $('#model_infermaticai_select').on('change', onInfermaticAIModelSelect);628 $('#model_infermaticai_select').on('change', onInfermaticAIModelSelect);
@@ -708,6 +712,7 @@ jQuery(function () {
708 searchInputPlaceholder: 'Search providers...',712 searchInputPlaceholder: 'Search providers...',
709 searchInputCssClass: 'text_pole',713 searchInputCssClass: 'text_pole',
710 width: '100%',714 width: '100%',
715 closeOnSelect: false,
711 });716 });
712 providersSelect.on('select2:select', function (/** @type {any} */ evt) {717 providersSelect.on('select2:select', function (/** @type {any} */ evt) {
713 const element = evt.params.data.element;718 const element = evt.params.data.element;
@@ -718,4 +723,4 @@ jQuery(function () {
718 $(this).trigger('change');723 $(this).trigger('change');
719 });724 });
720 }725 }
721});726}
public/scripts/textgen-settings.js+30 -2
@@ -94,7 +94,7 @@ let DREAMGEN_SERVER = 'https://dreamgen.com';
94let OPENROUTER_SERVER = 'https://openrouter.ai/api';94let OPENROUTER_SERVER = 'https://openrouter.ai/api';
95let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';95let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';
9696
97const SERVER_INPUTS = {97export const SERVER_INPUTS = {
98 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',98 [textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
99 [textgen_types.VLLM]: '#vllm_api_url_text',99 [textgen_types.VLLM]: '#vllm_api_url_text',
100 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',100 [textgen_types.APHRODITE]: '#aphrodite_api_url_text',
@@ -1064,6 +1064,34 @@ function getLogprobsNumber() {
1064 return 10;1064 return 10;
1065}1065}
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 */
1072function 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
1067export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {1095export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
1068 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';1096 const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
1069 const dynatemp = isDynamicTemperatureSupported();1097 const dynatemp = isDynamicTemperatureSupported();
@@ -1103,7 +1131,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
1103 'dry_allowed_length': settings.dry_allowed_length,1131 'dry_allowed_length': settings.dry_allowed_length,
1104 'dry_multiplier': settings.dry_multiplier,1132 'dry_multiplier': settings.dry_multiplier,
1105 'dry_base': settings.dry_base,1133 'dry_base': settings.dry_base,
1106 'dry_sequence_breakers': settings.dry_sequence_breakers,1134 'dry_sequence_breakers': replaceMacrosInList(settings.dry_sequence_breakers),
1107 'dry_penalty_last_n': settings.dry_penalty_last_n,1135 'dry_penalty_last_n': settings.dry_penalty_last_n,
1108 'max_tokens_second': settings.max_tokens_second,1136 'max_tokens_second': settings.max_tokens_second,
1109 'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined,1137 'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined,
public/scripts/tokenizers.js+72 -3
@@ -26,6 +26,7 @@ export const tokenizers = {
26 API_KOBOLD: 10,26 API_KOBOLD: 10,
27 CLAUDE: 11,27 CLAUDE: 11,
28 LLAMA3: 12,28 LLAMA3: 12,
29 GEMMA: 13,
29 BEST_MATCH: 99,30 BEST_MATCH: 99,
30};31};
3132
@@ -34,6 +35,7 @@ export const SENTENCEPIECE_TOKENIZERS = [
34 tokenizers.MISTRAL,35 tokenizers.MISTRAL,
35 tokenizers.YI,36 tokenizers.YI,
36 tokenizers.LLAMA3,37 tokenizers.LLAMA3,
38 tokenizers.GEMMA,
37 // uncomment when NovelAI releases Kayra and Clio weights, lol39 // uncomment when NovelAI releases Kayra and Clio weights, lol
38 //tokenizers.NERD,40 //tokenizers.NERD,
39 //tokenizers.NERD2,41 //tokenizers.NERD2,
@@ -91,6 +93,11 @@ const TOKENIZER_URLS = {
91 decode: '/api/tokenizers/llama3/decode',93 decode: '/api/tokenizers/llama3/decode',
92 count: '/api/tokenizers/llama3/encode',94 count: '/api/tokenizers/llama3/encode',
93 },95 },
96 [tokenizers.GEMMA]: {
97 encode: '/api/tokenizers/gemma/encode',
98 decode: '/api/tokenizers/gemma/decode',
99 count: '/api/tokenizers/gemma/encode',
100 },
94 [tokenizers.API_TEXTGENERATIONWEBUI]: {101 [tokenizers.API_TEXTGENERATIONWEBUI]: {
95 encode: '/api/tokenizers/remote/textgenerationwebui/encode',102 encode: '/api/tokenizers/remote/textgenerationwebui/encode',
96 count: '/api/tokenizers/remote/textgenerationwebui/encode',103 count: '/api/tokenizers/remote/textgenerationwebui/encode',
@@ -141,9 +148,45 @@ async function resetTokenCache() {
141}148}
142149
143/**150/**
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 */
161export 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 */
174export 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/**
144 * Gets the friendly name of the current tokenizer.187 * Gets the friendly name of the current tokenizer.
145 * @param {string} forApi API to get the tokenizer for. Defaults to the main API.188 * @param {string} forApi API to get the tokenizer for. Defaults to the main API.
146 * @returns { { tokenizerName: string, tokenizerId: number } } Tokenizer info189 * @returns {Tokenizer} Tokenizer info
147 */190 */
148export function getFriendlyTokenizerName(forApi) {191export function getFriendlyTokenizerName(forApi) {
149 if (!forApi) {192 if (!forApi) {
@@ -178,7 +221,9 @@ export function getFriendlyTokenizerName(forApi) {
178 ? tokenizers.OPENAI221 ? tokenizers.OPENAI
179 : tokenizerId;222 : tokenizerId;
180223
181 return { tokenizerName, tokenizerId };224 const tokenizerKey = Object.entries(tokenizers).find(([_, value]) => value === tokenizerId)[0].toLocaleLowerCase();
225
226 return { tokenizerName, tokenizerKey, tokenizerId };
182}227}
183228
184/**229/**
@@ -232,6 +277,9 @@ export function getTokenizerBestMatch(forApi) {
232 if (model.includes('mistral') || model.includes('mixtral')) {277 if (model.includes('mistral') || model.includes('mixtral')) {
233 return tokenizers.MISTRAL;278 return tokenizers.MISTRAL;
234 }279 }
280 if (model.includes('gemma')) {
281 return tokenizers.GEMMA;
282 }
235 }283 }
236284
237 return tokenizers.LLAMA;285 return tokenizers.LLAMA;
@@ -441,12 +489,14 @@ export function getTokenizerModel() {
441 const turbo0301Tokenizer = 'gpt-3.5-turbo-0301';489 const turbo0301Tokenizer = 'gpt-3.5-turbo-0301';
442 const turboTokenizer = 'gpt-3.5-turbo';490 const turboTokenizer = 'gpt-3.5-turbo';
443 const gpt4Tokenizer = 'gpt-4';491 const gpt4Tokenizer = 'gpt-4';
492 const gpt4oTokenizer = 'gpt-4o';
444 const gpt2Tokenizer = 'gpt2';493 const gpt2Tokenizer = 'gpt2';
445 const claudeTokenizer = 'claude';494 const claudeTokenizer = 'claude';
446 const llamaTokenizer = 'llama';495 const llamaTokenizer = 'llama';
447 const llama3Tokenizer = 'llama3';496 const llama3Tokenizer = 'llama3';
448 const mistralTokenizer = 'mistral';497 const mistralTokenizer = 'mistral';
449 const yiTokenizer = 'yi';498 const yiTokenizer = 'yi';
499 const gemmaTokenizer = 'gemma';
450500
451 // Assuming no one would use it for different models.. right?501 // Assuming no one would use it for different models.. right?
452 if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {502 if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
@@ -491,6 +541,12 @@ export function getTokenizerModel() {
491 else if (model?.architecture?.tokenizer === 'Yi') {541 else if (model?.architecture?.tokenizer === 'Yi') {
492 return yiTokenizer;542 return yiTokenizer;
493 }543 }
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 }
494 else if (oai_settings.openrouter_model.includes('gpt-4')) {550 else if (oai_settings.openrouter_model.includes('gpt-4')) {
495 return gpt4Tokenizer;551 return gpt4Tokenizer;
496 }552 }
@@ -509,7 +565,7 @@ export function getTokenizerModel() {
509 }565 }
510566
511 if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {567 if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
512 return oai_settings.google_model;568 return gemmaTokenizer;
513 }569 }
514570
515 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {571 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
@@ -543,12 +599,24 @@ export function getTokenizerModel() {
543 if (oai_settings.groq_model.includes('mistral') || oai_settings.groq_model.includes('mixtral')) {599 if (oai_settings.groq_model.includes('mistral') || oai_settings.groq_model.includes('mixtral')) {
544 return mistralTokenizer;600 return mistralTokenizer;
545 }601 }
602 if (oai_settings.groq_model.includes('gemma')) {
603 return gemmaTokenizer;
604 }
546 }605 }
547606
548 if (oai_settings.chat_completion_source === chat_completion_sources.ZEROONEAI) {607 if (oai_settings.chat_completion_source === chat_completion_sources.ZEROONEAI) {
549 return yiTokenizer;608 return yiTokenizer;
550 }609 }
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
552 // Default to Turbo 3.5620 // Default to Turbo 3.5
553 return turboTokenizer;621 return turboTokenizer;
554}622}
@@ -770,6 +838,7 @@ function getTextgenAPITokenizationParams(str) {
770 url: getTextGenServer(),838 url: getTextGenServer(),
771 legacy_api: textgen_settings.legacy_api && (textgen_settings.type === OOBA || textgen_settings.type === APHRODITE),839 legacy_api: textgen_settings.legacy_api && (textgen_settings.type === OOBA || textgen_settings.type === APHRODITE),
772 vllm_model: textgen_settings.vllm_model,840 vllm_model: textgen_settings.vllm_model,
841 aphrodite_model: textgen_settings.aphrodite_model,
773 };842 };
774}843}
775844
public/scripts/utils.js+38 -7
@@ -498,9 +498,8 @@ export function restoreCaretPosition(element, position) {
498}498}
499499
500export async function resetScrollHeight(element) {500export async function resetScrollHeight(element) {
501 let scrollHeight = $(element).prop('scrollHeight');
502 $(element).css('height', '0px');501 $(element).css('height', '0px');
503 $(element).css('height', scrollHeight + 3 + 'px');502 $(element).css('height', $(element).prop('scrollHeight') + 3 + 'px');
504}503}
505504
506/**505/**
@@ -1729,20 +1728,24 @@ export function select2ModifyOptions(element, items, { select = false, changeEve
1729 /** @type {Select2Option[]} */1728 /** @type {Select2Option[]} */
1730 const dataItems = items.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);1729 const dataItems = items.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
17311730
1732 const existingValues = [];1731 const optionsToSelect = [];
1732 const newOptions = [];
1733
1733 dataItems.forEach(item => {1734 dataItems.forEach(item => {
1734 // Set the value, creating a new option if necessary1735 // Set the value, creating a new option if necessary
1735 if (element.find('option[value=\'' + item.id + '\']').length) {1736 if (element.find('option[value=\'' + item.id + '\']').length) {
1736 if (select) existingValues.push(item.id);1737 if (select) optionsToSelect.push(item.id);
1737 } else {1738 } else {
1738 // Create a DOM Option and optionally pre-select by default1739 // Create a DOM Option and optionally pre-select by default
1739 var newOption = new Option(item.text, item.id, select, select);1740 var newOption = new Option(item.text, item.id, select, select);
1740 // Append it to the select1741 // Append it to the select
1741 element.append(newOption);1742 newOptions.push(newOption);
1742 if (select) element.trigger('change', changeEventArgs);1743 if (select) optionsToSelect.push(item.id);
1743 }1744 }
1744 if (existingValues.length) element.val(existingValues).trigger('change', changeEventArgs);
1745 });1745 });
1746
1747 element.append(newOptions);
1748 if (optionsToSelect.length) element.val(optionsToSelect).trigger('change', changeEventArgs);
1746}1749}
17471750
1748/**1751/**
@@ -1931,6 +1934,34 @@ export function getFreeName(name, list, numberFormatter = (n) => ` #${n}`) {
1931 return `${name}${numberFormatter(counter)}`;1934 return `${name}${numberFormatter(counter)}`;
1932}1935}
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 */
1945export 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
1934export async function fetchFaFile(name) {1965export async function fetchFaFile(name) {
1935 const style = document.createElement('style');1966 const style = document.createElement('style');
1936 style.innerHTML = await (await fetch(`/css/${name}`)).text();1967 style.innerHTML = await (await fetch(`/css/${name}`)).text();
public/scripts/world-info.js+61 -64
@@ -17,6 +17,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
17import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';17import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
18import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';18import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
19import { StructuredCloneMap } from './util/StructuredCloneMap.js';19import { StructuredCloneMap } from './util/StructuredCloneMap.js';
20import { renderTemplateAsync } from './templates.js';
2021
21export const world_info_insertion_strategy = {22export const world_info_insertion_strategy = {
22 evenly: 0,23 evenly: 0,
@@ -72,6 +73,7 @@ export let world_info_match_whole_words = false;
72export let world_info_use_group_scoring = false;73export let world_info_use_group_scoring = false;
73export let world_info_character_strategy = world_info_insertion_strategy.character_first;74export let world_info_character_strategy = world_info_insertion_strategy.character_first;
74export let world_info_budget_cap = 0;75export let world_info_budget_cap = 0;
76export let world_info_max_recursion_steps = 0;
75const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), debounce_timeout.relaxed);77const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), debounce_timeout.relaxed);
76const saveSettingsDebounced = debounce(() => {78const saveSettingsDebounced = debounce(() => {
77 Object.assign(world_info, { globalSelect: selected_world_info });79 Object.assign(world_info, { globalSelect: selected_world_info });
@@ -709,6 +711,7 @@ export function getWorldInfoSettings() {
709 world_info_character_strategy,711 world_info_character_strategy,
710 world_info_budget_cap,712 world_info_budget_cap,
711 world_info_use_group_scoring,713 world_info_use_group_scoring,
714 world_info_max_recursion_steps,
712 };715 };
713}716}
714717
@@ -795,6 +798,8 @@ export function setWorldInfoSettings(settings, data) {
795 world_info_budget_cap = Number(settings.world_info_budget_cap);798 world_info_budget_cap = Number(settings.world_info_budget_cap);
796 if (settings.world_info_use_group_scoring !== undefined)799 if (settings.world_info_use_group_scoring !== undefined)
797 world_info_use_group_scoring = Boolean(settings.world_info_use_group_scoring);800 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
799 // Migrate old settings804 // Migrate old settings
800 if (world_info_budget > 100) {805 if (world_info_budget > 100) {
@@ -843,6 +848,9 @@ export function setWorldInfoSettings(settings, data) {
843 $('#world_info_budget_cap').val(world_info_budget_cap);848 $('#world_info_budget_cap').val(world_info_budget_cap);
844 $('#world_info_budget_cap_counter').val(world_info_budget_cap);849 $('#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
846 world_names = data.world_names?.length ? data.world_names : [];854 world_names = data.world_names?.length ? data.world_names : [];
847855
848 // Add to existing selected WI if it exists856 // Add to existing selected WI if it exists
@@ -1854,28 +1862,9 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
1854 worldEntriesList.find('*').off();1862 worldEntriesList.find('*').off();
1855 worldEntriesList.empty();1863 worldEntriesList.empty();
18561864
1857 const keywordHeaders = `1865 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);
1879 const isCustomOrder = $('#world_info_sort_order').find(':selected').data('rule') === 'custom';1868 const isCustomOrder = $('#world_info_sort_order').find(':selected').data('rule') === 'custom';
1880 if (!isCustomOrder) {1869 if (!isCustomOrder) {
1881 blocks.forEach(block => {1870 blocks.forEach(block => {
@@ -2275,7 +2264,7 @@ export function parseRegexFromString(input) {
2275 }2264 }
2276}2265}
22772266
2278function getWorldEntry(name, data, entry) {2267async function getWorldEntry(name, data, entry) {
2279 if (!data.entries[entry.uid]) {2268 if (!data.entries[entry.uid]) {
2280 return;2269 return;
2281 }2270 }
@@ -2317,6 +2306,9 @@ function getWorldEntry(name, data, entry) {
2317 }2306 }
23182307
2319 if (isFancyInput) {2308 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
2320 input.select2({2312 input.select2({
2321 ajax: dynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),2313 ajax: dynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
2322 tags: true,2314 tags: true,
@@ -2358,8 +2350,6 @@ function getWorldEntry(name, data, entry) {
2358 input.next('span.select2-container').find('textarea')2350 input.next('span.select2-container').find('textarea')
2359 .val(key).trigger('input');2351 .val(key).trigger('input');
2360 }, { openDrawer: true });2352 }, { openDrawer: true });
2361
2362 select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
2363 }2353 }
2364 else {2354 else {
2365 // Compatibility with mobile devices. On mobile we need a text input field, not a select option control, so we need its own event handlers2355 // 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) {
2476 if (!isMobile()) {2466 if (!isMobile()) {
2477 $(characterFilter).select2({2467 $(characterFilter).select2({
2478 width: '100%',2468 width: '100%',
2479 placeholder: 'All characters will pull from this entry.',2469 placeholder: 'Tie this entry to specific characters or characters with specific tags',
2480 allowClear: true,2470 allowClear: true,
2481 closeOnSelect: false,2471 closeOnSelect: false,
2482 });2472 });
@@ -2876,21 +2866,7 @@ function getWorldEntry(name, data, entry) {
2876 //add UID above content box (less important doesn't need to be always visible)2866 //add UID above content box (less important doesn't need to be always visible)
2877 template.find('.world_entry_form_uid_value').text(`(UID: ${entry.uid})`);2867 template.find('.world_entry_form_uid_value').text(`(UID: ${entry.uid})`);
28782868
2879 // disable2869 //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
2894 const entryStateSelector = template.find('select[name="entryStateSelector"]');2870 const entryStateSelector = template.find('select[name="entryStateSelector"]');
2895 entryStateSelector.data('uid', entry.uid);2871 entryStateSelector.data('uid', entry.uid);
2896 entryStateSelector.on('click', function (event) {2872 entryStateSelector.on('click', function (event) {
@@ -2903,49 +2879,43 @@ function getWorldEntry(name, data, entry) {
2903 switch (value) {2879 switch (value) {
2904 case 'constant':2880 case 'constant':
2905 data.entries[uid].constant = true;2881 data.entries[uid].constant = true;
2906 data.entries[uid].disable = false;
2907 data.entries[uid].vectorized = false;2882 data.entries[uid].vectorized = false;
2908 setWIOriginalDataValue(data, uid, 'enabled', true);
2909 setWIOriginalDataValue(data, uid, 'constant', true);2883 setWIOriginalDataValue(data, uid, 'constant', true);
2910 setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);2884 setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
2911 template.removeClass('disabledWIEntry');
2912 break;2885 break;
2913 case 'normal':2886 case 'normal':
2914 data.entries[uid].constant = false;2887 data.entries[uid].constant = false;
2915 data.entries[uid].disable = false;
2916 data.entries[uid].vectorized = false;2888 data.entries[uid].vectorized = false;
2917 setWIOriginalDataValue(data, uid, 'enabled', true);
2918 setWIOriginalDataValue(data, uid, 'constant', false);2889 setWIOriginalDataValue(data, uid, 'constant', false);
2919 setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);2890 setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
2920 template.removeClass('disabledWIEntry');
2921 break;2891 break;
2922 case 'vectorized':2892 case 'vectorized':
2923 data.entries[uid].constant = false;2893 data.entries[uid].constant = false;
2924 data.entries[uid].disable = false;
2925 data.entries[uid].vectorized = true;2894 data.entries[uid].vectorized = true;
2926 setWIOriginalDataValue(data, uid, 'enabled', true);
2927 setWIOriginalDataValue(data, uid, 'constant', false);2895 setWIOriginalDataValue(data, uid, 'constant', false);
2928 setWIOriginalDataValue(data, uid, 'extensions.vectorized', true);2896 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');
2939 break;2897 break;
2940 }2898 }
2941 await saveWorldInfo(name, data);2899 await saveWorldInfo(name, data);
29422900
2943 });2901 });
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
2945 const entryState = function () {2917 const entryState = function () {
2946 if (entry.disable === true) {2918 if (entry.constant === true) {
2947 return 'disabled';
2948 } else if (entry.constant === true) {
2949 return 'constant';2919 return 'constant';
2950 } else if (entry.vectorized === true) {2920 } else if (entry.vectorized === true) {
2951 return 'vectorized';2921 return 'vectorized';
@@ -2953,6 +2923,12 @@ function getWorldEntry(name, data, entry) {
2953 return 'normal';2923 return 'normal';
2954 }2924 }
2955 };2925 };
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
2956 template2932 template
2957 .find(`select[name="entryStateSelector"] option[value=${entryState()}]`)2933 .find(`select[name="entryStateSelector"] option[value=${entryState()}]`)
2958 .prop('selected', true)2934 .prop('selected', true)
@@ -3754,6 +3730,12 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
3754 console.debug(`[WI] --- SEARCHING ENTRIES (on ${sortedEntries.length} entries) ---`);3730 console.debug(`[WI] --- SEARCHING ENTRIES (on ${sortedEntries.length} entries) ---`);
37553731
3756 while (scanState) {3732 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
3757 // Track how many times the loop has run. May be useful for debugging.3739 // Track how many times the loop has run. May be useful for debugging.
3758 count++;3740 count++;
37593741
@@ -4793,8 +4775,13 @@ jQuery(() => {
47934775
4794 $('#world_info_min_activations').on('input', function () {4776 $('#world_info_min_activations').on('input', function () {
4795 world_info_min_activations = Number($(this).val());4777 world_info_min_activations = Number($(this).val());
4796 $('#world_info_min_activations_counter').val($(this).val());4778 $('#world_info_min_activations_counter').val(world_info_min_activations);
4797 saveSettings();4779
4780 if (world_info_min_activations !== 0) {
4781 $('#world_info_max_recursion_steps').val(0).trigger('input');
4782 } else {
4783 saveSettings();
4784 }
4798 });4785 });
47994786
4800 $('#world_info_min_activations_depth_max').on('input', function () {4787 $('#world_info_min_activations_depth_max').on('input', function () {
@@ -4850,6 +4837,16 @@ jQuery(() => {
4850 saveSettings();4837 saveSettings();
4851 });4838 });
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
4853 $('#world_button').on('click', async function (event) {4850 $('#world_button').on('click', async function (event) {
4854 const chid = $('#set_character_world').data('chid');4851 const chid = $('#set_character_world').data('chid');
48554852
public/style.css+25 -0
@@ -3515,6 +3515,8 @@ grammarly-extension {
35153515
3516.drag-handle {3516.drag-handle {
3517 cursor: grab;3517 cursor: grab;
3518 /* Make the drag handle not selectable in most browsers */
3519 user-select: none;
3518}3520}
35193521
3520#form_rename_chat {3522#form_rename_chat {
@@ -4577,6 +4579,7 @@ a {
4577 image-rendering: -webkit-optimize-contrast;4579 image-rendering: -webkit-optimize-contrast;
4578}4580}
45794581
4582.mes_img_swipes,
4580.mes_img_controls {4583.mes_img_controls {
4581 position: absolute;4584 position: absolute;
4582 top: 0.1em;4585 top: 0.1em;
@@ -4586,9 +4589,16 @@ a {
4586 opacity: 0;4589 opacity: 0;
4587 flex-direction: row;4590 flex-direction: row;
4588 justify-content: space-between;4591 justify-content: space-between;
4592 align-items: center;
4589 padding: 1em;4593 padding: 1em;
4590}4594}
45914595
4596.mes_img_swipes {
4597 top: unset;
4598 bottom: 0.1rem;
4599}
4600
4601.mes_img_swipes .right_menu_button,
4592.mes_img_controls .right_menu_button {4602.mes_img_controls .right_menu_button {
4593 filter: brightness(90%);4603 filter: brightness(90%);
4594 text-shadow: 1px 1px var(--SmartThemeShadowColor) !important;4604 text-shadow: 1px 1px var(--SmartThemeShadowColor) !important;
@@ -4597,16 +4607,20 @@ a {
4597 width: 1.25em;4607 width: 1.25em;
4598}4608}
45994609
4610.mes_img_swipes .right_menu_button::before,
4600.mes_img_controls .right_menu_button::before {4611.mes_img_controls .right_menu_button::before {
4601 /* Fix weird alignment with this font-awesome icons on focus */4612 /* Fix weird alignment with this font-awesome icons on focus */
4602 position: relative;4613 position: relative;
4603 top: 0.6125em;4614 top: 0.6125em;
4604}4615}
46054616
4617.mes_img_swipes .right_menu_button:hover,
4606.mes_img_controls .right_menu_button:hover {4618.mes_img_controls .right_menu_button:hover {
4607 filter: brightness(150%);4619 filter: brightness(150%);
4608}4620}
46094621
4622.mes_img_container:hover .mes_img_swipes,
4623.mes_img_container:focus-within .mes_img_swipes,
4610.mes_img_container:hover .mes_img_controls,4624.mes_img_container:hover .mes_img_controls,
4611.mes_img_container:focus-within .mes_img_controls {4625.mes_img_container:focus-within .mes_img_controls {
4612 opacity: 1;4626 opacity: 1;
@@ -4620,6 +4634,17 @@ body:not(.caption) .mes_img_caption {
4620 display: none;4634 display: none;
4621}4635}
46224636
4637.mes_img_container:not(.img_swipes) .mes_img_swipes,
4638body: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
4623.img_enlarged_holder {4648.img_enlarged_holder {
4624 /* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */4649 /* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */
4625 min-height: 120px;4650 min-height: 120px;
server.js+212 -31
@@ -43,6 +43,8 @@ const {
43 getConfigValue,43 getConfigValue,
44 color,44 color,
45 forwardFetchResponse,45 forwardFetchResponse,
46 removeColorFormatting,
47 getSeparator,
46} = require('./src/util');48} = require('./src/util');
47const { ensureThumbnailCache } = require('./src/endpoints/thumbnails');49const { ensureThumbnailCache } = require('./src/endpoints/thumbnails');
4850
@@ -54,9 +56,6 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
54 if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);56 if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
55}57}
5658
57// Set default DNS resolution order to IPv4 first
58dns.setDefaultResultOrder('ipv4first');
59
60const DEFAULT_PORT = 8000;59const DEFAULT_PORT = 8000;
61const DEFAULT_AUTORUN = false;60const DEFAULT_AUTORUN = false;
62const DEFAULT_LISTEN = false;61const DEFAULT_LISTEN = false;
@@ -66,16 +65,46 @@ const DEFAULT_ACCOUNTS = false;
66const DEFAULT_CSRF_DISABLED = false;65const DEFAULT_CSRF_DISABLED = false;
67const DEFAULT_BASIC_AUTH = false;66const DEFAULT_BASIC_AUTH = false;
6867
68const DEFAULT_ENABLE_IPV6 = false;
69const DEFAULT_ENABLE_IPV4 = true;
70
71const DEFAULT_PREFER_IPV6 = false;
72
73const DEFAULT_AVOID_LOCALHOST = false;
74
75const DEFAULT_AUTORUN_HOSTNAME = 'auto';
76const DEFAULT_AUTORUN_PORT = -1;
77
69const cliArguments = yargs(hideBin(process.argv))78const cliArguments = yargs(hideBin(process.argv))
70 .usage('Usage: <your-start-script> <command> [options]')79 .usage('Usage: <your-start-script> <command> [options]')
71 .option('port', {80 .option('enableIPv6', {
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', {
72 type: 'number',89 type: 'number',
73 default: null,90 default: null,
74 describe: `Sets the port under which SillyTavern will run.\nIf not provided falls back to yaml config 'port'.\n[config default: ${DEFAULT_PORT}]`,91 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}]`,
75 }).option('autorun', {96 }).option('autorun', {
76 type: 'boolean',97 type: 'boolean',
77 default: null,98 default: null,
78 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}]`,99 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',
79 }).option('listen', {108 }).option('listen', {
80 type: 'boolean',109 type: 'boolean',
81 default: null,110 default: null,
@@ -108,6 +137,10 @@ const cliArguments = yargs(hideBin(process.argv))
108 type: 'string',137 type: 'string',
109 default: null,138 default: null,
110 describe: 'Root directory for data storage',139 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',
111 }).option('basicAuthMode', {144 }).option('basicAuthMode', {
112 type: 'boolean',145 type: 'boolean',
113 default: null,146 default: null,
@@ -138,6 +171,31 @@ const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
138171
139const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);172const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
140173
174const enableIPv6 = cliArguments.enableIPv6 ?? getConfigValue('protocol.ipv6', DEFAULT_ENABLE_IPV6);
175const enableIPv4 = cliArguments.enableIPv4 ?? getConfigValue('protocol.ipv4', DEFAULT_ENABLE_IPV4);
176
177const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
178const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT);
179
180const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6);
181
182const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST);
183
184if (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
194if (!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
141// CORS Settings //199// CORS Settings //
142const CORS = cors({200const CORS = cors({
143 origin: 'null',201 origin: 'null',
@@ -546,15 +604,15 @@ app.use('/api/speech', require('./src/endpoints/speech').router);
546// Azure TTS604// Azure TTS
547app.use('/api/azure', require('./src/endpoints/azure').router);605app.use('/api/azure', require('./src/endpoints/azure').router);
548606
549const tavernUrl = new URL(607const tavernUrlV6 = new URL(
550 (cliArguments.ssl ? 'https://' : 'http://') +608 (cliArguments.ssl ? 'https://' : 'http://') +
551 (listen ? '0.0.0.0' : '127.0.0.1') +609 (listen ? '[::]' : '[::1]') +
552 (':' + server_port),610 (':' + server_port),
553);611);
554612
555const autorunUrl = new URL(613const tavernUrl = new URL(
556 (cliArguments.ssl ? 'https://' : 'http://') +614 (cliArguments.ssl ? 'https://' : 'http://') +
557 ('127.0.0.1') +615 (listen ? '0.0.0.0' : '127.0.0.1') +
558 (':' + server_port),616 (':' + server_port),
559);617);
560618
@@ -607,19 +665,67 @@ const preSetupTasks = async function () {
607};665};
608666
609/**667/**
668 * Gets the hostname to use for autorun in the browser.
669 * @returns {string} The hostname to use for autorun
670 */
671function 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/**
610 * Tasks that need to be run after the server starts listening.691 * 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
611 */694 */
612const postSetupTasks = async function () {695const 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
613 console.log('Launching...');703 console.log('Launching...');
614704
615 if (autorun) open(autorunUrl.toString());705 if (autorun) open(autorunUrl.toString());
616706
617 setWindowTitle('SillyTavern WebServer');707 setWindowTitle('SillyTavern WebServer');
618708
619 console.log(color.green('SillyTavern is listening on: ' + tavernUrl));709 let logListen = 'SillyTavern is listening on';
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
621 if (listen) {727 if (listen) {
622 console.log('\n0.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 (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');728 console.log('[::] 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');
623 }729 }
624730
625 if (basicAuthMode) {731 if (basicAuthMode) {
@@ -674,6 +780,100 @@ function logSecurityAlert(message) {
674 process.exit(1);780 process.exit(1);
675}781}
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 */
788function 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 */
811function 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 */
830function 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
839async 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
870async function startServer() {
871 const [v6Failed, v4Failed] = await startHTTPorHTTPS();
872
873 handleServerListenFail(v6Failed, v4Failed);
874 postSetupTasks(v6Failed, v4Failed);
875}
876
677async function verifySecuritySettings() {877async function verifySecuritySettings() {
678 // Skip all security checks as listen is set to false878 // Skip all security checks as listen is set to false
679 if (!listen) {879 if (!listen) {
@@ -707,23 +907,4 @@ userModule.initUserStorage(dataRoot)
707 .then(userModule.migrateUserData)907 .then(userModule.migrateUserData)
708 .then(verifySecuritySettings)908 .then(verifySecuritySettings)
709 .then(preSetupTasks)909 .then(preSetupTasks)
710 .finally(() => {910 .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 = {
195 PERPLEXITY: 'perplexity',195 PERPLEXITY: 'perplexity',
196 GROQ: 'groq',196 GROQ: 'groq',
197 ZEROONEAI: '01ai',197 ZEROONEAI: '01ai',
198 BLOCKENTROPY: 'blockentropy',
198};199};
199200
200/**201/**
src/endpoints/anthropic.js+1 -1
@@ -28,7 +28,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
28 ],28 ],
29 },29 },
30 ],30 ],
31 max_tokens: 800,31 max_tokens: 4096,
32 };32 };
3333
34 console.log('Multimodal captioning request', body);34 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';
18const API_GROQ = 'https://api.groq.com/openai/v1';18const API_GROQ = 'https://api.groq.com/openai/v1';
19const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';19const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
20const API_01AI = 'https://api.01.ai/v1';20const API_01AI = 'https://api.01.ai/v1';
21const API_BLOCKENTROPY = 'https://api.blockentropy.ai/v1';
2122
22/**23/**
23 * Applies a post-processing step to the generated messages.24 * Applies a post-processing step to the generated messages.
@@ -104,6 +105,7 @@ async function sendClaudeRequest(request, response) {
104 const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();105 const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();
105 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);106 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);
106 const divider = '-'.repeat(process.stdout.columns);107 const divider = '-'.repeat(process.stdout.columns);
108 const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false);
107109
108 if (!apiKey) {110 if (!apiKey) {
109 console.log(color.red(`Claude API key is missing.\n${divider}`));111 console.log(color.red(`Claude API key is missing.\n${divider}`));
@@ -117,8 +119,8 @@ async function sendClaudeRequest(request, response) {
117 controller.abort();119 controller.abort();
118 });120 });
119 const additionalHeaders = {};121 const additionalHeaders = {};
120 let use_system_prompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;122 const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
121 let converted_prompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, use_system_prompt, request.body.human_sysprompt_message, request.body.char_name, request.body.user_name);123 const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, request.body.human_sysprompt_message, request.body.char_name, request.body.user_name);
122 // Add custom stop sequences124 // Add custom stop sequences
123 const stopSequences = [];125 const stopSequences = [];
124 if (Array.isArray(request.body.stop)) {126 if (Array.isArray(request.body.stop)) {
@@ -126,7 +128,7 @@ async function sendClaudeRequest(request, response) {
126 }128 }
127129
128 const requestBody = {130 const requestBody = {
129 messages: converted_prompt.messages,131 messages: convertedPrompt.messages,
130 model: request.body.model,132 model: request.body.model,
131 max_tokens: request.body.max_tokens,133 max_tokens: request.body.max_tokens,
132 stop_sequences: stopSequences,134 stop_sequences: stopSequences,
@@ -135,13 +137,15 @@ async function sendClaudeRequest(request, response) {
135 top_k: request.body.top_k,137 top_k: request.body.top_k,
136 stream: request.body.stream,138 stream: request.body.stream,
137 };139 };
138 if (use_system_prompt) {140 if (useSystemPrompt) {
139 requestBody.system = converted_prompt.systemPrompt;141 requestBody.system = enableSystemPromptCache
142 ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]
143 : convertedPrompt.systemPrompt;
140 }144 }
141 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {145 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
142 // Claude doesn't do prefills on function calls, and doesn't allow empty messages146 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
143 if (converted_prompt.messages.length && converted_prompt.messages[converted_prompt.messages.length - 1].role === 'assistant') {147 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
144 converted_prompt.messages.push({ role: 'user', content: '.' });148 convertedPrompt.messages.push({ role: 'user', content: '.' });
145 }149 }
146 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';150 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';
147 requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };151 requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };
@@ -150,6 +154,9 @@ async function sendClaudeRequest(request, response) {
150 .map(tool => tool.function)154 .map(tool => tool.function)
151 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));155 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
152 }156 }
157 if (enableSystemPromptCache) {
158 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
159 }
153 console.log('Claude request:', requestBody);160 console.log('Claude request:', requestBody);
154161
155 const generateResponse = await fetch(apiUrl + '/messages', {162 const generateResponse = await fetch(apiUrl + '/messages', {
@@ -252,7 +259,7 @@ async function sendMakerSuiteRequest(request, response) {
252 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);259 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
253260
254 if (!request.body.reverse_proxy && !apiKey) {261 if (!request.body.reverse_proxy && !apiKey) {
255 console.log('MakerSuite API key is missing.');262 console.log('Google AI Studio API key is missing.');
256 return response.status(400).send({ error: true });263 return response.status(400).send({ error: true });
257 }264 }
258265
@@ -319,7 +326,7 @@ async function sendMakerSuiteRequest(request, response) {
319 }326 }
320327
321 const body = isGemini ? getGeminiBody() : getBisonBody();328 const body = isGemini ? getGeminiBody() : getBisonBody();
322 console.log('MakerSuite request:', body);329 console.log('Google AI Studio request:', body);
323330
324 try {331 try {
325 const controller = new AbortController();332 const controller = new AbortController();
@@ -355,7 +362,7 @@ async function sendMakerSuiteRequest(request, response) {
355 }362 }
356 } else {363 } else {
357 if (!generateResponse.ok) {364 if (!generateResponse.ok) {
358 console.log(`MakerSuite API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);365 console.log(`Google AI Studio API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
359 return response.status(generateResponse.status).send({ error: true });366 return response.status(generateResponse.status).send({ error: true });
360 }367 }
361368
@@ -363,7 +370,7 @@ async function sendMakerSuiteRequest(request, response) {
363370
364 const candidates = generateResponseJson?.candidates;371 const candidates = generateResponseJson?.candidates;
365 if (!candidates || candidates.length === 0) {372 if (!candidates || candidates.length === 0) {
366 let message = 'MakerSuite API returned no candidate';373 let message = 'Google AI Studio API returned no candidate';
367 console.log(message, generateResponseJson);374 console.log(message, generateResponseJson);
368 if (generateResponseJson?.promptFeedback?.blockReason) {375 if (generateResponseJson?.promptFeedback?.blockReason) {
369 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;376 message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;
@@ -374,19 +381,19 @@ async function sendMakerSuiteRequest(request, response) {
374 const responseContent = candidates[0].content ?? candidates[0].output;381 const responseContent = candidates[0].content ?? candidates[0].output;
375 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.[0]?.text;382 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.[0]?.text;
376 if (!responseText) {383 if (!responseText) {
377 let message = 'MakerSuite Candidate text empty';384 let message = 'Google AI Studio Candidate text empty';
378 console.log(message, generateResponseJson);385 console.log(message, generateResponseJson);
379 return response.send({ error: { message } });386 return response.send({ error: { message } });
380 }387 }
381388
382 console.log('MakerSuite response:', responseText);389 console.log('Google AI Studio response:', responseText);
383390
384 // Wrap it back to OAI format391 // Wrap it back to OAI format
385 const reply = { choices: [{ 'message': { 'content': responseText } }] };392 const reply = { choices: [{ 'message': { 'content': responseText } }] };
386 return response.send(reply);393 return response.send(reply);
387 }394 }
388 } catch (error) {395 } catch (error) {
389 console.log('Error communicating with MakerSuite API: ', error);396 console.log('Error communicating with Google AI Studio API: ', error);
390 if (!response.headersSent) {397 if (!response.headersSent) {
391 return response.status(500).send({ error: true });398 return response.status(500).send({ error: true });
392 }399 }
@@ -675,6 +682,10 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
675 api_url = API_01AI;682 api_url = API_01AI;
676 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);683 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
677 headers = {};684 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 = {};
678 } else {689 } else {
679 console.log('This chat completion source is not supported yet.');690 console.log('This chat completion source is not supported yet.');
680 return response_getstatus_openai.status(400).send({ error: true });691 return response_getstatus_openai.status(400).send({ error: true });
@@ -941,6 +952,11 @@ router.post('/generate', jsonParser, function (request, response) {
941 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);952 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
942 headers = {};953 headers = {};
943 bodyParams = {};954 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 = {};
944 } else {960 } else {
945 console.log('This chat completion source is not supported yet.');961 console.log('This chat completion source is not supported yet.');
946 return response.status(400).send({ error: true });962 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
6const { jsonParser } = require('../../express-common');6const { jsonParser } = require('../../express-common');
7const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS, FEATHERLESS_KEYS } = require('../../constants');7const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS, FEATHERLESS_KEYS } = require('../../constants');
8const { forwardFetchResponse, trimV1 } = require('../../util');8const { forwardFetchResponse, trimV1, getConfigValue } = require('../../util');
9const { setAdditionalHeaders } = require('../../additional-headers');9const { setAdditionalHeaders } = require('../../additional-headers');
1010
11const router = express.Router();11const router = express.Router();
@@ -325,11 +325,12 @@ router.post('/generate', jsonParser, async function (request, response) {
325 }325 }
326326
327 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {327 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
328 const keepAlive = getConfigValue('ollama.keepAlive', -1);
328 args.body = JSON.stringify({329 args.body = JSON.stringify({
329 model: request.body.model,330 model: request.body.model,
330 prompt: request.body.prompt,331 prompt: request.body.prompt,
331 stream: request.body.stream ?? false,332 stream: request.body.stream ?? false,
332 keep_alive: -1,333 keep_alive: keepAlive,
333 raw: true,334 raw: true,
334 options: _.pickBy(request.body, (_, key) => OLLAMA_KEYS.includes(key)),335 options: _.pickBy(request.body, (_, key) => OLLAMA_KEYS.includes(key)),
335 });336 });
src/endpoints/google.js+1 -1
@@ -44,7 +44,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4444
45 if (!result.ok) {45 if (!result.ok) {
46 const error = await result.json();46 const error = await result.json();
47 console.log(`MakerSuite API returned error: ${result.status} ${result.statusText}`, error);47 console.log(`Google AI Studio API returned error: ${result.status} ${result.statusText}`, error);
48 return response.status(result.status).send({ error: true });48 return response.status(result.status).send({ error: true });
49 }49 }
5050
src/endpoints/images.js+1 -1
@@ -82,7 +82,7 @@ router.post('/list/:folder', (request, response) => {
82 }82 }
8383
84 try {84 try {
85 const images = getImages(directoryPath);85 const images = getImages(directoryPath, 'date');
86 return response.send(images);86 return response.send(images);
87 } catch (error) {87 } catch (error) {
88 console.error(error);88 console.error(error);
src/endpoints/openai.js+44 -1
@@ -67,7 +67,6 @@ router.post('/caption-image', jsonParser, async (request, response) => {
67 ],67 ],
68 },68 },
69 ],69 ],
70 max_tokens: 500,
71 ...bodyParams,70 ...bodyParams,
72 };71 };
7372
@@ -283,4 +282,48 @@ router.post('/generate-image', jsonParser, async (request, response) => {
283 }282 }
284});283});
285284
285const custom = express.Router();
286
287custom.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
327router.use('/custom', custom);
328
286module.exports = { router };329module.exports = { router };
src/endpoints/secrets.js+2 -0
@@ -44,6 +44,8 @@ const SECRET_KEYS = {
44 ZEROONEAI: 'api_key_01ai',44 ZEROONEAI: 'api_key_01ai',
45 HUGGINGFACE: 'api_key_huggingface',45 HUGGINGFACE: 'api_key_huggingface',
46 STABILITY: 'api_key_stability',46 STABILITY: 'api_key_stability',
47 BLOCKENTROPY: 'api_key_blockentropy',
48 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
47};49};
4850
49// These are the keys that are safe to expose, even if allowKeysExposure is false51// 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');
9const { getAllUserHandles, getUserDirectories } = require('../users');9const { getAllUserHandles, getUserDirectories } = require('../users');
1010
11const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);11const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);
12const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('enableExtensionsAutoUpdate', true);
12const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);13const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
1314
14// 10 minutes15// 10 minutes
@@ -268,6 +269,7 @@ router.post('/get', jsonParser, (request, response) => {
268 instruct,269 instruct,
269 context,270 context,
270 enable_extensions: ENABLE_EXTENSIONS,271 enable_extensions: ENABLE_EXTENSIONS,
272 enable_extensions_auto_update: ENABLE_EXTENSIONS_AUTO_UPDATE,
271 enable_accounts: ENABLE_ACCOUNTS,273 enable_accounts: ENABLE_ACCOUNTS,
272 });274 });
273});275});
src/endpoints/stable-diffusion.js+125 -0
@@ -908,10 +908,135 @@ stability.post('/generate', jsonParser, async (request, response) => {
908 }908 }
909});909});
910910
911const blockentropy = express.Router();
912
913blockentropy.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
949blockentropy.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
994const huggingface = express.Router();
995
996huggingface.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
911router.use('/comfy', comfy);1034router.use('/comfy', comfy);
912router.use('/together', together);1035router.use('/together', together);
913router.use('/drawthings', drawthings);1036router.use('/drawthings', drawthings);
914router.use('/pollinations', pollinations);1037router.use('/pollinations', pollinations);
915router.use('/stability', stability);1038router.use('/stability', stability);
1039router.use('/blockentropy', blockentropy);
1040router.use('/huggingface', huggingface);
9161041
917module.exports = { router };1042module.exports = { router };
src/endpoints/tokenizers.js+31 -3
@@ -143,6 +143,7 @@ const spp_nerd = new SentencePieceTokenizer('src/tokenizers/nerdstash.model');
143const spp_nerd_v2 = new SentencePieceTokenizer('src/tokenizers/nerdstash_v2.model');143const spp_nerd_v2 = new SentencePieceTokenizer('src/tokenizers/nerdstash_v2.model');
144const spp_mistral = new SentencePieceTokenizer('src/tokenizers/mistral.model');144const spp_mistral = new SentencePieceTokenizer('src/tokenizers/mistral.model');
145const spp_yi = new SentencePieceTokenizer('src/tokenizers/yi.model');145const spp_yi = new SentencePieceTokenizer('src/tokenizers/yi.model');
146const spp_gemma = new SentencePieceTokenizer('src/tokenizers/gemma.model');
146const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');147const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');
147const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json');148const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json');
148149
@@ -152,6 +153,7 @@ const sentencepieceTokenizers = [
152 'nerdstash_v2',153 'nerdstash_v2',
153 'mistral',154 'mistral',
154 'yi',155 'yi',
156 'gemma',
155];157];
156158
157/**159/**
@@ -180,6 +182,10 @@ function getSentencepiceTokenizer(model) {
180 return spp_yi;182 return spp_yi;
181 }183 }
182184
185 if (model.includes('gemma')) {
186 return spp_gemma;
187 }
188
183 return null;189 return null;
184}190}
185191
@@ -268,6 +274,10 @@ function getTokenizerModel(requestModel) {
268 return 'gpt-4o';274 return 'gpt-4o';
269 }275 }
270276
277 if (requestModel.includes('chatgpt-4o-latest')) {
278 return 'gpt-4o';
279 }
280
271 if (requestModel.includes('gpt-4-32k')) {281 if (requestModel.includes('gpt-4-32k')) {
272 return 'gpt-4-32k';282 return 'gpt-4-32k';
273 }283 }
@@ -308,8 +318,8 @@ function getTokenizerModel(requestModel) {
308 return 'yi';318 return 'yi';
309 }319 }
310320
311 if (requestModel.includes('gemini')) {321 if (requestModel.includes('gemma') || requestModel.includes('gemini')) {
312 return 'gpt-4o';322 return 'gemma';
313 }323 }
314324
315 // default325 // default
@@ -579,6 +589,7 @@ router.post('/nerdstash/encode', jsonParser, createSentencepieceEncodingHandler(
579router.post('/nerdstash_v2/encode', jsonParser, createSentencepieceEncodingHandler(spp_nerd_v2));589router.post('/nerdstash_v2/encode', jsonParser, createSentencepieceEncodingHandler(spp_nerd_v2));
580router.post('/mistral/encode', jsonParser, createSentencepieceEncodingHandler(spp_mistral));590router.post('/mistral/encode', jsonParser, createSentencepieceEncodingHandler(spp_mistral));
581router.post('/yi/encode', jsonParser, createSentencepieceEncodingHandler(spp_yi));591router.post('/yi/encode', jsonParser, createSentencepieceEncodingHandler(spp_yi));
592router.post('/gemma/encode', jsonParser, createSentencepieceEncodingHandler(spp_gemma));
582router.post('/gpt2/encode', jsonParser, createTiktokenEncodingHandler('gpt2'));593router.post('/gpt2/encode', jsonParser, createTiktokenEncodingHandler('gpt2'));
583router.post('/claude/encode', jsonParser, createWebTokenizerEncodingHandler(claude_tokenizer));594router.post('/claude/encode', jsonParser, createWebTokenizerEncodingHandler(claude_tokenizer));
584router.post('/llama3/encode', jsonParser, createWebTokenizerEncodingHandler(llama3_tokenizer));595router.post('/llama3/encode', jsonParser, createWebTokenizerEncodingHandler(llama3_tokenizer));
@@ -587,6 +598,7 @@ router.post('/nerdstash/decode', jsonParser, createSentencepieceDecodingHandler(
587router.post('/nerdstash_v2/decode', jsonParser, createSentencepieceDecodingHandler(spp_nerd_v2));598router.post('/nerdstash_v2/decode', jsonParser, createSentencepieceDecodingHandler(spp_nerd_v2));
588router.post('/mistral/decode', jsonParser, createSentencepieceDecodingHandler(spp_mistral));599router.post('/mistral/decode', jsonParser, createSentencepieceDecodingHandler(spp_mistral));
589router.post('/yi/decode', jsonParser, createSentencepieceDecodingHandler(spp_yi));600router.post('/yi/decode', jsonParser, createSentencepieceDecodingHandler(spp_yi));
601router.post('/gemma/decode', jsonParser, createSentencepieceDecodingHandler(spp_gemma));
590router.post('/gpt2/decode', jsonParser, createTiktokenDecodingHandler('gpt2'));602router.post('/gpt2/decode', jsonParser, createTiktokenDecodingHandler('gpt2'));
591router.post('/claude/decode', jsonParser, createWebTokenizerDecodingHandler(claude_tokenizer));603router.post('/claude/decode', jsonParser, createWebTokenizerDecodingHandler(claude_tokenizer));
592router.post('/llama3/decode', jsonParser, createWebTokenizerDecodingHandler(llama3_tokenizer));604router.post('/llama3/decode', jsonParser, createWebTokenizerDecodingHandler(llama3_tokenizer));
@@ -620,6 +632,11 @@ router.post('/openai/encode', jsonParser, async function (req, res) {
620 return handler(req, res);632 return handler(req, res);
621 }633 }
622634
635 if (queryModel.includes('gemma') || queryModel.includes('gemini')) {
636 const handler = createSentencepieceEncodingHandler(spp_gemma);
637 return handler(req, res);
638 }
639
623 const model = getTokenizerModel(queryModel);640 const model = getTokenizerModel(queryModel);
624 const handler = createTiktokenEncodingHandler(model);641 const handler = createTiktokenEncodingHandler(model);
625 return handler(req, res);642 return handler(req, res);
@@ -658,6 +675,11 @@ router.post('/openai/decode', jsonParser, async function (req, res) {
658 return handler(req, res);675 return handler(req, res);
659 }676 }
660677
678 if (queryModel.includes('gemma') || queryModel.includes('gemini')) {
679 const handler = createSentencepieceDecodingHandler(spp_gemma);
680 return handler(req, res);
681 }
682
661 const model = getTokenizerModel(queryModel);683 const model = getTokenizerModel(queryModel);
662 const handler = createTiktokenDecodingHandler(model);684 const handler = createTiktokenDecodingHandler(model);
663 return handler(req, res);685 return handler(req, res);
@@ -704,6 +726,11 @@ router.post('/openai/count', jsonParser, async function (req, res) {
704 return res.send({ 'token_count': num_tokens });726 return res.send({ 'token_count': num_tokens });
705 }727 }
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
707 const tokensPerName = queryModel.includes('gpt-3.5-turbo-0301') ? -1 : 1;734 const tokensPerName = queryModel.includes('gpt-3.5-turbo-0301') ? -1 : 1;
708 const tokensPerMessage = queryModel.includes('gpt-3.5-turbo-0301') ? 4 : 3;735 const tokensPerMessage = queryModel.includes('gpt-3.5-turbo-0301') ? 4 : 3;
709 const tokensPadding = 3;736 const tokensPadding = 3;
@@ -785,6 +812,7 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
785 const baseUrl = String(request.body.url);812 const baseUrl = String(request.body.url);
786 const legacyApi = Boolean(request.body.legacy_api);813 const legacyApi = Boolean(request.body.legacy_api);
787 const vllmModel = String(request.body.vllm_model) || '';814 const vllmModel = String(request.body.vllm_model) || '';
815 const aphroditeModel = String(request.body.aphrodite_model) || '';
788816
789 try {817 try {
790 const args = {818 const args = {
@@ -820,7 +848,7 @@ router.post('/remote/textgenerationwebui/encode', jsonParser, async function (re
820 break;848 break;
821 case TEXTGEN_TYPES.APHRODITE:849 case TEXTGEN_TYPES.APHRODITE:
822 url += '/v1/tokenize';850 url += '/v1/tokenize';
823 args.body = JSON.stringify({ 'prompt': text });851 args.body = JSON.stringify({ 'model': aphroditeModel, 'prompt': text });
824 break;852 break;
825 default:853 default:
826 url += '/v1/internal/encode';854 url += '/v1/internal/encode';
src/endpoints/translate.js+6 -3
@@ -1,6 +1,7 @@
1const fetch = require('node-fetch').default;1const fetch = require('node-fetch').default;
2const https = require('https');2const https = require('https');
3const express = require('express');3const express = require('express');
4const iconv = require('iconv-lite');
4const { readSecret, SECRET_KEYS } = require('./secrets');5const { readSecret, SECRET_KEYS } = require('./secrets');
5const { getConfigValue, uuidv4 } = require('../util');6const { getConfigValue, uuidv4 } = require('../util');
6const { jsonParser } = require('../express-common');7const { jsonParser } = require('../express-common');
@@ -80,16 +81,18 @@ router.post('/google', jsonParser, async (request, response) => {
80 const url = generateRequestUrl(text, { to: lang });81 const url = generateRequestUrl(text, { to: lang });
8182
82 https.get(url, (resp) => {83 https.get(url, (resp) => {
83 let data = '';84 const data = [];
8485
85 resp.on('data', (chunk) => {86 resp.on('data', (chunk) => {
86 data += chunk;87 data.push(chunk);
87 });88 });
8889
89 resp.on('end', () => {90 resp.on('end', () => {
90 try {91 try {
91 const result = normaliseResponse(JSON.parse(data));92 const decodedData = iconv.decode(Buffer.concat(data), 'utf-8');
93 const result = normaliseResponse(JSON.parse(decodedData));
92 console.log('Translated text: ' + result.text);94 console.log('Translated text: ' + result.text);
95 response.setHeader('Content-Type', 'text/plain; charset=utf-8');
93 return response.send(result.text);96 return response.send(result.text);
94 } catch (error) {97 } catch (error) {
95 console.log('Translation error', error);98 console.log('Translation error', error);
src/tokenizers/gemma.model+0 -0

Binary file

src/transformers.mjs+37 -4
@@ -1,6 +1,7 @@
1import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';1import { pipeline, env, RawImage, Pipeline } from 'sillytavern-transformers';
2import { getConfigValue } from './util.js';2import { getConfigValue } from './util.js';
3import path from 'path';3import path from 'path';
4import fs from 'fs';
45
5configureTransformers();6configureTransformers();
67
@@ -34,7 +35,7 @@ const tasks = {
34 defaultModel: 'Cohee/fooocus_expansion-onnx',35 defaultModel: 'Cohee/fooocus_expansion-onnx',
35 pipeline: null,36 pipeline: null,
36 configField: 'extras.promptExpansionModel',37 configField: 'extras.promptExpansionModel',
37 quantized: true,38 quantized: false,
38 },39 },
39 'automatic-speech-recognition': {40 'automatic-speech-recognition': {
40 defaultModel: 'Xenova/whisper-small',41 defaultModel: 'Xenova/whisper-small',
@@ -48,7 +49,7 @@ const tasks = {
48 configField: 'extras.textToSpeechModel',49 configField: 'extras.textToSpeechModel',
49 quantized: false,50 quantized: false,
50 },51 },
51}52};
5253
53/**54/**
54 * Gets a RawImage object from a base64-encoded image.55 * Gets a RawImage object from a base64-encoded image.
@@ -85,6 +86,36 @@ function getModelForTask(task) {
85 }86 }
86}87}
8788
89async 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
88/**119/**
89 * Gets the transformers.js pipeline for a given task.120 * Gets the transformers.js pipeline for a given task.
90 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for121 * @param {import('sillytavern-transformers').PipelineType} task The task to get the pipeline for
@@ -92,6 +123,8 @@ function getModelForTask(task) {
92 * @returns {Promise<Pipeline>} Pipeline for the task123 * @returns {Promise<Pipeline>} Pipeline for the task
93 */124 */
94async function getPipeline(task, forceModel = '') {125async function getPipeline(task, forceModel = '') {
126 await migrateCacheToDataDir();
127
95 if (tasks[task].pipeline) {128 if (tasks[task].pipeline) {
96 if (forceModel === '' || tasks[task].currentModel === forceModel) {129 if (forceModel === '' || tasks[task].currentModel === forceModel) {
97 return tasks[task].pipeline;130 return tasks[task].pipeline;
@@ -100,11 +133,11 @@ async function getPipeline(task, forceModel = '') {
100 await tasks[task].pipeline.dispose();133 await tasks[task].pipeline.dispose();
101 }134 }
102135
103 const cache_dir = path.join(process.cwd(), 'cache');136 const cacheDir = path.join(global.DATA_ROOT, '_cache');
104 const model = forceModel || getModelForTask(task);137 const model = forceModel || getModelForTask(task);
105 const localOnly = getConfigValue('extras.disableAutoDownload', false);138 const localOnly = getConfigValue('extras.disableAutoDownload', false);
106 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);139 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
107 const instance = await pipeline(task, model, { cache_dir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });140 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
108 tasks[task].pipeline = instance;141 tasks[task].pipeline = instance;
109 tasks[task].currentModel = model;142 tasks[task].currentModel = model;
110 return instance;143 return instance;
src/users.js+5 -11
@@ -20,12 +20,6 @@ const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
20const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');20const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2121
22/**22/**
23 * The root directory for user data.
24 * @type {string}
25 */
26let DATA_ROOT = './data';
27
28/**
29 * Cache for user directories.23 * Cache for user directories.
30 * @type {Map<string, UserDirectoryList>}24 * @type {Map<string, UserDirectoryList>}
31 */25 */
@@ -138,7 +132,7 @@ async function migrateUserData() {
138132
139 console.log();133 console.log();
140 console.log(color.magenta('Preparing to migrate user data...'));134 console.log(color.magenta('Preparing to migrate user data...'));
141 console.log(`All public data will be moved to the ${DATA_ROOT} directory.`);135 console.log(`All public data will be moved to the ${global.DATA_ROOT} directory.`);
142 console.log('This process may take a while depending on the amount of data to move.');136 console.log('This process may take a while depending on the amount of data to move.');
143 console.log(`Backups will be placed in the ${PUBLIC_DIRECTORIES.backups} directory.`);137 console.log(`Backups will be placed in the ${PUBLIC_DIRECTORIES.backups} directory.`);
144 console.log(`The process will start in ${TIMEOUT} seconds. Press Ctrl+C to cancel.`);138 console.log(`The process will start in ${TIMEOUT} seconds. Press Ctrl+C to cancel.`);
@@ -352,11 +346,11 @@ function toAvatarKey(handle) {
352 * @returns {Promise<void>}346 * @returns {Promise<void>}
353 */347 */
354async function initUserStorage(dataRoot) {348async function initUserStorage(dataRoot) {
355 DATA_ROOT = dataRoot;349 global.DATA_ROOT = dataRoot;
356 console.log('Using data root:', color.green(DATA_ROOT));350 console.log('Using data root:', color.green(global.DATA_ROOT));
357 console.log();351 console.log();
358 await storage.init({352 await storage.init({
359 dir: path.join(DATA_ROOT, '_storage'),353 dir: path.join(global.DATA_ROOT, '_storage'),
360 ttl: false, // Never expire354 ttl: false, // Never expire
361 });355 });
362356
@@ -457,7 +451,7 @@ function getUserDirectories(handle) {
457451
458 const directories = structuredClone(USER_DIRECTORY_TEMPLATE);452 const directories = structuredClone(USER_DIRECTORY_TEMPLATE);
459 for (const key in directories) {453 for (const key in directories) {
460 directories[key] = path.join(DATA_ROOT, handle, USER_DIRECTORY_TEMPLATE[key]);454 directories[key] = path.join(global.DATA_ROOT, handle, USER_DIRECTORY_TEMPLATE[key]);
461 }455 }
462 DIRECTORIES_CACHE.set(handle, directories);456 DIRECTORIES_CACHE.set(handle, directories);
463 return directories;457 return directories;
src/util.js+41 -3
@@ -382,14 +382,31 @@ function removeOldBackups(directory, prefix) {
382 }382 }
383}383}
384384
385function 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 */
391function 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
386 return fs403 return fs
387 .readdirSync(path)404 .readdirSync(directoryPath)
388 .filter(file => {405 .filter(file => {
389 const type = mime.lookup(file);406 const type = mime.lookup(file);
390 return type && type.startsWith('image/');407 return type && type.startsWith('image/');
391 })408 })
392 .sort(Intl.Collator().compare);409 .sort(getSortFunction());
393}410}
394411
395/**412/**
@@ -610,6 +627,25 @@ class Cache {
610 }627 }
611}628}
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 */
635function 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 */
645function getSeparator(n) {
646 return '='.repeat(n);
647}
648
613module.exports = {649module.exports = {
614 getConfig,650 getConfig,
615 getConfigValue,651 getConfigValue,
@@ -637,4 +673,6 @@ module.exports = {
637 trimV1,673 trimV1,
638 Cache,674 Cache,
639 makeHttp2Request,675 makeHttp2Request,
676 removeColorFormatting,
677 getSeparator,
640};678};
src/vectors/makersuite-vectors.js+4 -4
@@ -23,8 +23,8 @@ async function getMakerSuiteVector(text, directories) {
23 const key = readSecret(directories, SECRET_KEYS.MAKERSUITE);23 const key = readSecret(directories, SECRET_KEYS.MAKERSUITE);
2424
25 if (!key) {25 if (!key) {
26 console.log('No MakerSuite key found');26 console.log('No Google AI Studio key found');
27 throw new Error('No MakerSuite key found');27 throw new Error('No Google AI Studio key found');
28 }28 }
2929
30 const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/embedding-gecko-001:embedText?key=${key}`, {30 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
40 if (!response.ok) {40 if (!response.ok) {
41 const text = await response.text();41 const text = await response.text();
42 console.log('MakerSuite request failed', response.statusText, text);42 console.log('Google AI Studio request failed', response.statusText, text);
43 throw new Error('MakerSuite request failed');43 throw new Error('Google AI Studio request failed');
44 }44 }
4545
46 const data = await response.json();46 const data = await response.json();
tests/package-lock.json+6 -8
@@ -1653,10 +1653,9 @@
1653 "license": "MIT"1653 "license": "MIT"
1654 },1654 },
1655 "node_modules/axios": {1655 "node_modules/axios": {
1656 "version": "1.7.2",1656 "version": "1.7.4",
1657 "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",1657 "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
1658 "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",1658 "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
1659 "license": "MIT",
1660 "dependencies": {1659 "dependencies": {
1661 "follow-redirects": "^1.15.6",1660 "follow-redirects": "^1.15.6",
1662 "form-data": "^4.0.0",1661 "form-data": "^4.0.0",
@@ -4514,10 +4513,9 @@
4514 }4513 }
4515 },4514 },
4516 "node_modules/micromatch": {4515 "node_modules/micromatch": {
4517 "version": "4.0.7",4516 "version": "4.0.8",
4518 "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",4517 "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
4519 "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",4518 "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
4520 "license": "MIT",
4521 "dependencies": {4519 "dependencies": {
4522 "braces": "^3.0.3",4520 "braces": "^3.0.3",
4523 "picomatch": "^2.3.1"4521 "picomatch": "^2.3.1"