Merge pull request #5154 from SillyTavern/staging Staging

e3b866b5d2bcc7fbaa889bb926fbb567cd1ed25b

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

Signed
141 files changed, +5636 -1164Showing whitespace changes
.dockerignore+55 -15
@@ -1,21 +1,61 @@
1# --- Git & CI ---
1.git2.git
2.github3.github
3.vscode4.gitignore
4node_modules5
5npm-debug.log6# --- Docker ---
6readme*7/Dockerfile
7Start.bat8/.dockerignore
8/dist9/docker/docker-compose.yml
9/backups
10cloudflared.exe
11access.log
12/data
13/cache
14.DS_Store
15/public/scripts/extensions/third-party
16/colab
17.gemini
18/docker/config10/docker/config
19/docker/extensions11/docker/extensions
20/docker/data12/docker/data
21/docker/plugins13/docker/plugins
14/public/scripts/extensions/third-party
15
16# --- Plugins (keep only package files) ---
17/plugins/*
18!/plugins/package.json
19!/plugins/package-lock.json
20
21# --- The Folders ---
22/backups
23/cache
24/colab
25/data
26/dist
27/node_modules
28/tests
29
30# --- Sensitive Info ---
31**/.env*
32**/*.pem
33**/certs
34
35# --- Documentation ---
36readme*
37*.md
38Update-Instructions.txt
39
40# --- OS & System Junk ---
41**/.DS_Store
42*.bat
43*.cmd
44*.exe
45start.sh
46
47# --- Dev Config ---
48.editorconfig
49.eslintrc.cjs
50.eslintrc*
51.vscode
52**/jsconfig.json
53.npmignore
54.gemini
55replit.nix
56.replit
57.nomedia
58
59# -- Logs & Temp ---
60*.log
61**/tmp
.eslintrc.cjs+1 -1
@@ -98,7 +98,7 @@ module.exports = {
98 'no-cond-assign': 'error',98 'no-cond-assign': 'error',
99 'no-unneeded-ternary': 'error',99 'no-unneeded-ternary': 'error',
100 'no-irregular-whitespace': ['error', { skipStrings: true, skipTemplates: true }],100 'no-irregular-whitespace': ['error', { skipStrings: true, skipTemplates: true }],
101101 'dot-notation': ['error', { 'allowPattern': '[A-Z]\\w*$' }],
102 // These rules should eventually be enabled.102 // These rules should eventually be enabled.
103 'no-async-promise-executor': 'off',103 'no-async-promise-executor': 'off',
104 'no-inner-declarations': 'off',104 'no-inner-declarations': 'off',
Dockerfile+15 -11
@@ -1,44 +1,48 @@
1FROM node:lts-alpine3.221FROM node:lts-alpine3.23
22
3# Arguments3# Arguments
4ARG APP_HOME=/home/node/app4ARG APP_HOME=/home/node/app
55
6# Install system dependencies6# Install system dependencies
7RUN apk add --no-cache gcompat tini git git-lfs7# "Don't rely on the base image for tools; if you call it, you install it." ;)
8RUN apk add --no-cache gcompat tini git git-lfs su-exec shadow dos2unix
89
9# Create app directory10# Create app directory and set ownership
10WORKDIR ${APP_HOME}11WORKDIR ${APP_HOME}
12RUN chown node:node ${APP_HOME}
1113
12# Set NODE_ENV to production14# Set NODE_ENV to production
13ENV NODE_ENV=production15ENV NODE_ENV=production
1416
15# Bundle app source17# Bundle app source and set ownership
16COPY . ./18COPY --chown=node:node . ./
1719
18RUN \20RUN \
19 echo "*** Install npm packages ***" && \21 echo "*** Install npm packages ***" && \
20 npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force22 npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force
2123
22# Create config directory and link config.yaml24# Create config directory and link config.yaml. Added hardcoded dirs(constants.js?)
25# that must be present for Non-Root Mode and volumeless docker runs.
23RUN \26RUN \
24 rm -f "config.yaml" || true && \27 rm -f "config.yaml" || true && \
25 ln -s "./config/config.yaml" "config.yaml" || true && \28 mkdir -p config data plugins public/scripts/extensions/third-party backups && \
26 mkdir "config" || true29 chown -R node:node config data plugins public/scripts/extensions/third-party backups && \
30 ln -s "./config/config.yaml" "config.yaml"
2731
28# Pre-compile public libraries32# Pre-compile public libraries
29RUN \33RUN \
30 echo "*** Run Webpack ***" && \34 echo "*** Run Webpack ***" && \
31 node "./docker/build-lib.js"35 node "./docker/build-lib.js"
3236
33# Set the entrypoint script37# Set the entrypoint script and cleanup
34RUN \38RUN \
35 echo "*** Cleanup ***" && \39 echo "*** Cleanup ***" && \
36 mv "./docker/docker-entrypoint.sh" "./" && \40 mv "./docker/docker-entrypoint.sh" "./" && \
37 rm -rf "./docker" && \
38 echo "*** Make docker-entrypoint.sh executable ***" && \41 echo "*** Make docker-entrypoint.sh executable ***" && \
39 chmod +x "./docker-entrypoint.sh" && \42 chmod +x "./docker-entrypoint.sh" && \
40 echo "*** Convert line endings to Unix format ***" && \43 echo "*** Convert line endings to Unix format ***" && \
41 dos2unix "./docker-entrypoint.sh"44 dos2unix "./docker-entrypoint.sh" && \
45 rm -rf "./docker"
4246
43# Fix extension repos permissions47# Fix extension repos permissions
44RUN git config --global --add safe.directory "*"48RUN git config --global --add safe.directory "*"
default/config.yaml+25 -2
@@ -38,6 +38,9 @@ browserLaunch:
38 avoidLocalhost: false38 avoidLocalhost: false
39# Server port39# Server port
40port: 800040port: 8000
41# Interval in seconds to write a heartbeat file. Set to 0 to disable.
42# This is used primarily for Docker healthchecks.
43heartbeatInterval: 0
41# -- SSL options --44# -- SSL options --
42ssl:45ssl:
43 # Enable SSL/TLS encryption46 # Enable SSL/TLS encryption
@@ -68,6 +71,25 @@ basicAuthUser:
68 password: "password"71 password: "password"
69# Enables CORS proxy middleware72# Enables CORS proxy middleware
70enableCorsProxy: false73enableCorsProxy: false
74# CORS settings (applied to all routes)
75cors:
76 # Enable or disable CORS middleware
77 enabled: true
78 # Allowed origins. Use "null" to match the default browser file origin.
79 # You can set "*" to allow any origin, or a list of allowed origins.
80 origin:
81 - "null"
82 # Allowed methods
83 methods:
84 - "OPTIONS"
85 # Allowed request headers (optional)
86 allowedHeaders: []
87 # Exposed response headers (optional)
88 exposedHeaders: []
89 # Allow credentials (cookies, authorization headers)
90 credentials: false
91 # Preflight cache max age in seconds (optional)
92 maxAge: null
71# -- REQUEST PROXY CONFIGURATION --93# -- REQUEST PROXY CONFIGURATION --
72requestProxy:94requestProxy:
73 # If a proxy is enabled, all outgoing HTTP/HTTPS requests will be routed through it.95 # If a proxy is enabled, all outgoing HTTP/HTTPS requests will be routed through it.
@@ -200,7 +222,6 @@ whitelistImportDomains:
200 - cdn.discordapp.com222 - cdn.discordapp.com
201 - files.catbox.moe223 - files.catbox.moe
202 - raw.githubusercontent.com224 - raw.githubusercontent.com
203 - char-archive.evulid.cc
204# API request overrides (for KoboldAI and Text Completion APIs)225# API request overrides (for KoboldAI and Text Completion APIs)
205## Note: host includes the port number if it's not the default (80 or 443)226## Note: host includes the port number if it's not the default (80 or 443)
206## Format is an array of objects:227## Format is an array of objects:
@@ -265,7 +286,7 @@ ollama:
265# -- ANTHROPIC CLAUDE API CONFIGURATION --286# -- ANTHROPIC CLAUDE API CONFIGURATION --
266claude:287claude:
267 # Enables caching of the system prompt (if supported).288 # Enables caching of the system prompt (if supported).
268 # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching289 # https://platform.claude.com/docs/en/build-with-claude/prompt-caching
269 # -- IMPORTANT! --290 # -- IMPORTANT! --
270 # Use only when the prompt before the chat history is static and doesn't change between requests291 # Use only when the prompt before the chat history is static and doesn't change between requests
271 # (e.g {{random}} macro or lorebooks not as in-chat injections).292 # (e.g {{random}} macro or lorebooks not as in-chat injections).
@@ -287,6 +308,8 @@ claude:
287gemini:308gemini:
288 # API endpoint version ("v1beta" or "v1alpha")309 # API endpoint version ("v1beta" or "v1alpha")
289 apiVersion: 'v1beta'310 apiVersion: 'v1beta'
311 # Adds thought signatures to requests (if available). Only for Gemini 3 and above.
312 thoughtSignatures: true
290 # Enables caching of the system prompt (if supported). Only for OpenRouter.313 # Enables caching of the system prompt (if supported). Only for OpenRouter.
291 # -- IMPORTANT! --314 # -- IMPORTANT! --
292 # Use only when the prompt before the chat history is static and doesn't change between requests315 # Use only when the prompt before the chat history is static and doesn't change between requests
docker/docker-compose.yml+7 -0
@@ -7,6 +7,7 @@ services:
7 environment:7 environment:
8 - NODE_ENV=production8 - NODE_ENV=production
9 - FORCE_COLOR=19 - FORCE_COLOR=1
10 - SILLYTAVERN_HEARTBEATINTERVAL=30
10 ports:11 ports:
11 - "8000:8000"12 - "8000:8000"
12 volumes:13 volumes:
@@ -14,4 +15,10 @@ services:
14 - "./data:/home/node/app/data"15 - "./data:/home/node/app/data"
15 - "./plugins:/home/node/app/plugins"16 - "./plugins:/home/node/app/plugins"
16 - "./extensions:/home/node/app/public/scripts/extensions/third-party"17 - "./extensions:/home/node/app/public/scripts/extensions/third-party"
18 healthcheck:
19 test: ["CMD", "node", "src/healthcheck.js"]
20 interval: 30s
21 timeout: 10s
22 start_period: 20s
23 retries: 3
17 restart: unless-stopped24 restart: unless-stopped
docker/docker-entrypoint.sh+90 -3
@@ -1,12 +1,99 @@
1#!/bin/sh1#!/bin/sh
22
3# Function to handle startup logic (Config check + Postinstall + Start)
4start_sillytavern() {
5 local PREFIX="$1"
6 shift # Remove the first argument (PREFIX) so $@ contains the rest
7
8 # Config Check
3 if [ ! -e "config/config.yaml" ]; then9 if [ ! -e "config/config.yaml" ]; then
4 echo "Resource not found, copying from defaults: config.yaml"10 echo "Resource not found, copying from defaults: config.yaml"
5 cp -r "default/config.yaml" "config/config.yaml"11 $PREFIX cp "default/config.yaml" "config/config.yaml"
6 fi12 fi
713
8 # Execute postinstall to auto-populate config.yaml with missing values14 # Execute postinstall to auto-populate config.yaml with missing values
9npm run postinstall15 $PREFIX npm run postinstall
1016
11 # Start the server17 # Start the server
12exec node server.js --listen "$@"18 exec $PREFIX node server.js --listen "$@"
19}
20
21# Dirs that MUST be present at this point (e.g for volumeless docker runs).
22# Please update list, if in the future a related perm issue appear.
23CORE_DIRS="config data plugins public/scripts/extensions/third-party backups"
24
25# Mounted Volumes (External)
26# Parse mounts, handling files vs directories
27RAW_MOUNTS=$(awk -v app_path="/home/node/app" '$2 ~ "^" app_path {print $2}' /proc/mounts)
28MOUNTED_DIRS=""
29
30for mount in $RAW_MOUNTS; do
31 if [ -f "$mount" ]; then
32 # If it is a mounted file (e.g. cert.pem), we want to check its PARENT directory
33 # so that the app can write adjacent files (e.g. key.pem).
34 PARENT_DIR=$(dirname "$mount")
35
36 # Performance Safety: If the file is in the root of the app,
37 # we do NOT add the parent (App Root), or we will recursively scan the whole app.
38 [ "$PARENT_DIR" != "/home/node/app" ] && MOUNTED_DIRS="$MOUNTED_DIRS $PARENT_DIR" || MOUNTED_DIRS="$MOUNTED_DIRS $mount"
39 else
40 # It is a directory, add it directly
41 MOUNTED_DIRS="$MOUNTED_DIRS $mount"
42 fi
43done
44
45# Combine dirs for checks
46CHECK_DIRS=$(echo "$CORE_DIRS $MOUNTED_DIRS" | tr ' ' '\n' | sort -u)
47
48# Ensure the needed directories exist
49for dir in $CHECK_DIRS; do
50 if [ ! -e "$dir" ]; then
51 echo "Creating missing directory: $dir"
52 mkdir -p "$dir" 2>/dev/null || echo "Warning: Could not create $dir" >&2
53 fi
54done
55
56# Mode Selection
57if [ "$(id -u)" = "0" ]; then
58 # Check if PUID/PGID variables are provided
59 if [ -n "$PUID" ] && [ -n "$PGID" ]; then
60 echo "Mode: PUID/PGID (UID:$PUID GID:$PGID)"
61
62 # Update the internal 'node' user to match requested IDs
63 groupmod -o -g "$PGID" node
64 usermod -o -u "$PUID" -g "$PGID" node
65
66 for dir in $CHECK_DIRS; do
67 if [ -d "$dir" ]; then
68 # Runs chown only if there is an mismatch
69 DIR_UID=$(stat -c '%u' "$dir")
70 DIR_GID=$(stat -c '%g' "$dir")
71
72 if [ "$DIR_UID" != "$PUID" ] || [ "$DIR_GID" != "$PGID" ]; then
73 echo "(Detected mismatch) Adjusting permissions for: $dir."
74 chown -R node:node "$dir" || echo "Warning: Failed to update permissions for '$dir'." >&2
75 fi
76 fi
77 done
78
79 # Fix config file specifically
80 chown node:node "config/config.yaml" 2>/dev/null
81
82 # Set execution prefix to run as 'node' user
83 EXEC_PREFIX="su-exec node:node"
84 else
85 # Default: Run as Root (original behavior)
86 echo "Mode: Default (Root)"
87 EXEC_PREFIX=""
88 fi
89
90else
91 # Non-Root Mode (Docker CLI --user flag)
92 echo "Mode: Strict Non-Root (UID: $(id -u))"
93 # We CANNOT auto-fix permissions in this mode because we lack privileges.
94 # Relying solely on the user configuring their host permissions correctly.
95 EXEC_PREFIX=""
96fi
97
98# Calling function with the determined prefix
99start_sillytavern "$EXEC_PREFIX" "$@"
package-lock.json+233 -164
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.15.0",3 "version": "1.16.0",
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.15.0",9 "version": "1.16.0",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -45,7 +45,7 @@
45 "bowser": "^2.12.1",45 "bowser": "^2.12.1",
46 "bytes": "^3.1.2",46 "bytes": "^3.1.2",
47 "chalk": "^5.6.0",47 "chalk": "^5.6.0",
48 "chevrotain": "^11.0.3",48 "chevrotain": "^11.1.1",
49 "command-exists": "^1.2.9",49 "command-exists": "^1.2.9",
50 "compression": "^1.8.1",50 "compression": "^1.8.1",
51 "cookie-parser": "^1.4.6",51 "cookie-parser": "^1.4.6",
@@ -67,6 +67,7 @@
67 "host-validation-middleware": "^0.1.1",67 "host-validation-middleware": "^0.1.1",
68 "html-entities": "^2.6.0",68 "html-entities": "^2.6.0",
69 "iconv-lite": "^0.6.3",69 "iconv-lite": "^0.6.3",
70 "image-size": "^2.0.2",
70 "ip-matching": "^2.1.2",71 "ip-matching": "^2.1.2",
71 "ip-regex": "^5.0.0",72 "ip-regex": "^5.0.0",
72 "ipaddr.js": "^2.2.0",73 "ipaddr.js": "^2.2.0",
@@ -174,42 +175,42 @@
174 "license": "Apache-2.0"175 "license": "Apache-2.0"
175 },176 },
176 "node_modules/@chevrotain/cst-dts-gen": {177 "node_modules/@chevrotain/cst-dts-gen": {
177 "version": "11.1.0",178 "version": "11.1.1",
178 "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.0.tgz",179 "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz",
179 "integrity": "sha512-Sa/G9XD23V4StfHMeQNnXbFmj8CsYUBmf+L895/hKm0RFDhxAfHzV6e58NA8j5ninntT5yqMzBW8QEbYxLkNUw==",180 "integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==",
180 "license": "Apache-2.0",181 "license": "Apache-2.0",
181 "dependencies": {182 "dependencies": {
182 "@chevrotain/gast": "11.1.0",183 "@chevrotain/gast": "11.1.1",
183 "@chevrotain/types": "11.1.0",184 "@chevrotain/types": "11.1.1",
184 "lodash-es": "4.17.21"185 "lodash-es": "4.17.23"
185 }186 }
186 },187 },
187 "node_modules/@chevrotain/gast": {188 "node_modules/@chevrotain/gast": {
188 "version": "11.1.0",189 "version": "11.1.1",
189 "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.0.tgz",190 "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz",
190 "integrity": "sha512-0fyRYDFneUhbyV6k22R6bBY02+FasLqcxXYVt8z51IWTZ10l2Z2Lc0hiPTgm8MNRbYZnDbNv78b9zY5DoIJKjQ==",191 "integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==",
191 "license": "Apache-2.0",192 "license": "Apache-2.0",
192 "dependencies": {193 "dependencies": {
193 "@chevrotain/types": "11.1.0",194 "@chevrotain/types": "11.1.1",
194 "lodash-es": "4.17.21"195 "lodash-es": "4.17.23"
195 }196 }
196 },197 },
197 "node_modules/@chevrotain/regexp-to-ast": {198 "node_modules/@chevrotain/regexp-to-ast": {
198 "version": "11.1.0",199 "version": "11.1.1",
199 "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.0.tgz",200 "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz",
200 "integrity": "sha512-3rW046uSp36liIAc/5G6A6h3gGbDN1eONpmJQpybIb+G2kSz0BNRc9ziT4DYrCUUbgNLd6bNVROqN9r7ZaajYg==",201 "integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==",
201 "license": "Apache-2.0"202 "license": "Apache-2.0"
202 },203 },
203 "node_modules/@chevrotain/types": {204 "node_modules/@chevrotain/types": {
204 "version": "11.1.0",205 "version": "11.1.1",
205 "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.0.tgz",206 "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz",
206 "integrity": "sha512-GXni/dwJAkClMfwCtrbGU19RXQ9O76hFxq3sgy/zufXNj3ov6J/8FOWIXxJLhnKx7gzSweATmRccjlpmr5W2nA==",207 "integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==",
207 "license": "Apache-2.0"208 "license": "Apache-2.0"
208 },209 },
209 "node_modules/@chevrotain/utils": {210 "node_modules/@chevrotain/utils": {
210 "version": "11.1.0",211 "version": "11.1.1",
211 "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.0.tgz",212 "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz",
212 "integrity": "sha512-DrS2yldzFnjmBV0O/kDngcFxWuqg2FdmUpaD6KyTmgIIE6lR53dq80R/Zz+o6LpUrXsLJk192kXuaeIPic4WVg==",213 "integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==",
213 "license": "Apache-2.0"214 "license": "Apache-2.0"
214 },215 },
215 "node_modules/@es-joy/jsdoccomment": {216 "node_modules/@es-joy/jsdoccomment": {
@@ -1474,17 +1475,13 @@
1474 }1475 }
1475 },1476 },
1476 "node_modules/@jridgewell/gen-mapping": {1477 "node_modules/@jridgewell/gen-mapping": {
1477 "version": "0.3.8",1478 "version": "0.3.13",
1478 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",1479 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
1479 "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",1480 "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
1480 "license": "MIT",1481 "license": "MIT",
1481 "dependencies": {1482 "dependencies": {
1482 "@jridgewell/set-array": "^1.2.1",1483 "@jridgewell/sourcemap-codec": "^1.5.0",
1483 "@jridgewell/sourcemap-codec": "^1.4.10",
1484 "@jridgewell/trace-mapping": "^0.3.24"1484 "@jridgewell/trace-mapping": "^0.3.24"
1485 },
1486 "engines": {
1487 "node": ">=6.0.0"
1488 }1485 }
1489 },1486 },
1490 "node_modules/@jridgewell/resolve-uri": {1487 "node_modules/@jridgewell/resolve-uri": {
@@ -1496,19 +1493,10 @@
1496 "node": ">=6.0.0"1493 "node": ">=6.0.0"
1497 }1494 }
1498 },1495 },
1499 "node_modules/@jridgewell/set-array": {
1500 "version": "1.2.1",
1501 "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
1502 "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
1503 "license": "MIT",
1504 "engines": {
1505 "node": ">=6.0.0"
1506 }
1507 },
1508 "node_modules/@jridgewell/source-map": {1496 "node_modules/@jridgewell/source-map": {
1509 "version": "0.3.6",1497 "version": "0.3.11",
1510 "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",1498 "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
1511 "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",1499 "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
1512 "license": "MIT",1500 "license": "MIT",
1513 "dependencies": {1501 "dependencies": {
1514 "@jridgewell/gen-mapping": "^0.3.5",1502 "@jridgewell/gen-mapping": "^0.3.5",
@@ -1516,15 +1504,15 @@
1516 }1504 }
1517 },1505 },
1518 "node_modules/@jridgewell/sourcemap-codec": {1506 "node_modules/@jridgewell/sourcemap-codec": {
1519 "version": "1.5.0",1507 "version": "1.5.5",
1520 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",1508 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
1521 "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",1509 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
1522 "license": "MIT"1510 "license": "MIT"
1523 },1511 },
1524 "node_modules/@jridgewell/trace-mapping": {1512 "node_modules/@jridgewell/trace-mapping": {
1525 "version": "0.3.25",1513 "version": "0.3.31",
1526 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",1514 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
1527 "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",1515 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
1528 "license": "MIT",1516 "license": "MIT",
1529 "dependencies": {1517 "dependencies": {
1530 "@jridgewell/resolve-uri": "^3.1.0",1518 "@jridgewell/resolve-uri": "^3.1.0",
@@ -1921,9 +1909,9 @@
1921 }1909 }
1922 },1910 },
1923 "node_modules/@types/estree": {1911 "node_modules/@types/estree": {
1924 "version": "1.0.6",1912 "version": "1.0.8",
1925 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",1913 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1926 "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",1914 "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1927 "license": "MIT"1915 "license": "MIT"
1928 },1916 },
1929 "node_modules/@types/express": {1917 "node_modules/@types/express": {
@@ -2618,9 +2606,9 @@
2618 }2606 }
2619 },2607 },
2620 "node_modules/acorn": {2608 "node_modules/acorn": {
2621 "version": "8.14.0",2609 "version": "8.15.0",
2622 "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",2610 "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
2623 "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",2611 "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
2624 "license": "MIT",2612 "license": "MIT",
2625 "peer": true,2613 "peer": true,
2626 "bin": {2614 "bin": {
@@ -2630,6 +2618,18 @@
2630 "node": ">=0.4.0"2618 "node": ">=0.4.0"
2631 }2619 }
2632 },2620 },
2621 "node_modules/acorn-import-phases": {
2622 "version": "1.0.4",
2623 "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
2624 "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
2625 "license": "MIT",
2626 "engines": {
2627 "node": ">=10.13.0"
2628 },
2629 "peerDependencies": {
2630 "acorn": "^8.14.0"
2631 }
2632 },
2633 "node_modules/acorn-jsx": {2633 "node_modules/acorn-jsx": {
2634 "version": "5.3.2",2634 "version": "5.3.2",
2635 "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",2635 "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -2998,13 +2998,13 @@
2998 }2998 }
2999 },2999 },
3000 "node_modules/axios": {3000 "node_modules/axios": {
3001 "version": "1.12.0",3001 "version": "1.13.5",
3002 "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",3002 "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
3003 "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",3003 "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
3004 "license": "MIT",3004 "license": "MIT",
3005 "dependencies": {3005 "dependencies": {
3006 "follow-redirects": "^1.15.6",3006 "follow-redirects": "^1.15.11",
3007 "form-data": "^4.0.4",3007 "form-data": "^4.0.5",
3008 "proxy-from-env": "^1.1.0"3008 "proxy-from-env": "^1.1.0"
3009 }3009 }
3010 },3010 },
@@ -3050,6 +3050,15 @@
3050 ],3050 ],
3051 "license": "MIT"3051 "license": "MIT"
3052 },3052 },
3053 "node_modules/baseline-browser-mapping": {
3054 "version": "2.9.19",
3055 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
3056 "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
3057 "license": "Apache-2.0",
3058 "bin": {
3059 "baseline-browser-mapping": "dist/cli.js"
3060 }
3061 },
3053 "node_modules/basic-ftp": {3062 "node_modules/basic-ftp": {
3054 "version": "5.0.5",3063 "version": "5.0.5",
3055 "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",3064 "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
@@ -3182,9 +3191,9 @@
3182 }3191 }
3183 },3192 },
3184 "node_modules/browserslist": {3193 "node_modules/browserslist": {
3185 "version": "4.24.0",3194 "version": "4.28.1",
3186 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",3195 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
3187 "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",3196 "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
3188 "funding": [3197 "funding": [
3189 {3198 {
3190 "type": "opencollective",3199 "type": "opencollective",
@@ -3202,10 +3211,11 @@
3202 "license": "MIT",3211 "license": "MIT",
3203 "peer": true,3212 "peer": true,
3204 "dependencies": {3213 "dependencies": {
3205 "caniuse-lite": "^1.0.30001663",3214 "baseline-browser-mapping": "^2.9.0",
3206 "electron-to-chromium": "^1.5.28",3215 "caniuse-lite": "^1.0.30001759",
3207 "node-releases": "^2.0.18",3216 "electron-to-chromium": "^1.5.263",
3208 "update-browserslist-db": "^1.1.0"3217 "node-releases": "^2.0.27",
3218 "update-browserslist-db": "^1.2.0"
3209 },3219 },
3210 "bin": {3220 "bin": {
3211 "browserslist": "cli.js"3221 "browserslist": "cli.js"
@@ -3364,9 +3374,9 @@
3364 }3374 }
3365 },3375 },
3366 "node_modules/caniuse-lite": {3376 "node_modules/caniuse-lite": {
3367 "version": "1.0.30001669",3377 "version": "1.0.30001768",
3368 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz",3378 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz",
3369 "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==",3379 "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==",
3370 "funding": [3380 "funding": [
3371 {3381 {
3372 "type": "opencollective",3382 "type": "opencollective",
@@ -3452,17 +3462,17 @@
3452 }3462 }
3453 },3463 },
3454 "node_modules/chevrotain": {3464 "node_modules/chevrotain": {
3455 "version": "11.1.0",3465 "version": "11.1.1",
3456 "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.0.tgz",3466 "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz",
3457 "integrity": "sha512-BqwSf3RDQlHQ+EyWqTLDd23IwJ3clav6QyNQM4FNj0RF2/HfXESPjrApKkEstV5jbyJtUB8U4zrUFdLd2Cx1oA==",3467 "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==",
3458 "license": "Apache-2.0",3468 "license": "Apache-2.0",
3459 "dependencies": {3469 "dependencies": {
3460 "@chevrotain/cst-dts-gen": "11.1.0",3470 "@chevrotain/cst-dts-gen": "11.1.1",
3461 "@chevrotain/gast": "11.1.0",3471 "@chevrotain/gast": "11.1.1",
3462 "@chevrotain/regexp-to-ast": "11.1.0",3472 "@chevrotain/regexp-to-ast": "11.1.1",
3463 "@chevrotain/types": "11.1.0",3473 "@chevrotain/types": "11.1.1",
3464 "@chevrotain/utils": "11.1.0",3474 "@chevrotain/utils": "11.1.1",
3465 "lodash-es": "4.17.21"3475 "lodash-es": "4.17.23"
3466 }3476 }
3467 },3477 },
3468 "node_modules/chrome-trace-event": {3478 "node_modules/chrome-trace-event": {
@@ -4311,9 +4321,9 @@
4311 "license": "MIT"4321 "license": "MIT"
4312 },4322 },
4313 "node_modules/electron-to-chromium": {4323 "node_modules/electron-to-chromium": {
4314 "version": "1.5.39",4324 "version": "1.5.286",
4315 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39.tgz",4325 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
4316 "integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg==",4326 "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
4317 "license": "ISC"4327 "license": "ISC"
4318 },4328 },
4319 "node_modules/emoji-regex": {4329 "node_modules/emoji-regex": {
@@ -4341,13 +4351,13 @@
4341 }4351 }
4342 },4352 },
4343 "node_modules/enhanced-resolve": {4353 "node_modules/enhanced-resolve": {
4344 "version": "5.17.1",4354 "version": "5.19.0",
4345 "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",4355 "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
4346 "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",4356 "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
4347 "license": "MIT",4357 "license": "MIT",
4348 "dependencies": {4358 "dependencies": {
4349 "graceful-fs": "^4.2.4",4359 "graceful-fs": "^4.2.4",
4350 "tapable": "^2.2.0"4360 "tapable": "^2.3.0"
4351 },4361 },
4352 "engines": {4362 "engines": {
4353 "node": ">=10.13.0"4363 "node": ">=10.13.0"
@@ -4402,6 +4412,7 @@
4402 "version": "1.5.4",4412 "version": "1.5.4",
4403 "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",4413 "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
4404 "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",4414 "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
4415 "dev": true,
4405 "license": "MIT"4416 "license": "MIT"
4406 },4417 },
4407 "node_modules/es-object-atoms": {4418 "node_modules/es-object-atoms": {
@@ -4964,9 +4975,9 @@
4964 "license": "MIT"4975 "license": "MIT"
4965 },4976 },
4966 "node_modules/fast-uri": {4977 "node_modules/fast-uri": {
4967 "version": "3.0.6",4978 "version": "3.1.0",
4968 "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",4979 "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
4969 "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",4980 "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
4970 "funding": [4981 "funding": [
4971 {4982 {
4972 "type": "github",4983 "type": "github",
@@ -5119,15 +5130,16 @@
5119 "license": "ISC"5130 "license": "ISC"
5120 },5131 },
5121 "node_modules/follow-redirects": {5132 "node_modules/follow-redirects": {
5122 "version": "1.15.6",5133 "version": "1.15.11",
5123 "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",5134 "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
5124 "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",5135 "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
5125 "funding": [5136 "funding": [
5126 {5137 {
5127 "type": "individual",5138 "type": "individual",
5128 "url": "https://github.com/sponsors/RubenVerborgh"5139 "url": "https://github.com/sponsors/RubenVerborgh"
5129 }5140 }
5130 ],5141 ],
5142 "license": "MIT",
5131 "engines": {5143 "engines": {
5132 "node": ">=4.0"5144 "node": ">=4.0"
5133 },5145 },
@@ -5153,9 +5165,9 @@
5153 }5165 }
5154 },5166 },
5155 "node_modules/form-data": {5167 "node_modules/form-data": {
5156 "version": "4.0.4",5168 "version": "4.0.5",
5157 "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",5169 "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
5158 "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",5170 "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
5159 "license": "MIT",5171 "license": "MIT",
5160 "dependencies": {5172 "dependencies": {
5161 "asynckit": "^0.4.0",5173 "asynckit": "^0.4.0",
@@ -5836,6 +5848,18 @@
5836 "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",5848 "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
5837 "license": "MIT"5849 "license": "MIT"
5838 },5850 },
5851 "node_modules/image-size": {
5852 "version": "2.0.2",
5853 "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
5854 "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==",
5855 "license": "MIT",
5856 "bin": {
5857 "image-size": "bin/image-size.js"
5858 },
5859 "engines": {
5860 "node": ">=16.x"
5861 }
5862 },
5839 "node_modules/immediate": {5863 "node_modules/immediate": {
5840 "version": "3.0.6",5864 "version": "3.0.6",
5841 "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",5865 "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -6338,12 +6362,16 @@
6338 }6362 }
6339 },6363 },
6340 "node_modules/loader-runner": {6364 "node_modules/loader-runner": {
6341 "version": "4.3.0",6365 "version": "4.3.1",
6342 "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",6366 "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
6343 "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",6367 "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
6344 "license": "MIT",6368 "license": "MIT",
6345 "engines": {6369 "engines": {
6346 "node": ">=6.11.5"6370 "node": ">=6.11.5"
6371 },
6372 "funding": {
6373 "type": "opencollective",
6374 "url": "https://opencollective.com/webpack"
6347 }6375 }
6348 },6376 },
6349 "node_modules/localforage": {6377 "node_modules/localforage": {
@@ -6732,10 +6760,37 @@
6732 "node": ">=10.12.0"6760 "node": ">=10.12.0"
6733 }6761 }
6734 },6762 },
6763 "node_modules/node-persist/node_modules/p-limit": {
6764 "version": "3.1.0",
6765 "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
6766 "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
6767 "license": "MIT",
6768 "dependencies": {
6769 "yocto-queue": "^0.1.0"
6770 },
6771 "engines": {
6772 "node": ">=10"
6773 },
6774 "funding": {
6775 "url": "https://github.com/sponsors/sindresorhus"
6776 }
6777 },
6778 "node_modules/node-persist/node_modules/yocto-queue": {
6779 "version": "0.1.0",
6780 "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
6781 "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
6782 "license": "MIT",
6783 "engines": {
6784 "node": ">=10"
6785 },
6786 "funding": {
6787 "url": "https://github.com/sponsors/sindresorhus"
6788 }
6789 },
6735 "node_modules/node-releases": {6790 "node_modules/node-releases": {
6736 "version": "2.0.18",6791 "version": "2.0.27",
6737 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",6792 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
6738 "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",6793 "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
6739 "license": "MIT"6794 "license": "MIT"
6740 },6795 },
6741 "node_modules/normalize-path": {6796 "node_modules/normalize-path": {
@@ -6941,10 +6996,27 @@
6941 "node": ">=8"6996 "node": ">=8"
6942 }6997 }
6943 },6998 },
6944 "node_modules/p-limit": {6999 "node_modules/p-locate": {
7000 "version": "5.0.0",
7001 "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
7002 "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
7003 "dev": true,
7004 "license": "MIT",
7005 "dependencies": {
7006 "p-limit": "^3.0.2"
7007 },
7008 "engines": {
7009 "node": ">=10"
7010 },
7011 "funding": {
7012 "url": "https://github.com/sponsors/sindresorhus"
7013 }
7014 },
7015 "node_modules/p-locate/node_modules/p-limit": {
6945 "version": "3.1.0",7016 "version": "3.1.0",
6946 "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",7017 "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
6947 "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",7018 "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
7019 "dev": true,
6948 "license": "MIT",7020 "license": "MIT",
6949 "dependencies": {7021 "dependencies": {
6950 "yocto-queue": "^0.1.0"7022 "yocto-queue": "^0.1.0"
@@ -6956,15 +7028,12 @@
6956 "url": "https://github.com/sponsors/sindresorhus"7028 "url": "https://github.com/sponsors/sindresorhus"
6957 }7029 }
6958 },7030 },
6959 "node_modules/p-locate": {7031 "node_modules/p-locate/node_modules/yocto-queue": {
6960 "version": "5.0.0",7032 "version": "0.1.0",
6961 "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",7033 "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
6962 "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",7034 "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
6963 "dev": true,7035 "dev": true,
6964 "license": "MIT",7036 "license": "MIT",
6965 "dependencies": {
6966 "p-limit": "^3.0.2"
6967 },
6968 "engines": {7037 "engines": {
6969 "node": ">=10"7038 "node": ">=10"
6970 },7039 },
@@ -7221,9 +7290,9 @@
7221 }7290 }
7222 },7291 },
7223 "node_modules/picocolors": {7292 "node_modules/picocolors": {
7224 "version": "1.1.0",7293 "version": "1.1.1",
7225 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",7294 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
7226 "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",7295 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
7227 "license": "ISC"7296 "license": "ISC"
7228 },7297 },
7229 "node_modules/picomatch": {7298 "node_modules/picomatch": {
@@ -7419,9 +7488,9 @@
7419 }7488 }
7420 },7489 },
7421 "node_modules/qs": {7490 "node_modules/qs": {
7422 "version": "6.14.1",7491 "version": "6.14.2",
7423 "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",7492 "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
7424 "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",7493 "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
7425 "license": "BSD-3-Clause",7494 "license": "BSD-3-Clause",
7426 "dependencies": {7495 "dependencies": {
7427 "side-channel": "^1.1.0"7496 "side-channel": "^1.1.0"
@@ -7811,9 +7880,9 @@
7811 "license": "ISC"7880 "license": "ISC"
7812 },7881 },
7813 "node_modules/schema-utils": {7882 "node_modules/schema-utils": {
7814 "version": "4.3.2",7883 "version": "4.3.3",
7815 "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",7884 "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
7816 "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",7885 "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
7817 "license": "MIT",7886 "license": "MIT",
7818 "dependencies": {7887 "dependencies": {
7819 "@types/json-schema": "^7.0.9",7888 "@types/json-schema": "^7.0.9",
@@ -8447,12 +8516,16 @@
8447 }8516 }
8448 },8517 },
8449 "node_modules/tapable": {8518 "node_modules/tapable": {
8450 "version": "2.2.1",8519 "version": "2.3.0",
8451 "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",8520 "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
8452 "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",8521 "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
8453 "license": "MIT",8522 "license": "MIT",
8454 "engines": {8523 "engines": {
8455 "node": ">=6"8524 "node": ">=6"
8525 },
8526 "funding": {
8527 "type": "opencollective",
8528 "url": "https://opencollective.com/webpack"
8456 }8529 }
8457 },8530 },
8458 "node_modules/tar-stream": {8531 "node_modules/tar-stream": {
@@ -8466,13 +8539,13 @@
8466 }8539 }
8467 },8540 },
8468 "node_modules/terser": {8541 "node_modules/terser": {
8469 "version": "5.39.0",8542 "version": "5.46.0",
8470 "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",8543 "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
8471 "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",8544 "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
8472 "license": "BSD-2-Clause",8545 "license": "BSD-2-Clause",
8473 "dependencies": {8546 "dependencies": {
8474 "@jridgewell/source-map": "^0.3.3",8547 "@jridgewell/source-map": "^0.3.3",
8475 "acorn": "^8.8.2",8548 "acorn": "^8.15.0",
8476 "commander": "^2.20.0",8549 "commander": "^2.20.0",
8477 "source-map-support": "~0.5.20"8550 "source-map-support": "~0.5.20"
8478 },8551 },
@@ -8484,9 +8557,9 @@
8484 }8557 }
8485 },8558 },
8486 "node_modules/terser-webpack-plugin": {8559 "node_modules/terser-webpack-plugin": {
8487 "version": "5.3.12",8560 "version": "5.3.16",
8488 "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.12.tgz",8561 "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
8489 "integrity": "sha512-jDLYqo7oF8tJIttjXO6jBY5Hk8p3A8W4ttih7cCEq64fQFWmgJ4VqAQjKr7WwIDlmXKEc6QeoRb5ecjZ+2afcg==",8562 "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
8490 "license": "MIT",8563 "license": "MIT",
8491 "dependencies": {8564 "dependencies": {
8492 "@jridgewell/trace-mapping": "^0.3.25",8565 "@jridgewell/trace-mapping": "^0.3.25",
@@ -8740,9 +8813,9 @@
8740 }8813 }
8741 },8814 },
8742 "node_modules/update-browserslist-db": {8815 "node_modules/update-browserslist-db": {
8743 "version": "1.1.1",8816 "version": "1.2.3",
8744 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",8817 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
8745 "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",8818 "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
8746 "funding": [8819 "funding": [
8747 {8820 {
8748 "type": "opencollective",8821 "type": "opencollective",
@@ -8760,7 +8833,7 @@
8760 "license": "MIT",8833 "license": "MIT",
8761 "dependencies": {8834 "dependencies": {
8762 "escalade": "^3.2.0",8835 "escalade": "^3.2.0",
8763 "picocolors": "^1.1.0"8836 "picocolors": "^1.1.1"
8764 },8837 },
8765 "bin": {8838 "bin": {
8766 "update-browserslist-db": "cli.js"8839 "update-browserslist-db": "cli.js"
@@ -8865,9 +8938,9 @@
8865 "license": "Apache-2.0"8938 "license": "Apache-2.0"
8866 },8939 },
8867 "node_modules/watchpack": {8940 "node_modules/watchpack": {
8868 "version": "2.4.2",8941 "version": "2.5.1",
8869 "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",8942 "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
8870 "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",8943 "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
8871 "license": "MIT",8944 "license": "MIT",
8872 "dependencies": {8945 "dependencies": {
8873 "glob-to-regexp": "^0.4.1",8946 "glob-to-regexp": "^0.4.1",
@@ -8908,34 +8981,36 @@
8908 }8981 }
8909 },8982 },
8910 "node_modules/webpack": {8983 "node_modules/webpack": {
8911 "version": "5.98.0",8984 "version": "5.105.0",
8912 "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",8985 "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz",
8913 "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",8986 "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==",
8914 "license": "MIT",8987 "license": "MIT",
8915 "dependencies": {8988 "dependencies": {
8916 "@types/eslint-scope": "^3.7.7",8989 "@types/eslint-scope": "^3.7.7",
8917 "@types/estree": "^1.0.6",8990 "@types/estree": "^1.0.8",
8991 "@types/json-schema": "^7.0.15",
8918 "@webassemblyjs/ast": "^1.14.1",8992 "@webassemblyjs/ast": "^1.14.1",
8919 "@webassemblyjs/wasm-edit": "^1.14.1",8993 "@webassemblyjs/wasm-edit": "^1.14.1",
8920 "@webassemblyjs/wasm-parser": "^1.14.1",8994 "@webassemblyjs/wasm-parser": "^1.14.1",
8921 "acorn": "^8.14.0",8995 "acorn": "^8.15.0",
8922 "browserslist": "^4.24.0",8996 "acorn-import-phases": "^1.0.3",
8997 "browserslist": "^4.28.1",
8923 "chrome-trace-event": "^1.0.2",8998 "chrome-trace-event": "^1.0.2",
8924 "enhanced-resolve": "^5.17.1",8999 "enhanced-resolve": "^5.19.0",
8925 "es-module-lexer": "^1.2.1",9000 "es-module-lexer": "^2.0.0",
8926 "eslint-scope": "5.1.1",9001 "eslint-scope": "5.1.1",
8927 "events": "^3.2.0",9002 "events": "^3.2.0",
8928 "glob-to-regexp": "^0.4.1",9003 "glob-to-regexp": "^0.4.1",
8929 "graceful-fs": "^4.2.11",9004 "graceful-fs": "^4.2.11",
8930 "json-parse-even-better-errors": "^2.3.1",9005 "json-parse-even-better-errors": "^2.3.1",
8931 "loader-runner": "^4.2.0",9006 "loader-runner": "^4.3.1",
8932 "mime-types": "^2.1.27",9007 "mime-types": "^2.1.27",
8933 "neo-async": "^2.6.2",9008 "neo-async": "^2.6.2",
8934 "schema-utils": "^4.3.0",9009 "schema-utils": "^4.3.3",
8935 "tapable": "^2.1.1",9010 "tapable": "^2.3.0",
8936 "terser-webpack-plugin": "^5.3.11",9011 "terser-webpack-plugin": "^5.3.16",
8937 "watchpack": "^2.4.1",9012 "watchpack": "^2.5.1",
8938 "webpack-sources": "^3.2.3"9013 "webpack-sources": "^3.3.3"
8939 },9014 },
8940 "bin": {9015 "bin": {
8941 "webpack": "bin/webpack.js"9016 "webpack": "bin/webpack.js"
@@ -8954,14 +9029,20 @@
8954 }9029 }
8955 },9030 },
8956 "node_modules/webpack-sources": {9031 "node_modules/webpack-sources": {
8957 "version": "3.2.3",9032 "version": "3.3.3",
8958 "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",9033 "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
8959 "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",9034 "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
8960 "license": "MIT",9035 "license": "MIT",
8961 "engines": {9036 "engines": {
8962 "node": ">=10.13.0"9037 "node": ">=10.13.0"
8963 }9038 }
8964 },9039 },
9040 "node_modules/webpack/node_modules/es-module-lexer": {
9041 "version": "2.0.0",
9042 "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
9043 "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
9044 "license": "MIT"
9045 },
8965 "node_modules/webpack/node_modules/eslint-scope": {9046 "node_modules/webpack/node_modules/eslint-scope": {
8966 "version": "5.1.1",9047 "version": "5.1.1",
8967 "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",9048 "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -9236,18 +9317,6 @@
9236 "node": ">=12"9317 "node": ">=12"
9237 }9318 }
9238 },9319 },
9239 "node_modules/yocto-queue": {
9240 "version": "0.1.0",
9241 "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
9242 "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
9243 "license": "MIT",
9244 "engines": {
9245 "node": ">=10"
9246 },
9247 "funding": {
9248 "url": "https://github.com/sponsors/sindresorhus"
9249 }
9250 },
9251 "node_modules/zip-stream": {9320 "node_modules/zip-stream": {
9252 "version": "6.0.1",9321 "version": "6.0.1",
9253 "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",9322 "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
package.json+3 -8
@@ -35,7 +35,7 @@
35 "bowser": "^2.12.1",35 "bowser": "^2.12.1",
36 "bytes": "^3.1.2",36 "bytes": "^3.1.2",
37 "chalk": "^5.6.0",37 "chalk": "^5.6.0",
38 "chevrotain": "^11.0.3",38 "chevrotain": "^11.1.1",
39 "command-exists": "^1.2.9",39 "command-exists": "^1.2.9",
40 "compression": "^1.8.1",40 "compression": "^1.8.1",
41 "cookie-parser": "^1.4.6",41 "cookie-parser": "^1.4.6",
@@ -57,6 +57,7 @@
57 "host-validation-middleware": "^0.1.1",57 "host-validation-middleware": "^0.1.1",
58 "html-entities": "^2.6.0",58 "html-entities": "^2.6.0",
59 "iconv-lite": "^0.6.3",59 "iconv-lite": "^0.6.3",
60 "image-size": "^2.0.2",
60 "ip-matching": "^2.1.2",61 "ip-matching": "^2.1.2",
61 "ip-regex": "^5.0.0",62 "ip-regex": "^5.0.0",
62 "ipaddr.js": "^2.2.0",63 "ipaddr.js": "^2.2.0",
@@ -99,14 +100,8 @@
99 "vectra": {100 "vectra": {
100 "openai": "^4.17.0"101 "openai": "^4.17.0"
101 },102 },
102 "axios": {
103 "follow-redirects": "^1.15.4"
104 },
105 "node-fetch": {103 "node-fetch": {
106 "whatwg-url": "^14.0.0"104 "whatwg-url": "^14.0.0"
107 },
108 "chevrotain": {
109 "lodash-es": "^4.17.23"
110 }105 }
111 },106 },
112 "name": "sillytavern",107 "name": "sillytavern",
@@ -116,7 +111,7 @@
116 "type": "git",111 "type": "git",
117 "url": "https://github.com/SillyTavern/SillyTavern.git"112 "url": "https://github.com/SillyTavern/SillyTavern.git"
118 },113 },
119 "version": "1.15.0",114 "version": "1.16.0",
120 "scripts": {115 "scripts": {
121 "start": "node server.js",116 "start": "node server.js",
122 "debug": "node --inspect server.js",117 "debug": "node --inspect server.js",
public/css/backgrounds.css+6 -0
@@ -96,6 +96,12 @@
96 font-size: calc(var(--mainFontSize) * 0.95);96 font-size: calc(var(--mainFontSize) * 0.95);
97}97}
9898
99#bg-sort {
100 width: auto;
101 max-width: 6em;
102 flex-shrink: 0;
103}
104
99/* Thumbnails */105/* Thumbnails */
100.bg_example:hover .BGSampleTitle {106.bg_example:hover .BGSampleTitle {
101 opacity: 1;107 opacity: 1;
public/css/extensions-panel.css+5 -1
@@ -39,7 +39,7 @@ label[for="extensions_autoconnect"] {
39 text-align: left;39 text-align: left;
40}40}
4141
42.extensions_info h3 {42.extensions_info h3:not(.margin0) {
43 margin-bottom: 0.5em;43 margin-bottom: 0.5em;
44}44}
4545
@@ -112,6 +112,10 @@ label[for="extensions_autoconnect"] {
112 color: limegreen;112 color: limegreen;
113}113}
114114
115.extensions_info .third_party_toolbar {
116 user-select: none;
117}
118
115input.extension_missing[type="checkbox"] {119input.extension_missing[type="checkbox"] {
116 opacity: 0.5;120 opacity: 0.5;
117}121}
public/css/macros.css+88 -0
@@ -465,6 +465,89 @@
465 color: #F89406;465 color: #F89406;
466}466}
467467
468/* Arity warning banner in details */
469.macro-ac-warning {
470 display: flex;
471 align-items: baseline;
472 gap: 0.5em;
473 padding: 0.5em 0.75em;
474 background: linear-gradient(90deg, rgba(248, 148, 6, 0.2), transparent);
475 border-left: 3px solid #F89406;
476 border-radius: 0 4px 4px 0;
477 margin-bottom: 0.5em;
478 font-size: 0.9em;
479 color: #F89406;
480}
481
482.macro-ac-warning i {
483 font-size: 0.9em;
484}
485
486/* Scoped content info banner in details */
487.macro-ac-scoped-info {
488 display: flex;
489 align-items: baseline;
490 gap: 0.5em;
491 padding: 0.5em 0.75em;
492 background: linear-gradient(90deg, rgba(91, 192, 222, 0.2), transparent);
493 border-left: 3px solid #5BC0DE;
494 border-radius: 0 4px 4px 0;
495 margin-bottom: 0.5em;
496 font-size: 0.9em;
497 color: #5BC0DE;
498}
499
500.macro-ac-scoped-info i {
501 font-size: 0.9em;
502}
503
504.macro-ac-scoped-info code {
505 background: rgba(91, 192, 222, 0.15);
506 padding: 0.1em 0.3em;
507 border-radius: 3px;
508}
509
510/* OPTIONAL badge for optional scoped content */
511.macro-ac-optional-badge {
512 display: inline-block;
513 background: linear-gradient(135deg, #f0ad4e, #ec971f);
514 color: #000;
515 font-weight: bold;
516 font-size: 0.75em;
517 padding: 0.15em 0.5em;
518 border-radius: 3px;
519 text-transform: uppercase;
520 letter-spacing: 0.05em;
521 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
522}
523
524/* Smaller variant for use in autocomplete list items */
525.macro-ac-optional-badge-small {
526 font-size: 0.65em;
527 padding: 0.1em 0.35em;
528 vertical-align: middle;
529 color: #f0ad4e;
530}
531
532/* Closing tag autocomplete option */
533.autoComplete > .item.macro-closing-tag-item > .type {
534 color: var(--ac-color-matchedText, var(--SmartThemeBorderColor));
535}
536
537.macro-closing-tag-details {
538 padding: 0.5em;
539}
540
541.macro-closing-tag-details h3 {
542 margin: 0 0 0.5em 0;
543 font-size: 1.1em;
544}
545
546.macro-closing-tag-details p {
547 margin: 0;
548 opacity: 0.9;
549}
550
468/* Current argument hint banner in details */551/* Current argument hint banner in details */
469.macro-ac-arg-hint {552.macro-ac-arg-hint {
470 display: flex;553 display: flex;
@@ -483,6 +566,11 @@
483 font-size: 0.8em;566 font-size: 0.8em;
484}567}
485568
569.macro-ac-arg-hint .macro-ac-arg-hint-small {
570 font-size: 0.85em;
571 opacity: 0.8;
572}
573
486.macro-ac-hint-type {574.macro-ac-hint-type {
487 font-family: var(--monoFontFamily);575 font-family: var(--monoFontFamily);
488 font-size: 0.85em;576 font-size: 0.85em;
public/css/tags.css+4 -0
@@ -67,6 +67,10 @@
67 display: none;67 display: none;
68}68}
6969
70.tag.tag-absent {
71 text-decoration: line-through;
72}
73
70.tag.actionable {74.tag.actionable {
71 border-radius: 50%;75 border-radius: 50%;
72 aspect-ratio: 1 / 1;76 aspect-ratio: 1 / 1;
public/css/toggle-dependent.css+4 -0
@@ -564,3 +564,7 @@ label[for="bind_preset_to_connection"]:has(input:checked) {
564#request_images_block:has(#openai_request_images:not(:checked)) #request_images_settings {564#request_images_block:has(#openai_request_images:not(:checked)) #request_images_settings {
565 display: none;565 display: none;
566}566}
567
568#adaptive_p_block:has([data-tg-samplers="adaptive_target"][style*="display: none"]):has([data-tg-samplers="adaptive_decay"][style*="display: none"]) {
569 display: none;
570}
public/css/welcome.css+9 -0
@@ -115,6 +115,7 @@ body.hideChatAvatars .welcomePanel .recentChatList .recentChat .avatar {
115 cursor: pointer;115 cursor: pointer;
116 gap: 10px;116 gap: 10px;
117 border: 1px solid var(--SmartThemeBorderColor);117 border: 1px solid var(--SmartThemeBorderColor);
118 position: relative;
118}119}
119120
120.welcomeRecent .recentChatList .recentChat .avatar {121.welcomeRecent .recentChatList .recentChat .avatar {
@@ -222,6 +223,14 @@ body.big-avatars .welcomeRecent .recentChatList .recentChat .chatMessageContaine
222 transform: rotate(180deg);223 transform: rotate(180deg);
223}224}
224225
226.welcomeRecent .recentChatList .recentChat .recentChatPinned {
227 top: 1px;
228 left: 1px;
229 position: absolute;
230 opacity: 0.8;
231 color: var(--SmartThemeQuoteColor);
232}
233
225@media screen and (max-width: 1000px) {234@media screen and (max-width: 1000px) {
226 .welcomePanel .welcomeShortcuts a span {235 .welcomePanel .welcomeShortcuts a span {
227 display: none;236 display: none;
public/global.d.ts+9 -0
@@ -38,6 +38,7 @@ declare global {
38 avatar_url?: string;38 avatar_url?: string;
39 hideMutedSprites?: boolean;39 hideMutedSprites?: boolean;
40 fav?: boolean;40 fav?: boolean;
41 date_last_chat?: MessageTimestamp;
41 }42 }
4243
43 interface ChatFile extends Array<ChatMessage> {44 interface ChatFile extends Array<ChatMessage> {
@@ -235,3 +236,11 @@ declare global {
235236
236 type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>;237 type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>;
237}238}
239
240//Overrides for public/scripts/chats.js
241declare module 'dompurify' {
242 interface Config {
243 MESSAGE_SANITIZE?: boolean;
244 MESSAGE_ALLOW_SYSTEM_UI?: boolean;
245 }
246}
public/index.html+131 -54
@@ -1399,6 +1399,28 @@
1399 <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />1399 <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />
1400 <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="max_tokens_second_textgenerationwebui" id="max_tokens_second_counter_textgenerationwebui">1400 <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="max_tokens_second_textgenerationwebui" id="max_tokens_second_counter_textgenerationwebui">
1401 </div>1401 </div>
1402
1403 <div data-tg-type="koboldcpp, llamacpp, tabby" id="adaptive_p_block" class="wide100p">
1404 <h4 class="wide100p textAlignCenter">
1405 <label data-i18n="Adaptive-P">Adaptive-P</label>
1406 <a href="https://github.com/MrJackSpade/adaptive-p-docs" target="_blank">
1407 <div class="fa-solid fa-circle-info opacity50p"></div>
1408 </a>
1409 </h4>
1410 <div class="flex-container flexFlowRow alignitemscenter gap10px flexShrink">
1411 <div data-tg-samplers="adaptive_target" class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0">
1412 <small data-i18n="Target">Target</small>
1413 <input class="neo-range-slider" type="range" id="adaptive_target_textgenerationwebui" min="-0.01" max="1" step="0.01" />
1414 <input class="neo-range-input" type="number" min="-0.01" max="1" step="0.01" data-for="adaptive_target_textgenerationwebui" id="adaptive_target_counter_textgenerationwebui">
1415 </div>
1416 <div data-tg-samplers="adaptive_decay" class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0">
1417 <small data-i18n="Decay">Decay</small>
1418 <input class="neo-range-slider" type="range" id="adaptive_decay_textgenerationwebui" min="0" max="0.99" step="0.01" />
1419 <input class="neo-range-input" type="number" min="0" max="0.99" step="0.01" data-for="adaptive_decay_textgenerationwebui" id="adaptive_decay_counter_textgenerationwebui">
1420 </div>
1421 </div>
1422 </div>
1423
1402 <div data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" data-tg-samplers="smoothing_factor" id="smoothingBlock" name="smoothingBlock" class="wide100p">1424 <div data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" data-tg-samplers="smoothing_factor" id="smoothingBlock" name="smoothingBlock" class="wide100p">
1403 <h4 class="wide100p textAlignCenter">1425 <h4 class="wide100p textAlignCenter">
1404 <label data-i18n="Smooth Sampling">Smooth Sampling</label>1426 <label data-i18n="Smooth Sampling">Smooth Sampling</label>
@@ -1974,7 +1996,7 @@
1974 </b>1996 </b>
1975 </div>1997 </div>
1976 </div>1998 </div>
1977 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai">1999 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai,nanogpt">
1978 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">2000 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1979 <input id="openai_function_calling" type="checkbox" />2001 <input id="openai_function_calling" type="checkbox" />
1980 <span data-i18n="Enable function calling">Enable function calling</span>2002 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -2002,7 +2024,7 @@
2002 <i class="icon-supported fa-solid fa-film" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>2024 <i class="icon-supported fa-solid fa-film" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
2003 <i class="icon-unsupported fa-solid fa-film" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>2025 <i class="icon-unsupported fa-solid fa-film" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2004 </div>2026 </div>
2005 <div id="openai_audio_inlining_supported" data-source="makersuite,vertexai,openrouter">2027 <div id="openai_audio_inlining_supported" data-source="makersuite,vertexai,openrouter,openai,custom">
2006 <i class="icon-supported fa-solid fa-music" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>2028 <i class="icon-supported fa-solid fa-music" title="Supported by the current model" data-i18n="[title]Supported by the current model"></i>
2007 <i class="icon-unsupported fa-solid fa-music" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>2029 <i class="icon-unsupported fa-solid fa-music" title="Unsupported by the current model" data-i18n="[title]Unsupported by the current model"></i>
2008 </div>2030 </div>
@@ -2083,12 +2105,12 @@
2083 <span data-i18n="Allows the model to return its thinking process.">2105 <span data-i18n="Allows the model to return its thinking process.">
2084 Allows the model to return its thinking process.2106 Allows the model to return its thinking process.
2085 </span>2107 </span>
2086 <strong data-i18n="This setting affects visibility only." data-source-mode="except" data-source="zai">2108 <strong data-i18n="This setting affects visibility only." data-source-mode="except" data-source="zai,moonshot">
2087 This setting affects visibility only.2109 This setting affects visibility only.
2088 </strong>2110 </strong>
2089 </div>2111 </div>
2090 </div>2112 </div>
2091 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes">2113 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes,nanogpt">
2092 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">2114 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
2093 <label for="openai_reasoning_effort">2115 <label for="openai_reasoning_effort">
2094 <span data-i18n="Reasoning Effort">Reasoning Effort</span>2116 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2419,6 +2441,20 @@
2419 <span data-i18n="Allow fallback providers">Allow fallback providers</span>2441 <span data-i18n="Allow fallback providers">Allow fallback providers</span>
2420 </label>2442 </label>
2421 </div>2443 </div>
2444 <div>
2445 <h4 data-i18n="Model Quantizations">Model Quantizations</h4>
2446 <select id="openrouter_quantizations_text" class="openrouter_quantizations" multiple>
2447 <option data-i18n="Integer (4 bit)" value="int4">Integer (4 bit)</option>
2448 <option data-i18n="Integer (8 bit)" value="int8">Integer (8 bit)</option>
2449 <option data-i18n="Floating point (4 bit)" value="fp4">Floating point (4 bit)</option>
2450 <option data-i18n="Floating point (6 bit)" value="fp6">Floating point (6 bit)</option>
2451 <option data-i18n="Floating point (8 bit)" value="fp8">Floating point (8 bit)</option>
2452 <option data-i18n="Floating point (16 bit)" value="fp16">Floating point (16 bit)</option>
2453 <option data-i18n="Brain floating point (16 bit)" value="bf16">Brain floating point (16 bit)</option>
2454 <option data-i18n="Floating point (32 bit)" value="fp32">Floating point (32 bit)</option>
2455 <option data-i18n="Unknown" value="unknown">Unknown</option>
2456 </select>
2457 </div>
2422 </div>2458 </div>
2423 <div data-tg-type="infermaticai" class="flex-container flexFlowColumn">2459 <div data-tg-type="infermaticai" class="flex-container flexFlowColumn">
2424 <h4 data-i18n="InfermaticAI API Key">InfermaticAI API Key</h4>2460 <h4 data-i18n="InfermaticAI API Key">InfermaticAI API Key</h4>
@@ -2845,7 +2881,7 @@
2845 <option value="zai">Z.AI (GLM)</option>2881 <option value="zai">Z.AI (GLM)</option>
2846 </optgroup>2882 </optgroup>
2847 </select>2883 </select>
2848 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,vertexai,deepseek,xai">2884 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,vertexai,deepseek,xai,zai,moonshot">
2849 <div class="inline-drawer-toggle inline-drawer-header">2885 <div class="inline-drawer-toggle inline-drawer-header">
2850 <b data-i18n="Reverse Proxy">Reverse Proxy</b>2886 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
2851 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>2887 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2909,7 +2945,7 @@
2909 </div>2945 </div>
2910 </div>2946 </div>
2911 </div>2947 </div>
2912 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,vertexai,deepseek,xai">2948 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,vertexai,deepseek,xai,zai,moonshot">
2913 <div class="reverse_proxy_warning">2949 <div class="reverse_proxy_warning">
2914 <b>2950 <b>
2915 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">2951 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
@@ -3068,6 +3104,7 @@
3068 <h4 data-i18n="Claude Model">Claude Model</h4>3104 <h4 data-i18n="Claude Model">Claude Model</h4>
3069 <select id="model_claude_select">3105 <select id="model_claude_select">
3070 <optgroup label="Versions">3106 <optgroup label="Versions">
3107 <option value="claude-opus-4-6">claude-opus-4-6</option>
3071 <option value="claude-opus-4-5">claude-opus-4-5</option>3108 <option value="claude-opus-4-5">claude-opus-4-5</option>
3072 <option value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>3109 <option value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>
3073 <option value="claude-sonnet-4-5">claude-sonnet-4-5</option>3110 <option value="claude-sonnet-4-5">claude-sonnet-4-5</option>
@@ -3161,6 +3198,20 @@
3161 <i class="fa-solid fa-lightbulb"></i>3198 <i class="fa-solid fa-lightbulb"></i>
3162 <span data-i18n="To use instruct formatting, switch to OpenRouter under Text Completion API.">To use instruct formatting, switch to OpenRouter under Text Completion API.</span>3199 <span data-i18n="To use instruct formatting, switch to OpenRouter under Text Completion API.">To use instruct formatting, switch to OpenRouter under Text Completion API.</span>
3163 </small>3200 </small>
3201 <div>
3202 <h4 data-i18n="Model Quantizations">Model Quantizations</h4>
3203 <select id="openrouter_quantizations_chat" class="openrouter_quantizations" multiple>
3204 <option data-i18n="Integer (4 bit)" value="int4">Integer (4 bit)</option>
3205 <option data-i18n="Integer (8 bit)" value="int8">Integer (8 bit)</option>
3206 <option data-i18n="Floating point (4 bit)" value="fp4">Floating point (4 bit)</option>
3207 <option data-i18n="Floating point (6 bit)" value="fp6">Floating point (6 bit)</option>
3208 <option data-i18n="Floating point (8 bit)" value="fp8">Floating point (8 bit)</option>
3209 <option data-i18n="Floating point (16 bit)" value="fp16">Floating point (16 bit)</option>
3210 <option data-i18n="Brain floating point (16 bit)" value="bf16">Brain floating point (16 bit)</option>
3211 <option data-i18n="Floating point (32 bit)" value="fp32">Floating point (32 bit)</option>
3212 <option data-i18n="Unknown" value="unknown">Unknown</option>
3213 </select>
3214 </div>
3164 </form>3215 </form>
3165 <form id="ai21_form" data-source="ai21" action="javascript:void(null);" method="post" enctype="multipart/form-data">3216 <form id="ai21_form" data-source="ai21" action="javascript:void(null);" method="post" enctype="multipart/form-data">
3166 <h4 data-i18n="AI21 API Key">AI21 API Key</h4>3217 <h4 data-i18n="AI21 API Key">AI21 API Key</h4>
@@ -3750,19 +3801,22 @@
3750 </select>3801 </select>
3751 </div>3802 </div>
3752 <div id="pollinations_form" data-source="pollinations">3803 <div id="pollinations_form" data-source="pollinations">
3804 <h4>
3805 <a href="https://enter.pollinations.ai/" target="_blank" rel="noopener noreferrer" data-i18n="Pollinations API Key">
3806 Pollinations API Key
3807 </a>
3808 </h4>
3809 <div class="flex-container">
3810 <input id="api_key_pollinations" name="api_key_pollinations" class="text_pole flex1" value="" type="text" autocomplete="off">
3811 <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_pollinations"></div>
3812 </div>
3813 <div data-for="api_key_pollinations" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3814 For privacy reasons, your API key will be hidden after you click 'Connect'.
3815 </div>
3753 <h4 data-i18n="Pollinations Model">Pollinations Model</h4>3816 <h4 data-i18n="Pollinations Model">Pollinations Model</h4>
3754 <select id="model_pollinations_select">3817 <select id="model_pollinations_select">
3755 <!-- Populated by JavaScript -->3818 <!-- Populated by JavaScript -->
3756 </select>3819 </select>
3757 <div class="info-block hint">
3758 <a href="https://pollinations.ai/" target="_blank" rel="noopener noreferrer" data-i18n="Provided free of charge by Pollinations.AI">
3759 Provided free of charge by Pollinations.AI
3760 </a>
3761 <br>
3762 <span data-i18n="Avoid sending sensitive information. Provider's outputs may include ads.">
3763 Avoid sending sensitive information. Provider's outputs may include ads.
3764 </span>
3765 </div>
3766 </div>3820 </div>
3767 <div id="moonshot_form" data-source="moonshot">3821 <div id="moonshot_form" data-source="moonshot">
3768 <h4>3822 <h4>
@@ -3811,7 +3865,10 @@
3811 </select>3865 </select>
3812 <h4 data-i18n="Z.AI Model">Z.AI Model</h4>3866 <h4 data-i18n="Z.AI Model">Z.AI Model</h4>
3813 <select id="model_zai_select">3867 <select id="model_zai_select">
3868 <option value="glm-5">glm-5</option>
3814 <option value="glm-4.7">glm-4.7</option>3869 <option value="glm-4.7">glm-4.7</option>
3870 <option value="glm-4.7-flash">glm-4.7-flash</option>
3871 <option value="glm-4.7-flashx">glm-4.7-flashx</option>
3815 <option value="glm-4.6">glm-4.6</option>3872 <option value="glm-4.6">glm-4.6</option>
3816 <option value="glm-4.6v">glm-4.6v</option>3873 <option value="glm-4.6v">glm-4.6v</option>
3817 <option value="glm-4.6v-flash">glm-4.6v-flash</option>3874 <option value="glm-4.6v-flash">glm-4.6v-flash</option>
@@ -3982,7 +4039,7 @@
3982 <small data-i18n="Story String">Story String</small>4039 <small data-i18n="Story String">Story String</small>
3983 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="context_story_string" title="Expand the editor" data-i18n="[title]Expand the editor"></i>4040 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="context_story_string" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
3984 </label>4041 </label>
3985 <textarea id="context_story_string" class="text_pole textarea_compact autoSetHeight"></textarea>4042 <textarea id="context_story_string" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
3986 </div>4043 </div>
3987 <div class="flex-container flexFlowColumn" data-cc-null>4044 <div class="flex-container flexFlowColumn" data-cc-null>
3988 <div id="context_story_string_position_block">4045 <div id="context_story_string_position_block">
@@ -4019,7 +4076,7 @@
4019 <small data-i18n="Example Separator">Example Separator</small>4076 <small data-i18n="Example Separator">Example Separator</small>
4020 </label>4077 </label>
4021 <div>4078 <div>
4022 <textarea id="context_example_separator" class="text_pole textarea_compact autoSetHeight"></textarea>4079 <textarea id="context_example_separator" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4023 </div>4080 </div>
4024 </div>4081 </div>
4025 <div class="flex1">4082 <div class="flex1">
@@ -4027,7 +4084,7 @@
4027 <small data-i18n="Chat Start">Chat Start</small>4084 <small data-i18n="Chat Start">Chat Start</small>
4028 </label>4085 </label>
4029 <div>4086 <div>
4030 <textarea id="context_chat_start" class="text_pole textarea_compact autoSetHeight"></textarea>4087 <textarea id="context_chat_start" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4031 </div>4088 </div>
4032 </div>4089 </div>
4033 </div>4090 </div>
@@ -4191,11 +4248,11 @@
4191 <div class="flex-container">4248 <div class="flex-container">
4192 <div class="flexAuto" title="Inserted before a User message and as a last prompt line when impersonating." data-i18n="[title]Inserted before a User message and as a last prompt line when impersonating.">4249 <div class="flexAuto" title="Inserted before a User message and as a last prompt line when impersonating." data-i18n="[title]Inserted before a User message and as a last prompt line when impersonating.">
4193 <small data-i18n="User Prefix">User Message Prefix</small>4250 <small data-i18n="User Prefix">User Message Prefix</small>
4194 <textarea id="instruct_input_sequence" class="text_pole textarea_compact autoSetHeight"></textarea>4251 <textarea id="instruct_input_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4195 </div>4252 </div>
4196 <div class="flexAuto" title="Inserted after a User message." data-i18n="[title]Inserted after a User message.">4253 <div class="flexAuto" title="Inserted after a User message." data-i18n="[title]Inserted after a User message.">
4197 <small data-i18n="User Suffix">User Message Suffix</small>4254 <small data-i18n="User Suffix">User Message Suffix</small>
4198 <textarea id="instruct_input_suffix" class="text_pole wide100p textarea_compact autoSetHeight"></textarea>4255 <textarea id="instruct_input_suffix" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
4199 </div>4256 </div>
4200 </div>4257 </div>
4201 </details>4258 </details>
@@ -4204,11 +4261,11 @@
4204 <div class="flex-container">4261 <div class="flex-container">
4205 <div class="flexAuto" title="Inserted before an Assistant message and as a last prompt line when generating an AI reply." data-i18n="[title]Inserted before an Assistant message and as a last prompt line when generating an AI reply.">4262 <div class="flexAuto" title="Inserted before an Assistant message and as a last prompt line when generating an AI reply." data-i18n="[title]Inserted before an Assistant message and as a last prompt line when generating an AI reply.">
4206 <small data-i18n="Assistant Prefix">Assistant Message Prefix</small>4263 <small data-i18n="Assistant Prefix">Assistant Message Prefix</small>
4207 <textarea id="instruct_output_sequence" class="text_pole wide100p textarea_compact autoSetHeight"></textarea>4264 <textarea id="instruct_output_sequence" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
4208 </div>4265 </div>
4209 <div class="flexAuto" title="Inserted after an Assistant message." data-i18n="[title]Inserted after an Assistant message.">4266 <div class="flexAuto" title="Inserted after an Assistant message." data-i18n="[title]Inserted after an Assistant message.">
4210 <small data-i18n="Assistant Suffix">Assistant Message Suffix</small>4267 <small data-i18n="Assistant Suffix">Assistant Message Suffix</small>
4211 <textarea id="instruct_output_suffix" class="text_pole wide100p textarea_compact autoSetHeight"></textarea>4268 <textarea id="instruct_output_suffix" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
4212 </div>4269 </div>
4213 </div>4270 </div>
4214 </details>4271 </details>
@@ -4217,11 +4274,11 @@
4217 <div class="flex-container">4274 <div class="flex-container">
4218 <div class="flexAuto" id="instruct_system_sequence_block" title="Inserted before a System (added by slash commands or extensions) message." data-i18n="[title]Inserted before a System (added by slash commands or extensions) message.">4275 <div class="flexAuto" id="instruct_system_sequence_block" title="Inserted before a System (added by slash commands or extensions) message." data-i18n="[title]Inserted before a System (added by slash commands or extensions) message.">
4219 <small data-i18n="System Prefix">System Message Prefix</small>4276 <small data-i18n="System Prefix">System Message Prefix</small>
4220 <textarea id="instruct_system_sequence" class="text_pole textarea_compact autoSetHeight"></textarea>4277 <textarea id="instruct_system_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4221 </div>4278 </div>
4222 <div class="flexAuto" id="instruct_system_suffix_block" title="Inserted after a System message." data-i18n="[title]Inserted after a System message.">4279 <div class="flexAuto" id="instruct_system_suffix_block" title="Inserted after a System message." data-i18n="[title]Inserted after a System message.">
4223 <small data-i18n="System Suffix">System Message Suffix</small>4280 <small data-i18n="System Suffix">System Message Suffix</small>
4224 <textarea id="instruct_system_suffix" class="text_pole wide100p textarea_compact autoSetHeight"></textarea>4281 <textarea id="instruct_system_suffix" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
4225 </div>4282 </div>
4226 </div>4283 </div>
4227 <div class="flexBasis100p" title="If enabled, System Sequences will be the same as User Sequences." data-i18n="[title]If enabled, System Sequences will be the same as User Sequences.">4284 <div class="flexBasis100p" title="If enabled, System Sequences will be the same as User Sequences." data-i18n="[title]If enabled, System Sequences will be the same as User Sequences.">
@@ -4236,37 +4293,37 @@
4236 <div class="flex-container">4293 <div class="flex-container">
4237 <div class="flexAuto" title="Inserted before the first Assistant's message." data-i18n="[title]Inserted before the first Assistant's message.">4294 <div class="flexAuto" title="Inserted before the first Assistant's message." data-i18n="[title]Inserted before the first Assistant's message.">
4238 <small data-i18n="First Assistant Prefix">First Assistant Prefix</small>4295 <small data-i18n="First Assistant Prefix">First Assistant Prefix</small>
4239 <textarea id="instruct_first_output_sequence" class="text_pole textarea_compact autoSetHeight"></textarea>4296 <textarea id="instruct_first_output_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4240 </div>4297 </div>
4241 <div class="flexAuto" title="Inserted before the last Assistant's message or as a last prompt line when generating an AI reply (except a neutral/system role)." data-i18n="[title]instruct_last_output_sequence">4298 <div class="flexAuto" title="Inserted before the last Assistant's message or as a last prompt line when generating an AI reply (except a neutral/system role)." data-i18n="[title]instruct_last_output_sequence">
4242 <small data-i18n="Last Assistant Prefix">Last Assistant Prefix</small>4299 <small data-i18n="Last Assistant Prefix">Last Assistant Prefix</small>
4243 <textarea id="instruct_last_output_sequence" class="text_pole wide100p textarea_compact autoSetHeight"></textarea>4300 <textarea id="instruct_last_output_sequence" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
4244 </div>4301 </div>
4245 </div>4302 </div>
4246 <div class="flex-container">4303 <div class="flex-container">
4247 <div class="flexAuto" title="Inserted before the first User's message." data-i18n="[title]Inserted before the first User's message.">4304 <div class="flexAuto" title="Inserted before the first User's message." data-i18n="[title]Inserted before the first User's message.">
4248 <small data-i18n="First User Prefix">First User Prefix</small>4305 <small data-i18n="First User Prefix">First User Prefix</small>
4249 <textarea id="instruct_first_input_sequence" class="text_pole textarea_compact autoSetHeight"></textarea>4306 <textarea id="instruct_first_input_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4250 </div>4307 </div>
4251 <div class="flexAuto" title="Inserted before the last User's message." data-i18n="[title]instruct_last_input_sequence">4308 <div class="flexAuto" title="Inserted before the last User's message." data-i18n="[title]instruct_last_input_sequence">
4252 <small data-i18n="Last User Prefix">Last User Prefix</small>4309 <small data-i18n="Last User Prefix">Last User Prefix</small>
4253 <textarea id="instruct_last_input_sequence" class="text_pole wide100p textarea_compact autoSetHeight"></textarea>4310 <textarea id="instruct_last_input_sequence" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
4254 </div>4311 </div>
4255 </div>4312 </div>
4256 <div class="flex-container">4313 <div class="flex-container">
4257 <div class="flexAuto" title="Will be inserted as a last prompt line when using system/neutral generation." data-i18n="[title]Will be inserted as a last prompt line when using system/neutral generation.">4314 <div class="flexAuto" title="Will be inserted as a last prompt line when using system/neutral generation." data-i18n="[title]Will be inserted as a last prompt line when using system/neutral generation.">
4258 <small data-i18n="System Instruction Prefix">System Instruction Prefix</small>4315 <small data-i18n="System Instruction Prefix">System Instruction Prefix</small>
4259 <textarea id="instruct_last_system_sequence" class="text_pole textarea_compact autoSetHeight"></textarea>4316 <textarea id="instruct_last_system_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4260 </div>4317 </div>
4261 <div class="flexAuto" title="If a stop sequence is generated, everything past it will be removed from the output (inclusive)." data-i18n="[title]If a stop sequence is generated, everything past it will be removed from the output (inclusive).">4318 <div class="flexAuto" title="If a stop sequence is generated, everything past it will be removed from the output (inclusive)." data-i18n="[title]If a stop sequence is generated, everything past it will be removed from the output (inclusive).">
4262 <small data-i18n="Stop Sequence">Stop Sequence</small>4319 <small data-i18n="Stop Sequence">Stop Sequence</small>
4263 <textarea id="instruct_stop_sequence" class="text_pole textarea_compact autoSetHeight"></textarea>4320 <textarea id="instruct_stop_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4264 </div>4321 </div>
4265 </div>4322 </div>
4266 <div class="flex-container">4323 <div class="flex-container">
4267 <div class="flexAuto" title="Will be inserted at the start of the chat history if it doesn't start with a User message." data-i18n="[title]Will be inserted at the start of the chat history if it doesn't start with a User message.">4324 <div class="flexAuto" title="Will be inserted at the start of the chat history if it doesn't start with a User message." data-i18n="[title]Will be inserted at the start of the chat history if it doesn't start with a User message.">
4268 <small data-i18n="User Filler Message">User Filler Message</small>4325 <small data-i18n="User Filler Message">User Filler Message</small>
4269 <textarea id="instruct_user_alignment_message" class="text_pole textarea_compact autoSetHeight"></textarea>4326 <textarea id="instruct_user_alignment_message" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4270 </div>4327 </div>
4271 </div>4328 </div>
4272 </details>4329 </details>
@@ -4304,7 +4361,7 @@
4304 <small data-i18n="Prompt Content">Prompt Content</small>4361 <small data-i18n="Prompt Content">Prompt Content</small>
4305 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="sysprompt_content" title="Expand the editor" data-i18n="[title]Expand the editor"></i>4362 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="sysprompt_content" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
4306 </label>4363 </label>
4307 <textarea id="sysprompt_content" class="text_pole textarea_compact autoSetHeight"></textarea>4364 <textarea id="sysprompt_content" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4308 </div>4365 </div>
43094366
4310 <div>4367 <div>
@@ -4312,7 +4369,7 @@
4312 <small data-i18n="Post-History Instructions">Post-History Instructions</small>4369 <small data-i18n="Post-History Instructions">Post-History Instructions</small>
4313 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="sysprompt_post_history" title="Expand the editor" data-i18n="[title]Expand the editor"></i>4370 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="sysprompt_post_history" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
4314 </label>4371 </label>
4315 <textarea id="sysprompt_post_history" class="text_pole textarea_compact autoSetHeight"></textarea>4372 <textarea id="sysprompt_post_history" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4316 </div>4373 </div>
4317 </div>4374 </div>
43184375
@@ -4437,17 +4494,17 @@
4437 <div class="flex-container">4494 <div class="flex-container">
4438 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">4495 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
4439 <small data-i18n="Prefix">Prefix</small>4496 <small data-i18n="Prefix">Prefix</small>
4440 <textarea id="reasoning_prefix" class="text_pole textarea_compact autoSetHeight"></textarea>4497 <textarea id="reasoning_prefix" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4441 </div>4498 </div>
4442 <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">4499 <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix">
4443 <small data-i18n="Suffix">Suffix</small>4500 <small data-i18n="Suffix">Suffix</small>
4444 <textarea id="reasoning_suffix" class="text_pole textarea_compact autoSetHeight"></textarea>4501 <textarea id="reasoning_suffix" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4445 </div>4502 </div>
4446 </div>4503 </div>
4447 <div class="flex-container">4504 <div class="flex-container">
4448 <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">4505 <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator">
4449 <small data-i18n="Separator">Separator</small>4506 <small data-i18n="Separator">Separator</small>
4450 <textarea id="reasoning_separator" class="text_pole textarea_compact autoSetHeight"></textarea>4507 <textarea id="reasoning_separator" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4451 </div>4508 </div>
4452 </div>4509 </div>
4453 </details>4510 </details>
@@ -4469,7 +4526,7 @@
4469 </span>4526 </span>
4470 </small>4527 </small>
4471 <div>4528 <div>
4472 <input id="markdown_escape_strings" class="text_pole textarea_compact" type="text" data-i18n="[placeholder]comma delimited,no spaces between" placeholder="comma delimited,no spaces between" />4529 <input id="markdown_escape_strings" data-macros class="text_pole textarea_compact" type="text" data-i18n="[placeholder]comma delimited,no spaces between" placeholder="comma delimited,no spaces between" />
4473 </div>4530 </div>
4474 </div>4531 </div>
44754532
@@ -4481,7 +4538,7 @@
4481 </span>4538 </span>
4482 </small>4539 </small>
4483 <div>4540 <div>
4484 <textarea id="start_reply_with" class="text_pole textarea_compact autoSetHeight"></textarea>4541 <textarea id="start_reply_with" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
4485 </div>4542 </div>
4486 <label class="checkbox_label" for="chat-show-reply-prefix-checkbox">4543 <label class="checkbox_label" for="chat-show-reply-prefix-checkbox">
4487 <input id="chat-show-reply-prefix-checkbox" type="checkbox" />4544 <input id="chat-show-reply-prefix-checkbox" type="checkbox" />
@@ -4780,7 +4837,7 @@
4780 <div name="themeElements" class="flex-container flexFlowColumn flexNoGap">4837 <div name="themeElements" class="flex-container flexFlowColumn flexNoGap">
4781 <!-- <h4><span data-i18n="UI Colors">Theme Settings</span></h4> -->4838 <!-- <h4><span data-i18n="UI Colors">Theme Settings</span></h4> -->
4782 <div name="AvatarAndChatDisplay" class="flex-container flexFlowColumn">4839 <div name="AvatarAndChatDisplay" class="flex-container flexFlowColumn">
4783 <div class="flex-container alignItemsBaseline" title="This style applies to all avatars globaly, including your Persona, Character Managment, Account selection, etc." data-i18n="[title]This style applies to all avatars globaly, including your Persona, Character Managment, Account selection, etc.">4840 <div class="flex-container alignItemsBaseline" title="This style applies to all avatars globaly, including your Persona, Character Management, Account selection, etc." data-i18n="[title]This style applies to all avatars globaly, including your Persona, Character Management, Account selection, etc.">
4784 <span data-i18n="Avatar Style:">Avatars:</span>4841 <span data-i18n="Avatar Style:">Avatars:</span>
4785 <select id="avatar_style" class="widthNatural flex1 margin0 text_pole">4842 <select id="avatar_style" class="widthNatural flex1 margin0 text_pole">
4786 <option value="0" data-i18n="Circle">Circle</option>4843 <option value="0" data-i18n="Circle">Circle</option>
@@ -5362,6 +5419,7 @@
5362 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>5419 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
5363 </div>5420 </div>
5364 <div class="inline-drawer-content">5421 <div class="inline-drawer-content">
5422 <div class="flex1" title="When to show the autocomplete for slash commands and macros." data-i18n="[title]When to show the autocomplete for slash commands and macros.">
5365 <label for="stscript_autocomplete_state">5423 <label for="stscript_autocomplete_state">
5366 <small data-i18n="Visibility">Visibility</small>5424 <small data-i18n="Visibility">Visibility</small>
5367 </label>5425 </label>
@@ -5370,12 +5428,19 @@
5370 <option value="1" data-i18n="Input length > 1">Input length > 1</option>5428 <option value="1" data-i18n="Input length > 1">Input length > 1</option>
5371 <option value="2" data-i18n="Always show">Always show</option>5429 <option value="2" data-i18n="Always show">Always show</option>
5372 </select>5430 </select>
5431 </div>
5373 <label class="checkbox_label" for="stscript_autocomplete_autoHide">5432 <label class="checkbox_label" for="stscript_autocomplete_autoHide">
5374 <input id="stscript_autocomplete_autoHide" type="checkbox" />5433 <input id="stscript_autocomplete_autoHide" type="checkbox" />
5375 <small data-i18n="Automatically hide details">5434 <small data-i18n="Automatically hide details">
5376 Automatically hide details5435 Automatically hide details
5377 </small>5436 </small>
5378 </label>5437 </label>
5438 <label class="checkbox_label" for="stscript_autocomplete_showInAllMacroFields" title="Show macro autocomplete in all macro-enabled fields. When off, autocomplete only shows in expanded editors or when pressing Ctrl+Space." data-i18n="[title]Show macro autocomplete in all macro-enabled fields. When off, autocomplete only shows in expanded editors or when pressing Ctrl+Space.">
5439 <input id="stscript_autocomplete_showInAllMacroFields" type="checkbox" />
5440 <small data-i18n="Show in all macro fields">
5441 Show in all macro fields
5442 </small>
5443 </label>
5379 <div class="flex-container">5444 <div class="flex-container">
5380 <div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete.">5445 <div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete.">
5381 <label for="stscript_matching">5446 <label for="stscript_matching">
@@ -5499,6 +5564,12 @@
5499 </div>5564 </div>
5500 <div class="bg-header-row-2">5565 <div class="bg-header-row-2">
5501 <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />5566 <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />
5567 <select id="bg-sort" class="text_pole margin0" title="Sort backgrounds" data-i18n="[title]Sort backgrounds">
5568 <option value="az" data-i18n="A-Z">A-Z</option>
5569 <option value="za" data-i18n="Z-A">Z-A</option>
5570 <option value="newest" data-i18n="Newest">Newest</option>
5571 <option value="oldest" data-i18n="Oldest">Oldest</option>
5572 </select>
5502 </div>5573 </div>
5503 </div>5574 </div>
5504 <div id="bg_tabs" class="heading-container-with-controls">5575 <div id="bg_tabs" class="heading-container-with-controls">
@@ -5695,7 +5766,7 @@
5695 <span data-i18n="Persona Description">Persona Description</span>5766 <span data-i18n="Persona Description">Persona Description</span>
5696 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="persona_description" title="Expand the editor" data-i18n="[title]Expand the editor"></i>5767 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="persona_description" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
5697 </h4>5768 </h4>
5698 <textarea id="persona_description" name="persona_description" data-i18n="[placeholder]Example: [{{user}} is a 28-year-old Romanian cat girl.]" placeholder="Example:&#10;[{{user}} is a 28-year-old Romanian cat girl.]" class="text_pole textarea_compact" value="" autocomplete="off" rows="8"></textarea>5769 <textarea id="persona_description" name="persona_description" data-macros data-i18n="[placeholder]Example: [{{user}} is a 28-year-old Romanian cat girl.]" placeholder="Example:&#10;[{{user}} is a 28-year-old Romanian cat girl.]" class="text_pole textarea_compact" value="" autocomplete="off" rows="8"></textarea>
56995770
5700 <div class="flex-container justifySpaceBetween">5771 <div class="flex-container justifySpaceBetween">
5701 <h4 data-i18n="Position">Position</h4>5772 <h4 data-i18n="Position">Position</h4>
@@ -5954,7 +6025,7 @@
5954 </span>6025 </span>
5955 </div>6026 </div>
5956 </div>6027 </div>
5957 <textarea id="description_textarea" class="mdHotkeys" data-i18n="[placeholder]Describe your character's physical and mental traits here." placeholder="Describe your character's physical and mental traits here." name="description" placeholder=""></textarea>6028 <textarea id="description_textarea" class="mdHotkeys" data-macros data-i18n="[placeholder]Describe your character's physical and mental traits here." placeholder="Describe your character's physical and mental traits here." name="description" placeholder=""></textarea>
5958 <div class="extension_token_counter">6029 <div class="extension_token_counter">
5959 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="description_textarea" data-token-permanent="true">counting...</span>6030 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="description_textarea" data-token-permanent="true">counting...</span>
5960 </div>6031 </div>
@@ -5974,7 +6045,7 @@
5974 </span>6045 </span>
5975 </div>6046 </div>
5976 </div>6047 </div>
5977 <textarea class="mdHotkeys" id="firstmessage_textarea" data-i18n="[placeholder]This will be the first message from the character that starts every chat." placeholder="This will be the first message from the character that starts every chat." name="first_mes" placeholder=""></textarea>6048 <textarea id="firstmessage_textarea" class="mdHotkeys" data-macros data-i18n="[placeholder]This will be the first message from the character that starts every chat." placeholder="This will be the first message from the character that starts every chat." name="first_mes" placeholder=""></textarea>
5978 <div class="extension_token_counter">6049 <div class="extension_token_counter">
5979 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="firstmessage_textarea">counting...</span>6050 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="firstmessage_textarea">counting...</span>
5980 </div>6051 </div>
@@ -6104,6 +6175,12 @@
6104 </div>6175 </div>
6105 <div class="inline-drawer-content">6176 <div class="inline-drawer-content">
6106 <div id="currentGroupMembers" name="Current Group Members" class="flex-container flexFlowColumn overflowYAuto flex1">6177 <div id="currentGroupMembers" name="Current Group Members" class="flex-container flexFlowColumn overflowYAuto flex1">
6178 <div id="rm_group_members_header">
6179 <input id="rm_group_members_filter" class="text_pole margin0" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />
6180 </div>
6181 <div class="rm_tag_controls">
6182 <div class="tags rm_tag_filter"></div>
6183 </div>
6107 <div id="rm_group_members_pagination" class="rm_group_members_pagination group_pagination"></div>6184 <div id="rm_group_members_pagination" class="rm_group_members_pagination group_pagination"></div>
6108 <div id="rm_group_members" class="rm_group_members overflowYAuto flex-container" group_empty_text="Group is empty." data-i18n="[group_empty_text]Group is empty."></div>6185 <div id="rm_group_members" class="rm_group_members overflowYAuto flex-container" group_empty_text="Group is empty." data-i18n="[group_empty_text]Group is empty."></div>
6109 </div>6186 </div>
@@ -6115,7 +6192,7 @@
6115 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>6192 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
6116 </div>6193 </div>
6117 <div class="inline-drawer-content">6194 <div class="inline-drawer-content">
6118 <div name="Unadded Char List" class="flex-container flexFlowColumn overflowYAuto flex1">6195 <div id="unaddedCharList" name="Unadded Char List" class="flex-container flexFlowColumn overflowYAuto flex1">
6119 <div id="rm_group_add_members_header">6196 <div id="rm_group_add_members_header">
6120 <input id="rm_group_filter" class="text_pole margin0" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />6197 <input id="rm_group_filter" class="text_pole margin0" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />
6121 </div>6198 </div>
@@ -6292,7 +6369,7 @@
6292 <span data-i18n="Main Prompt">Main Prompt</span>6369 <span data-i18n="Main Prompt">Main Prompt</span>
6293 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="system_prompt_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>6370 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="system_prompt_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
6294 </h4>6371 </h4>
6295 <textarea id="system_prompt_textarea" name="system_prompt" data-i18n="[placeholder]Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)" placeholder="Any contents here will replace the default Main Prompt used for this character.&#10;(v2 spec: system_prompt)" form="form_create" class="text_pole" autocomplete="off" rows="3"></textarea>6372 <textarea id="system_prompt_textarea" name="system_prompt" data-macros data-i18n="[placeholder]Any contents here will replace the default Main Prompt used for this character. (v2 spec: system_prompt)" placeholder="Any contents here will replace the default Main Prompt used for this character.&#10;(v2 spec: system_prompt)" form="form_create" class="text_pole" autocomplete="off" rows="3"></textarea>
6296 <div class="extension_token_counter">6373 <div class="extension_token_counter">
6297 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="system_prompt_textarea">counting...</span>6374 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="system_prompt_textarea">counting...</span>
6298 </div>6375 </div>
@@ -6302,7 +6379,7 @@
6302 <span data-i18n="Post-History Instructions">Post-History Instructions</span>6379 <span data-i18n="Post-History Instructions">Post-History Instructions</span>
6303 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="post_history_instructions_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>6380 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="post_history_instructions_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
6304 </h4>6381 </h4>
6305 <textarea id="post_history_instructions_textarea" name="post_history_instructions" data-i18n="[placeholder]Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)" placeholder="Any contents here will replace the default Post-History Instructions used for this character.&#10;(v2 spec: post_history_instructions)" form="form_create" class="text_pole" autocomplete="off" rows="3"></textarea>6382 <textarea id="post_history_instructions_textarea" name="post_history_instructions" data-macros data-i18n="[placeholder]Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)" placeholder="Any contents here will replace the default Post-History Instructions used for this character.&#10;(v2 spec: post_history_instructions)" form="form_create" class="text_pole" autocomplete="off" rows="3"></textarea>
6306 <div class="extension_token_counter">6383 <div class="extension_token_counter">
6307 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="post_history_instructions_textarea">counting...</span>6384 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="post_history_instructions_textarea">counting...</span>
6308 </div>6385 </div>
@@ -6355,7 +6432,7 @@
6355 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="personality_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>6432 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="personality_textarea" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
6356 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#personality-summary" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a>6433 <a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#personality-summary" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a>
6357 </h4>6434 </h4>
6358 <textarea id="personality_textarea" name="personality" data-i18n="[placeholder](A brief description of the personality)" placeholder="(A brief description of the personality)" form="form_create" class="text_pole" autocomplete="off" rows="4"></textarea>6435 <textarea id="personality_textarea" name="personality" data-macros data-i18n="[placeholder](A brief description of the personality)" placeholder="(A brief description of the personality)" form="form_create" class="text_pole" autocomplete="off" rows="4"></textarea>
6359 <div class="extension_token_counter">6436 <div class="extension_token_counter">
6360 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="personality_textarea" data-token-permanent="true">counting...</span>6437 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="personality_textarea" data-token-permanent="true">counting...</span>
6361 </div>6438 </div>
@@ -6368,7 +6445,7 @@
6368 <span class="fa-solid fa-circle-question note-link-span"></span>6445 <span class="fa-solid fa-circle-question note-link-span"></span>
6369 </a>6446 </a>
6370 </h4>6447 </h4>
6371 <textarea id="scenario_pole" name="scenario" data-i18n="[placeholder](Circumstances and context of the interaction)" placeholder="(Circumstances and context of the interaction)" class="text_pole" value="" autocomplete="off" form="form_create" rows="4"></textarea>6448 <textarea id="scenario_pole" name="scenario" data-macros data-i18n="[placeholder](Circumstances and context of the interaction)" placeholder="(Circumstances and context of the interaction)" class="text_pole" value="" autocomplete="off" form="form_create" rows="4"></textarea>
6372 <div class="extension_token_counter">6449 <div class="extension_token_counter">
6373 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="scenario_pole" data-token-permanent="true">counting...</span>6450 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="scenario_pole" data-token-permanent="true">counting...</span>
6374 </div>6451 </div>
@@ -6381,7 +6458,7 @@
6381 </span>6458 </span>
6382 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="depth_prompt_prompt" title="Expand the editor" data-i18n="[title]Expand the editor"></i>6459 <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="depth_prompt_prompt" title="Expand the editor" data-i18n="[title]Expand the editor"></i>
6383 </h4>6460 </h4>
6384 <textarea id="depth_prompt_prompt" name="depth_prompt_prompt" class="text_pole" rows="5" autocomplete="off" form="form_create" data-i18n="[placeholder](Text to be inserted in-chat @ designated depth and role)" placeholder="(Text to be inserted in-chat @ designated depth and role)"></textarea>6461 <textarea id="depth_prompt_prompt" name="depth_prompt_prompt" data-macros class="text_pole" rows="5" autocomplete="off" form="form_create" data-i18n="[placeholder](Text to be inserted in-chat @ designated depth and role)" placeholder="(Text to be inserted in-chat @ designated depth and role)"></textarea>
6385 </div>6462 </div>
6386 <div>6463 <div>
6387 <h4>6464 <h4>
@@ -6431,7 +6508,7 @@
6431 </a>6508 </a>
6432 </h5>6509 </h5>
6433 </div>6510 </div>
6434 <textarea id="mes_example_textarea" class="flexGrow mdHotkeys" name="mes_example" data-i18n="[placeholder](Examples of chat dialog. Begin each example with START on a new line.)" placeholder="(Examples of chat dialog. Begin each example with &lt;START&gt; on a new line.)" form="form_create" rows="6"></textarea>6511 <textarea id="mes_example_textarea" class="flexGrow mdHotkeys" name="mes_example" data-macros data-i18n="[placeholder](Examples of chat dialog. Begin each example with START on a new line.)" placeholder="(Examples of chat dialog. Begin each example with &lt;START&gt; on a new line.)" form="form_create" rows="6"></textarea>
6435 <div class="extension_token_counter">6512 <div class="extension_token_counter">
6436 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="mes_example_textarea">counting...</span>6513 <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="mes_example_textarea">counting...</span>
6437 </div>6514 </div>
@@ -7124,7 +7201,7 @@
7124 <span>&nbsp;</span>7201 <span>&nbsp;</span>
7125 <span id="completion_prompt_manager_popup_entry_source"></span>7202 <span id="completion_prompt_manager_popup_entry_source"></span>
7126 </div>7203 </div>
7127 <textarea id="completion_prompt_manager_popup_entry_form_prompt" class="text_pole" name="prompt" placeholder="The prompt to be sent." data-i18n="[placeholder]The prompt to be sent."></textarea>7204 <textarea id="completion_prompt_manager_popup_entry_form_prompt" class="text_pole" name="prompt" data-macros data-macros-autocomplete="always" data-macros-autocomplete-style="expanded" placeholder="The prompt to be sent." data-i18n="[placeholder]The prompt to be sent."></textarea>
7128 </div>7205 </div>
7129 <div class="completion_prompt_manager_popup_entry_form_footer">7206 <div class="completion_prompt_manager_popup_entry_form_footer">
7130 <a id="completion_prompt_manager_popup_entry_form_close" title="Close" data-i18n="[title]close" class="fa-solid fa-close menu_button"></a>7207 <a id="completion_prompt_manager_popup_entry_form_close" title="Close" data-i18n="[title]close" class="fa-solid fa-close menu_button"></a>
@@ -7410,7 +7487,7 @@
7410 </div>7487 </div>
7411 </div>7488 </div>
7412 </summary>7489 </summary>
7413 <textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="12"></textarea>7490 <textarea data-macros name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="12"></textarea>
7414 </details>7491 </details>
7415 </div>7492 </div>
7416 </div>7493 </div>
@@ -7500,7 +7577,7 @@
7500 <b data-i18n="Unique to this chat">Unique to this chat</b>.<br>7577 <b data-i18n="Unique to this chat">Unique to this chat</b>.<br>
7501 <span data-i18n="Checkpoints inherit the Note from their parent, and can be changed individually after that.">Checkpoints inherit the Note from their parent, and can be changed individually after that.</span><br>7578 <span data-i18n="Checkpoints inherit the Note from their parent, and can be changed individually after that.">Checkpoints inherit the Note from their parent, and can be changed individually after that.</span><br>
7502 </small>7579 </small>
7503 <textarea id="extension_floating_prompt" class="text_pole textarea_compact" rows="8"></textarea>7580 <textarea id="extension_floating_prompt" data-macros class="text_pole textarea_compact" rows="8"></textarea>
7504 <div class="extension_token_counter">7581 <div class="extension_token_counter">
7505 <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_prompt_token_counter">0</span>7582 <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_prompt_token_counter">0</span>
7506 </div>7583 </div>
@@ -7557,7 +7634,7 @@
7557 <div class="inline-drawer-content">7634 <div class="inline-drawer-content">
7558 <small data-i18n="Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.">Will be automatically added as the author's note for this character. Will be used in groups, but7635 <small data-i18n="Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open.">Will be automatically added as the author's note for this character. Will be used in groups, but
7559 can't be modified when a group chat is open.</small>7636 can't be modified when a group chat is open.</small>
7560 <textarea id="extension_floating_chara" class="text_pole textarea_compact" rows="8" placeholder="Example:&#10;[Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea>7637 <textarea id="extension_floating_chara" data-macros class="text_pole textarea_compact" rows="8" placeholder="Example:&#10;[Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea>
7561 <div class="extension_token_counter">7638 <div class="extension_token_counter">
7562 <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_chara_token_counter">0</span>7639 <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_chara_token_counter">0</span>
7563 </div>7640 </div>
@@ -7589,7 +7666,7 @@
7589 </div>7666 </div>
7590 <div class="inline-drawer-content">7667 <div class="inline-drawer-content">
7591 <small data-i18n="Will be automatically added as the Author's Note for all new chats.">Will be automatically added as the Author's Note for all new chats.</small>7668 <small data-i18n="Will be automatically added as the Author's Note for all new chats.">Will be automatically added as the Author's Note for all new chats.</small>
7592 <textarea id="extension_floating_default" class="text_pole textarea_compact" rows="8" placeholder="Example:&#10;[Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea>7669 <textarea id="extension_floating_default" data-macros class="text_pole textarea_compact" rows="8" placeholder="Example:&#10;[Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea>
7593 <div class="extension_token_counter">7670 <div class="extension_token_counter">
7594 <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_default_token_counter">0</span>7671 <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_default_token_counter">0</span>
7595 </div>7672 </div>
public/locales/fr-fr.json+2 -0
@@ -290,6 +290,8 @@
290 "View Remaining Credits": "Afficher les crédits restants",290 "View Remaining Credits": "Afficher les crédits restants",
291 "OpenRouter Model": "Modèle OpenRouter",291 "OpenRouter Model": "Modèle OpenRouter",
292 "Model Providers": "Fournisseurs de modèles",292 "Model Providers": "Fournisseurs de modèles",
293 "Model Quantizations": "Quantifications du modèle",
294 "Select quantizations. No selection = all quantizations.": "Sélectionnez les quantifications. Aucune sélection = toutes les quantifications.",
293 "InfermaticAI API Key": "Clé API InfermaticAI",295 "InfermaticAI API Key": "Clé API InfermaticAI",
294 "InfermaticAI Model": "Modèle InfermaticAI",296 "InfermaticAI Model": "Modèle InfermaticAI",
295 "DreamGen API key": "Clé API DreamGen",297 "DreamGen API key": "Clé API DreamGen",
public/locales/zh-cn.json+159 -177
@@ -185,9 +185,7 @@
185 "Mirostat (mode=1 is only for llama.cpp)": "Mirostat(mode=1 仅用于 llama.cpp)",185 "Mirostat (mode=1 is only for llama.cpp)": "Mirostat(mode=1 仅用于 llama.cpp)",
186 "Mirostat_desc": "Mirostat 是一个用于控制输出困惑度的恒温器",186 "Mirostat_desc": "Mirostat 是一个用于控制输出困惑度的恒温器",
187 "Mirostat Mode": "Mirostat 模式",187 "Mirostat Mode": "Mirostat 模式",
188 "Variability parameter for Mirostat outputs": "Mirostat 输出的变异性参数。",
189 "Mirostat Eta": "Mirostat η",188 "Mirostat Eta": "Mirostat η",
190 "Learning rate of Mirostat": "Mirostat 的学习率。",
191 "Beam search": "束搜索",189 "Beam search": "束搜索",
192 "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一种在LLM采样中使用的贪婪暴力算法,用于找到最可能的单词或标记序列。它一次扩展多个候选序列,在每一步保留固定数量(光束宽度)的最佳序列。",190 "A greedy, brute-force algorithm used in LLM sampling to find the most likely sequence of words or tokens. It expands multiple candidate sequences at once, maintaining a fixed number (beam width) of top sequences at each step.": "一种在LLM采样中使用的贪婪暴力算法,用于找到最可能的单词或标记序列。它一次扩展多个候选序列,在每一步保留固定数量(光束宽度)的最佳序列。",
193 "# of Beams": "光束数量",191 "# of Beams": "光束数量",
@@ -215,9 +213,9 @@
215 "Spaces Between Special Tokens": "特殊词符之间的空格",213 "Spaces Between Special Tokens": "特殊词符之间的空格",
216 "Seed_desc": "一个用于生成确定性和可复现的输出的随机种子。设置为 -1 时会使用随机种子。",214 "Seed_desc": "一个用于生成确定性和可复现的输出的随机种子。设置为 -1 时会使用随机种子。",
217 "LLaMA / Mistral / Yi models only": "LLaMA / Mistral / Yi模型专用。首先确保您选择了适当的词符化器。\n这项设置决定了你不想在结果中看到的字符串。\n每行一个字符串。可以是文本或者[词符id]。\n许多词符以空格开头。如果不确定,请使用词符计数器。",215 "LLaMA / Mistral / Yi models only": "LLaMA / Mistral / Yi模型专用。首先确保您选择了适当的词符化器。\n这项设置决定了你不想在结果中看到的字符串。\n每行一个字符串。可以是文本或者[词符id]。\n许多词符以空格开头。如果不确定,请使用词符计数器。",
218 "Global list": "Global list",216 "Global list": "全局列表",
219 "Example: some text [42, 69, 1337]": "例如:\n一些文本\n[42, 69, 1337]",217 "Example: some text [42, 69, 1337]": "例如:\n一些文本\n[42, 69, 1337]",
220 "Preset-specific list": "Preset-specific list",218 "Preset-specific list": "预设特有的列表",
221 "CFG": "CFG",219 "CFG": "CFG",
222 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",220 "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。",
223 "Scale": "缩放比例",221 "Scale": "缩放比例",
@@ -228,6 +226,7 @@
228 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",226 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。",
229 "JSON Schema": "JSON 结构",227 "JSON Schema": "JSON 结构",
230 "Type in the desired JSON schema": "输入所需的 JSON 结构",228 "Type in the desired JSON schema": "输入所需的 JSON 结构",
229 "Allow empty schema objects": "允许空结构对象",
231 "Top P & Min P": "Top P 和 Min P",230 "Top P & Min P": "Top P 和 Min P",
232 "Load default order": "加载默认顺序",231 "Load default order": "加载默认顺序",
233 "Sampler Order": "取样器顺序",232 "Sampler Order": "取样器顺序",
@@ -250,14 +249,12 @@
250 "Space": "空格",249 "Space": "空格",
251 "Newline": "换行",250 "Newline": "换行",
252 "Double Newline": "双换行",251 "Double Newline": "双换行",
253 "Wrap user messages in quotes before sending": "在发送之前将用户消息用引号括起来",
254 "Wrap in Quotes": "用引号包裹",
255 "Wrap entire user message in quotes before sending.": "在发送之前用引号包裹整个用户消息。",
256 "Leave off if you use quotes manually for speech.": "如果您手动使用引号包裹对话,请忽略此项。",
257 "Continue prefill": "继续预填充",252 "Continue prefill": "继续预填充",
258 "Continue sends the last message as assistant role instead of system message with instruction.": "继续发送的是作为助手角色的最后一条消息,而不是带有指示的系统消息。",253 "Continue sends the last message as assistant role instead of system message with instruction.": "继续发送的是作为助手角色的最后一条消息,而不是带有指示的系统消息。",
259 "Squash system messages": "压缩系统消息",254 "Squash system messages": "压缩系统消息",
260 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "将连续的系统消息合并为一条(不包括示例对话),可能会提高一些模型的连贯性。",255 "Combines consecutive system messages into one (excluding example dialogues). May improve coherence for some models.": "将连续的系统消息合并为一条(不包括示例对话),可能会提高一些模型的连贯性。",
256 "Use system prompt": "使用系统提示词",
257 "Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.": "为支持的模型发送系统提示词。如果禁用,则用户消息将添加到提示词的开头。",
261 "Enable web search": "启用联网搜索",258 "Enable web search": "启用联网搜索",
262 "Use search capabilities provided by the backend.": "使用后端提供的联网搜索功能。",259 "Use search capabilities provided by the backend.": "使用后端提供的联网搜索功能。",
263 "openrouter_web_search_fee": "收费,每个提示词会多收 $0.02。",260 "openrouter_web_search_fee": "收费,每个提示词会多收 $0.02。",
@@ -268,20 +265,19 @@
268 "enable_functions_desc_2": "功能工具",265 "enable_functions_desc_2": "功能工具",
269 "enable_functions_desc_3": "可以被各种扩展利用来提供附加功能。",266 "enable_functions_desc_3": "可以被各种扩展利用来提供附加功能。",
270 "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。",267 "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。",
271 "Send inline images": "发送图片",268 "Send inline media": "发送内联媒体",
272 "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送媒体文件。",269 "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送媒体文件。",
270 "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。",
271 "audio_inlining_hint_2": "音频必须小于 20 MB。",
273 "Inline Image Quality": "图片画质",272 "Inline Image Quality": "图片画质",
274 "openai_inline_image_quality_auto": "自动",273 "openai_inline_image_quality_auto": "自动",
275 "openai_inline_image_quality_low": "低",274 "openai_inline_image_quality_low": "低",
276 "openai_inline_image_quality_high": "高",275 "openai_inline_image_quality_high": "高",
277 "Send inline videos": "发送视频",
278 "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。",
279 "Request inline images": "请求图片返回",276 "Request inline images": "请求图片返回",
280 "Allows the model to return image attachments.": "允许模型返回图片附件。",277 "Allows the model to return image attachments.": "允许模型返回图片附件。",
281 "Request inline images_desc_2": "与以下几个功能不兼容:函数调用、联网搜搜、系统提示词。",278 "Request inline images_desc_2": "与以下几个功能不兼容:函数调用、联网搜搜、系统提示词。",
282 "Use system prompt": "使用系统提示词",279 "Resolution": "分辨率",
283 "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",280 "Aspect Ratio": "长宽比",
284 "Merges_all_system_messages_desc_2": "字段发送。",
285 "Request model reasoning": "请求思维链",281 "Request model reasoning": "请求思维链",
286 "Allows the model to return its thinking process.": "允许模型返回其思维过程。",282 "Allows the model to return its thinking process.": "允许模型返回其思维过程。",
287 "This setting affects visibility only.": "此设置只影响思维链是否可见。",283 "This setting affects visibility only.": "此设置只影响思维链是否可见。",
@@ -295,12 +291,18 @@
295 "openai_reasoning_effort_maximum": "极高",291 "openai_reasoning_effort_maximum": "极高",
296 "OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.": "OpenAI式选项:低、中、高。极低等于低,极高等于高。选择自动,则不传入推理强度参数。",292 "OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.": "OpenAI式选项:低、中、高。极低等于低,极高等于高。选择自动,则不传入推理强度参数。",
297 "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "将最大回复空间的一部分分配给思维链(极低:1024词符,低:10%,中:25%,高:50%,极高:95%),最低1024词符。选择“自动”不会请求模型思维链。",293 "Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.": "将最大回复空间的一部分分配给思维链(极低:1024词符,低:10%,中:25%,高:50%,极高:95%),最低1024词符。选择“自动”不会请求模型思维链。",
294 "Sets a dynamic reasoning depth level for thinking (Flash 3/Pro 3). High and low are supported by both, minimal and medium are Flash 3 only. Auto lets the model decide.": "设置动态推理强度等级(3 Flash / 3 Pro)。两者都支持高和低,但极低和中只有 3 Flash 支持。自动会让模型自己选择。",
298 "Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.": "将最大回复空间的一部分分配给思维链(仅 2.5 Flash / 2.5 Pro 模型)(极低:0/128词符,低:10%,中:25%,高:50%,极高:24576/32768词符),最低1024词符。选择“自动”会让模型自己决定。",295 "Allocates a portion of the response length for thinking (Flash 2.5/Pro 2.5) (min: 0/128 tokens, low: 10%, medium: 25%, high: 50%, max: 24576/32768 tokens). Auto lets the model decide.": "将最大回复空间的一部分分配给思维链(仅 2.5 Flash / 2.5 Pro 模型)(极低:0/128词符,低:10%,中:25%,高:50%,极高:24576/32768词符),最低1024词符。选择“自动”会让模型自己决定。",
296 "Verbosity": "长度",
297 "openai_verbosity_auto": "自动",
298 "openai_verbosity_low": "低",
299 "openai_verbosity_medium": "中",
300 "openai_verbosity_high": "高",
301 "Constrains the verbosity of the model's response.": "限制模型回复的长度。",
299 "Assistant Prefill": "AI预填",302 "Assistant Prefill": "AI预填",
300 "Expand the editor": "展开编辑器",303 "Expand the editor": "展开编辑器",
301 "Start Claude's answer with...": "以如下内容开始Claude的回答...",304 "Start Claude's answer with...": "以如下内容开始Claude的回答...",
302 "Assistant Impersonation Prefill": "AI帮答预填",305 "Assistant Impersonation Prefill": "AI帮答预填",
303 "Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.": "为支持的模型发送系统提示词。如果禁用,则用户消息将添加到提示词的开头。",
304 "Confirm token parsing with": "确认使用以下工具进行词符解析",306 "Confirm token parsing with": "确认使用以下工具进行词符解析",
305 "Tokenizer": "分词器",307 "Tokenizer": "分词器",
306 "New preset": "新预设",308 "New preset": "新预设",
@@ -381,7 +383,7 @@
381 "Date Desc": "日期倒序",383 "Date Desc": "日期倒序",
382 "category": "分类",384 "category": "分类",
383 "Top": "热门",385 "Top": "热门",
384 "New": "新建",386 "New": "最新",
385 "All": "全部",387 "All": "全部",
386 "All Classes": "所有分类",388 "All Classes": "所有分类",
387 "Toggle grid view": "切换网格视图",389 "Toggle grid view": "切换网格视图",
@@ -398,6 +400,7 @@
398 "Aphrodite Model": "Aphrodite 模型",400 "Aphrodite Model": "Aphrodite 模型",
399 "ggml-org/llama.cpp": "ggml-org/llama.cpp",401 "ggml-org/llama.cpp": "ggml-org/llama.cpp",
400 "Example: http://127.0.0.1:8080": "示例:http://127.0.0.1:8080",402 "Example: http://127.0.0.1:8080": "示例:http://127.0.0.1:8080",
403 "llama.cpp Model": "llama.cpp 模型",
401 "Example: http://127.0.0.1:11434": "示例:http://127.0.0.1:11434",404 "Example: http://127.0.0.1:11434": "示例:http://127.0.0.1:11434",
402 "Ollama Model": "Ollama 模型",405 "Ollama Model": "Ollama 模型",
403 "Download": "下载",406 "Download": "下载",
@@ -465,7 +468,7 @@
465 "(Express mode)": "(快速模式)",468 "(Express mode)": "(快速模式)",
466 "API Key": "API 密钥",469 "API Key": "API 密钥",
467 "Project ID": "项目ID:",470 "Project ID": "项目ID:",
468 "Project ID is required when selecting regions other than the default (us-central1). You can find this in a model 404 error message.": "仅当选择非默认区域(us-central1)时才需要`项目ID`。\n 您可以在模型 404 错误消息中找到它。",471 "Project ID is required when selecting regions other than the default (us-central1). You can find this in a model 404 error message.": "仅当选择非默认区域(us-central1)时才需要 项目ID。\n 您可以在模型 404 错误消息中找到它。",
469 "Service Account Configuration": "服务帐户配置",472 "Service Account Configuration": "服务帐户配置",
470 "Service Account JSON Content": "服务帐户 JSON 内容:",473 "Service Account JSON Content": "服务帐户 JSON 内容:",
471 "For privacy reasons, your Service Account JSON content will be hidden after you click 'Validate JSON'.": "出于隐私考虑,你的服务账号 JSON 内容将在点击“验证JSON”后隐藏。",474 "For privacy reasons, your Service Account JSON content will be hidden after you click 'Validate JSON'.": "出于隐私考虑,你的服务账号 JSON 内容将在点击“验证JSON”后隐藏。",
@@ -478,6 +481,13 @@
478 "Groq Model": "Groq 模型",481 "Groq Model": "Groq 模型",
479 "Electron Hub API Key": "Electron Hub API 密钥",482 "Electron Hub API Key": "Electron Hub API 密钥",
480 "Electron Hub Model": "Electron Hub 模型",483 "Electron Hub Model": "Electron Hub 模型",
484 "Electron Hub Model Sorting": "Electron Hub 模型排序",
485 "Input Price": "输入价格(最便宜)",
486 "Output Price": "输出价格(最便宜)",
487 "Chutes API Key": "Chutes API 密钥",
488 "View Billing/Balance": "查看账单/余额",
489 "Chutes Model": "Chutes 模型",
490 "Chutes Model Sorting": "Chutes 模型排序",
481 "NanoGPT API Key": "NanoGPT API 密钥",491 "NanoGPT API Key": "NanoGPT API 密钥",
482 "NanoGPT Model": "NanoGPT 模型",492 "NanoGPT Model": "NanoGPT 模型",
483 "DeepSeek API Key": "DeepSeek API 密钥",493 "DeepSeek API Key": "DeepSeek API 密钥",
@@ -506,6 +516,19 @@
506 "Avoid sending sensitive information. Provider's outputs may include ads.": "请避免发送敏感信息。输出可能有提供商的广告。",516 "Avoid sending sensitive information. Provider's outputs may include ads.": "请避免发送敏感信息。输出可能有提供商的广告。",
507 "Moonshot AI API Key": "Moonshot AI API 密钥",517 "Moonshot AI API Key": "Moonshot AI API 密钥",
508 "Moonshot AI Model": "Moonshot AI 模型",518 "Moonshot AI Model": "Moonshot AI 模型",
519 "Z.AI API Key": "Z.AI API 密钥",
520 "Z.AI Endpoint": "Z.AI 端点",
521 "Common API": "通用 API",
522 "Coding API": "编码 API",
523 "Z.AI Model": "Z.AI 模型",
524 "Azure Base URL": "Azure 基础 URL",
525 "Deployment Name": "部署名称",
526 "The name of your model deployment in Azure.": "你在 Azure 中的模型部署名称。",
527 "API Version": "API 版本",
528 "Azure API Key": "Azure API 密钥",
529 "Model Name": "模型名称",
530 "Click 'Connect' to fetch model name": "点击“连接”以获取模型名称",
531 "The underlying model of your deployment. This is detected automatically when you connect.": "你部署的底层模型。连接时会自动检测。",
509 "Prompt Post-Processing": "提示词后处理",532 "Prompt Post-Processing": "提示词后处理",
510 "Applies additional processing to the prompt before sending it to the API.": "在将提示词发送到 API 之前对其进行额外处理。",533 "Applies additional processing to the prompt before sending it to the API.": "在将提示词发送到 API 之前对其进行额外处理。",
511 "prompt_post_processing_none": "未选择",534 "prompt_post_processing_none": "未选择",
@@ -529,6 +552,7 @@
529 "Master Import": "全局导入",552 "Master Import": "全局导入",
530 "Export Advanced Formatting settings": "导出高级格式化设置",553 "Export Advanced Formatting settings": "导出高级格式化设置",
531 "Master Export": "全局导出",554 "Master Export": "全局导出",
555 "Grayed-out options have no effect when Chat Completion API is used.": "灰色选项在使用 聊天补全API 时无效。",
532 "Context Template": "上下文模板",556 "Context Template": "上下文模板",
533 "context_derived": "若可能,从模型的元数据获取。",557 "context_derived": "若可能,从模型的元数据获取。",
534 "Select your current Context Template": "选择你当前的上下文模板",558 "Select your current Context Template": "选择你当前的上下文模板",
@@ -728,6 +752,7 @@
728 "Delete a theme": "删除主题",752 "Delete a theme": "删除主题",
729 "Update a theme file": "更新主题文件",753 "Update a theme file": "更新主题文件",
730 "Save as a new theme": "另存为新主题",754 "Save as a new theme": "另存为新主题",
755 "This style applies to all avatars globaly, including your Persona, Character Management, Account selection, etc.": "此样式将应用在所有头像,包括您的用户设定、角色管理、帐户选择等。",
731 "Avatar Style:": "头像样式:",756 "Avatar Style:": "头像样式:",
732 "Circle": "圆形",757 "Circle": "圆形",
733 "Square": "正方形",758 "Square": "正方形",
@@ -737,6 +762,10 @@
737 "Flat": "扁平",762 "Flat": "扁平",
738 "Bubbles": "气泡",763 "Bubbles": "气泡",
739 "Document": "文档",764 "Document": "文档",
765 "Default display style for media attachments in chat messages. Extensions can override this setting.": "聊天消息中媒体附件的默认显示样式。扩展可以覆盖此设置。",
766 "Media Style:": "媒体样式:",
767 "List": "列表",
768 "Gallery": "画廊",
740 "Notifications:": "通知:",769 "Notifications:": "通知:",
741 "Top Left": "左上",770 "Top Left": "左上",
742 "Top Center": "顶部居中",771 "Top Center": "顶部居中",
@@ -835,9 +864,13 @@
835 "Find and delete backups, unused chats, files, images, etc.": "寻找和删除备份、未使用的聊天、文件、图片等。",864 "Find and delete backups, unused chats, files, images, etc.": "寻找和删除备份、未使用的聊天、文件、图片等。",
836 "Clean-Up": "清理",865 "Clean-Up": "清理",
837 "Smooth Streaming": "平滑流式传输",866 "Smooth Streaming": "平滑流式传输",
838 "Experimental feature. May not work for all backends.": "实验性功能。可能不适用于所有后端。",867 "Bypass smooth streaming in reasoning blocks.": "在推理块中不使用平滑流式传输。",
868 "Exclude 'Thinking...'": "排除“思考中...”",
839 "Slow": "慢",869 "Slow": "慢",
840 "Fast": "快",870 "Fast": "快",
871 "Fade in streamed text when it appears, instead of it just popping in": "流式传输的文本淡入显示,而不是直接弹出",
872 "Stream Fade-In": "流式淡入",
873 "Experimental feature. May not work for all backends.": "实验性功能。可能不适用于所有后端。",
841 "Play a sound when a message generation finishes": "当消息生成完毕时播放声音",874 "Play a sound when a message generation finishes": "当消息生成完毕时播放声音",
842 "Message Sound": "消息声音",875 "Message Sound": "消息声音",
843 "Only play a sound when ST's browser tab is unfocused": "仅在ST的浏览器标签页未被打开时播放声音",876 "Only play a sound when ST's browser tab is unfocused": "仅在ST的浏览器标签页未被打开时播放声音",
@@ -871,6 +904,9 @@
871 "Gradual push-out": "逐渐推出",904 "Gradual push-out": "逐渐推出",
872 "Always include examples": "始终包含示例",905 "Always include examples": "始终包含示例",
873 "Never include examples": "永不包含示例",906 "Never include examples": "永不包含示例",
907 "Image Swipe Behavior:": "图片滑动刷新行为:",
908 "Generate new": "生成新的",
909 "Roll over": "循环现有",
874 "Send on Enter": "按 Enter 发送",910 "Send on Enter": "按 Enter 发送",
875 "Disabled": "已禁用",911 "Disabled": "已禁用",
876 "Automatic (PC)": "自动(PC)",912 "Automatic (PC)": "自动(PC)",
@@ -896,6 +932,8 @@
896 "Allow {{user}}: in bot messages": "在机器人消息中允许 {{user}}: ",932 "Allow {{user}}: in bot messages": "在机器人消息中允许 {{user}}: ",
897 "Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "跳过消息文本中的编码和字符,允许一部分HTML标记以及Markdown",933 "Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "跳过消息文本中的编码和字符,允许一部分HTML标记以及Markdown",
898 "Show tags in responses": "在响应中显示标签",934 "Show tags in responses": "在响应中显示标签",
935 "Experimental Macro Engine": "实验性宏引擎",
936 "Experimental feature. Currently in development to test.": "实验性功能。目前正在开发测试中。",
899 "Allow AI messages in groups to contain lines spoken by other group members": "允许群聊中的AI输出群中其他成员说的话",937 "Allow AI messages in groups to contain lines spoken by other group members": "允许群聊中的AI输出群中其他成员说的话",
900 "Relax message trim in Groups": "减轻群聊中的消息修剪",938 "Relax message trim in Groups": "减轻群聊中的消息修剪",
901 "Log prompts to console": "将提示词输出到控制台",939 "Log prompts to console": "将提示词输出到控制台",
@@ -960,10 +998,15 @@
960 "Center": "居中",998 "Center": "居中",
961 "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景",999 "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景",
962 "Auto-select": "自动选择",1000 "Auto-select": "自动选择",
1001 "Add a new background": "添加新背景",
963 "Add Background": "添加背景",1002 "Add Background": "添加背景",
964 "Global": "全局",1003 "Global": "全局",
1004 "Chat": "聊天",
1005 "Make thumbnails smaller": "缩小缩略图",
1006 "Make thumbnails larger": "放大缩略图",
965 "bg_chat_hint_1": "使用生成的聊天背景",1007 "bg_chat_hint_1": "使用生成的聊天背景",
966 "bg_chat_hint_2": "扩展名将出现在这里。",1008 "bg_chat_hint_2": "扩展名将出现在这里。",
1009 "Scroll backgrounds to top": "回顶",
967 "Extensions": "扩展",1010 "Extensions": "扩展",
968 "Notify on extension updates": "在扩展更新时通知",1011 "Notify on extension updates": "在扩展更新时通知",
969 "Manage extensions": "管理扩展",1012 "Manage extensions": "管理扩展",
@@ -1004,7 +1047,6 @@
1004 "Click to lock your selected persona to the current character. Click again to remove the lock.": "点击将选择的用户设定与当前角色绑定。再次点击以解绑。",1047 "Click to lock your selected persona to the current character. Click again to remove the lock.": "点击将选择的用户设定与当前角色绑定。再次点击以解绑。",
1005 "Character": "角色",1048 "Character": "角色",
1006 "Click to lock your selected persona to the current chat. Click again to remove the lock.": "点击将选择的人设与当前聊天绑定。再次点击以解绑。",1049 "Click to lock your selected persona to the current chat. Click again to remove the lock.": "点击将选择的人设与当前聊天绑定。再次点击以解绑。",
1007 "Chat": "聊天",
1008 "Global Settings": "全局设置",1050 "Global Settings": "全局设置",
1009 "Show notifications on switching personas": "切换用户设定时显示通知",1051 "Show notifications on switching personas": "切换用户设定时显示通知",
1010 "When multiple personas are connected to a character, a popup will appear to select which one to use": "当多个用户设定与一个角色绑定时,会弹出一个弹窗让用户选择使用哪一个。",1052 "When multiple personas are connected to a character, a popup will appear to select which one to use": "当多个用户设定与一个角色绑定时,会弹出一个弹窗让用户选择使用哪一个。",
@@ -1038,7 +1080,7 @@
1038 "More...": "更多...",1080 "More...": "更多...",
1039 "Link to World Info": "链接到世界书",1081 "Link to World Info": "链接到世界书",
1040 "Import Card Lore": "导入角色卡的世界书",1082 "Import Card Lore": "导入角色卡的世界书",
1041 "Scenario Override": "场景覆盖",1083 "Character Settings Overrides": "角色设置覆盖",
1042 "Convert to Persona": "转换为用户角色",1084 "Convert to Persona": "转换为用户角色",
1043 "Rename": "重命名",1085 "Rename": "重命名",
1044 "Link to Source": "来源链接",1086 "Link to Source": "来源链接",
@@ -1078,7 +1120,7 @@
1078 "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "当选择“合并角色卡”时,角色的所有相应字段将被合并在一起。这意味着在故事字符串中,例如,所有角色描述都将合并为一个大文本。如果您希望将这些字段分开,可以在此处定义前缀或后缀。此值支持普通宏,还会将 {{char}} 替换为相关角色的名称,将 <FIELDNAME> 替换为部分的名称(例如:描述、个性、场景等)",1120 "When 'Join character cards' is selected, all respective fields of the characters are being joined together.This means that in the story string for example all character descriptions will be joined to one big text.If you want those fields to be separated, you can define a prefix or suffix here.This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)": "当选择“合并角色卡”时,角色的所有相应字段将被合并在一起。这意味着在故事字符串中,例如,所有角色描述都将合并为一个大文本。如果您希望将这些字段分开,可以在此处定义前缀或后缀。此值支持普通宏,还会将 {{char}} 替换为相关角色的名称,将 <FIELDNAME> 替换为部分的名称(例如:描述、个性、场景等)",
1079 "Inserted after each part of the joined fields.": "插入到加入字段的每个部分之后。",1121 "Inserted after each part of the joined fields.": "插入到加入字段的每个部分之后。",
1080 "Join Suffix": "加入后缀",1122 "Join Suffix": "加入后缀",
1081 "Set a group chat scenario": "设置群聊背景",1123 "Set group chat character settings overrides": "设置群聊角色设置覆盖",
1082 "Click to allow/forbid the use of external media for this group.": "单击以允许/禁止该组使用外部媒体。",1124 "Click to allow/forbid the use of external media for this group.": "单击以允许/禁止该组使用外部媒体。",
1083 "Restore collage avatar": "恢复拼贴头像",1125 "Restore collage avatar": "恢复拼贴头像",
1084 "Allow self responses": "允许自我回复",1126 "Allow self responses": "允许自我回复",
@@ -1156,9 +1198,9 @@
1156 "Save": "保存",1198 "Save": "保存",
1157 "Chat History": "聊天记录",1199 "Chat History": "聊天记录",
1158 "Import Chat": "导入聊天",1200 "Import Chat": "导入聊天",
1159 "Copy to global backgrounds": "复制到全局背景",
1160 "Lock": "锁定",1201 "Lock": "锁定",
1161 "Unlock": "解锁",1202 "Unlock": "解锁",
1203 "Copy to global backgrounds": "复制到全局背景",
1162 "Rename Background": "重命名背景",1204 "Rename Background": "重命名背景",
1163 "Delete Background": "删除背景",1205 "Delete Background": "删除背景",
1164 "Select a World Info file for": "选择一个世界书文件给",1206 "Select a World Info file for": "选择一个世界书文件给",
@@ -1191,6 +1233,8 @@
1191 "Optional Filter": "可选过滤器",1233 "Optional Filter": "可选过滤器",
1192 "Keywords or Regexes (ignored if empty)": "关键字或正则表达式(如果为空则忽略)",1234 "Keywords or Regexes (ignored if empty)": "关键字或正则表达式(如果为空则忽略)",
1193 "Comma separated list (ignored if empty)": "逗号分隔列表(如果为空则忽略)",1235 "Comma separated list (ignored if empty)": "逗号分隔列表(如果为空则忽略)",
1236 "wi_outlet_name": "为此世界信息条目设置锚点名称。\n\n位置为“锚点”的世界信息条目不会自动添加到提示词中。相反,它们将被收集并可作为提示词中的宏使用。\n在提示词中任何想要添加此特定锚点的所有世界信息条目的位置添加 {{outlet::YourName}}。",
1237 "Outlet Name": "锚点名称",
1194 "Use global setting": "使用全局设置",1238 "Use global setting": "使用全局设置",
1195 "Case-Sensitive": "区分大小写",1239 "Case-Sensitive": "区分大小写",
1196 "Use global": "使用全局",1240 "Use global": "使用全局",
@@ -1260,6 +1304,7 @@
1260 "at Depth System": "@D ⚙ [系统]在深度​​️",1304 "at Depth System": "@D ⚙ [系统]在深度​​️",
1261 "at Depth User": "@D 👤 [用户]在深度",1305 "at Depth User": "@D 👤 [用户]在深度",
1262 "at Depth AI": "@D 🤖 [AI]在深度",1306 "at Depth AI": "@D 🤖 [AI]在深度",
1307 "Outlet": "➡️ 锚点",
1263 "Depth": "深度",1308 "Depth": "深度",
1264 "Order:": "顺序:",1309 "Order:": "顺序:",
1265 "Order": "顺序",1310 "Order": "顺序",
@@ -1302,6 +1347,7 @@
1302 "Narrate": "朗读",1347 "Narrate": "朗读",
1303 "Exclude message from prompts": "从提示词中排除消息",1348 "Exclude message from prompts": "从提示词中排除消息",
1304 "Include message in prompts": "将消息包含在提示词中",1349 "Include message in prompts": "将消息包含在提示词中",
1350 "Toggle media display style": "切换媒体显示样式",
1305 "Embed file or image": "嵌入文件或图像",1351 "Embed file or image": "嵌入文件或图像",
1306 "Create checkpoint": "创建检查点",1352 "Create checkpoint": "创建检查点",
1307 "Create Branch": "创建分支",1353 "Create Branch": "创建分支",
@@ -1321,10 +1367,6 @@
1321 "Collapse all reasoning blocks": "折叠所有推理块",1367 "Collapse all reasoning blocks": "折叠所有推理块",
1322 "Copy reasoning": "复制推理内容",1368 "Copy reasoning": "复制推理内容",
1323 "Edit reasoning": "编辑推理内容",1369 "Edit reasoning": "编辑推理内容",
1324 "Expand and zoom": "展开并缩放",
1325 "Caption": "标题",
1326 "Swipe left": "向左滑动",
1327 "Swipe right": "向右滑动",
1328 "Welcome to SillyTavern!": "欢迎来到 SillyTavern!",1370 "Welcome to SillyTavern!": "欢迎来到 SillyTavern!",
1329 "SillyTavern is aimed at advanced users.": "SillyTavern 面向高级用户。",1371 "SillyTavern is aimed at advanced users.": "SillyTavern 面向高级用户。",
1330 "welcome_message_part_1": "阅读",1372 "welcome_message_part_1": "阅读",
@@ -1362,6 +1404,12 @@
1362 "(This will be the first message from the character that starts every chat)": "(这是每次聊天开始时角色的第一条消息)",1404 "(This will be the first message from the character that starts every chat)": "(这是每次聊天开始时角色的第一条消息)",
1363 "View contents": "查看内容",1405 "View contents": "查看内容",
1364 "Remove the file": "删除文件",1406 "Remove the file": "删除文件",
1407 "Expand and zoom": "展开并缩放",
1408 "Caption": "标题",
1409 "Swipe left": "向左滑动",
1410 "Swipe right": "向右滑动",
1411 "Play": "播放",
1412 "Mute": "静音",
1365 "Author's Note": "作者注释",1413 "Author's Note": "作者注释",
1366 "Unique to this chat": "仅对此聊天生效",1414 "Unique to this chat": "仅对此聊天生效",
1367 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",1415 "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。",
@@ -1479,6 +1527,7 @@
1479 "API": "API",1527 "API": "API",
1480 "Text Generation WebUI (oobabooga)": "文本生成 WebUI (oobabooga)",1528 "Text Generation WebUI (oobabooga)": "文本生成 WebUI (oobabooga)",
1481 "Model": "模型",1529 "Model": "模型",
1530 "Refresh model list": "刷新模型列表",
1482 "currently_selected": "[当前选定]",1531 "currently_selected": "[当前选定]",
1483 "currently_loaded": "[当前正在加载]",1532 "currently_loaded": "[当前正在加载]",
1484 "Custom Model Tag": "自定义模型标签",1533 "Custom Model Tag": "自定义模型标签",
@@ -1513,21 +1562,21 @@
1513 "Character Expressions": "角色表情",1562 "Character Expressions": "角色表情",
1514 "Use the selected API from Chat Translation extension settings.": "使用聊天翻译扩展程序中已选择的API。",1563 "Use the selected API from Chat Translation extension settings.": "使用聊天翻译扩展程序中已选择的API。",
1515 "Translate text to English before classification": "分类之前将文本翻译成英文",1564 "Translate text to English before classification": "分类之前将文本翻译成英文",
1516 "A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.": "A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.",1565 "A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.": "使单个关键词可以有多个表情包。每当出现该关键词时,将随机选择其中一个。",
1517 "Allow multiple sprites per expression": "Allow multiple sprites per expression",1566 "Allow multiple sprites per expression": "允许关键词重复",
1518 "If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.": "If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.",1567 "If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.": "再次使用相同关键词时,将重新刷新表情包。仅适用于关键词重复的表情包。",
1519 "Re-roll if same expression is used again": "Re-roll if same sprite is used again",1568 "Re-roll if same expression is used again": "再次使用相同关键词时刷新表情包。",
1520 "Classifier API": "分类器 API",1569 "Classifier API": "分类器 API",
1521 "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",1570 "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",
1522 "Main API": "当前连接的 API",1571 "Main API": "当前连接的 API",
1523 "WebLLM Extension": "WebLLM 扩展程序",1572 "WebLLM Extension": "WebLLM 扩展程序",
1524 "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.": "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.",1573 "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.": "使用 LLM 或 WebLLM 分类器时,仅显示和使用已分配表情包的关键词。",
1525 "Filter expressions for available sprites": "Filter expressions for available sprites",1574 "Filter expressions for available sprites": "筛选已有表情包的关键词",
1526 "LLM Prompt": "大语言模型提示词",1575 "LLM Prompt": "大语言模型提示词",
1527 "Used in addition to JSON schemas and function calling.": "Used in addition to JSON schemas and function calling.",1576 "Used in addition to JSON schemas and function calling.": "可与 JSON结构 和 函数调用 一同使用。",
1528 "LLM Prompt Strategy": "LLM Prompt Strategy",1577 "LLM Prompt Strategy": "LLM 提示词策略",
1529 "Limited Context": "Limited Context",1578 "Limited Context": "限制上下文",
1530 "Full Context": "Full Context",1579 "Full Context": "完整上下文",
1531 "Default / Fallback Expression": "默认/后备表达式",1580 "Default / Fallback Expression": "默认/后备表达式",
1532 "Set the default and fallback expression being used when no matching expression is found.": "设置在未找到匹配表达式时使用的默认表达式和后备表达式。",1581 "Set the default and fallback expression being used when no matching expression is found.": "设置在未找到匹配表达式时使用的默认表达式和后备表达式。",
1533 "Custom Expressions": "自定义表达式",1582 "Custom Expressions": "自定义表达式",
@@ -1640,46 +1689,53 @@
1640 "macro for manual injection)": "宏用于手动注入)",1689 "macro for manual injection)": "宏用于手动注入)",
1641 "Color": "颜色",1690 "Color": "颜色",
1642 "Only apply color as accent": "仅应用颜色作为强调",1691 "Only apply color as accent": "仅应用颜色作为强调",
1643 "ext_regex_new_global_script_desc": "新增「全局」正则表达式",
1644 "ext_regex_new_scoped_script_desc": "新增「局部」正则表达式",
1645 "ext_regex_new_preset_script_desc": "新增「预设」正则表达式",
1646 "ext_regex_debugger_active_rules": "激活的规则",1692 "ext_regex_debugger_active_rules": "激活的规则",
1693 "ext_regex_debugger_save_order_help": "保存当前规则顺序",
1647 "ext_regex_debugger_save_order": "保存此顺序",1694 "ext_regex_debugger_save_order": "保存此顺序",
1648 "ext_regex_debugger_testing_area": "测试区域",1695 "ext_regex_debugger_testing_area": "测试区域",
1649 "ext_regex_debugger_raw_input": "原始输入",1696 "ext_regex_debugger_raw_input": "原始输入",
1697 "ext_regex_debugger_run_test_help": "运行测试流程",
1650 "ext_regex_debugger_run_test": "运行测试",1698 "ext_regex_debugger_run_test": "运行测试",
1651 "ext_regex_debugger_display_replace": "替换",1699 "ext_regex_debugger_display_replace": "替换",
1652 "ext_regex_debugger_display_highlight": "高亮",1700 "ext_regex_debugger_display_highlight": "高亮",
1653 "ext_regex_debugger_render_text": "渲染为文本",1701 "ext_regex_debugger_render_text": "渲染为文本",
1654 "ext_regex_debugger_render_message": "渲染为消息",1702 "ext_regex_debugger_render_message": "渲染为消息",
1655 "ext_regex_debugger_step_by_step": "逐步转换",1703 "ext_regex_debugger_step_by_step": "逐步转换",
1704 "Expand view": "展开视图",
1656 "ext_regex_debugger_final_output": "最终输出",1705 "ext_regex_debugger_final_output": "最终输出",
1706 "Edit Rule": "编辑规则",
1657 "ext_regex_title": "正则",1707 "ext_regex_title": "正则",
1658 "ext_regex_presets": "正则预设",1708 "ext_regex_new_global_script_desc": "新增「全局」正则表达式",
1659 "ext_regex_presets_desc": "可以轻松保存并切换多组正则开关状态。",
1660 "ext_regex_preset_create": "创建新预设",
1661 "ext_regex_preset_update": "更新已有预设",
1662 "ext_regex_preset_apply": "重新应用当前预设",
1663 "ext_regex_preset_delete": "删除当前预设",
1664 "ext_regex_new_global_script": "新建全局正则",1709 "ext_regex_new_global_script": "新建全局正则",
1665 "ext_regex_new_scoped_script": "新建局部正则",1710 "ext_regex_new_preset_script_desc": "新增「预设」正则表达式",
1666 "ext_regex_new_preset_script": "新建预设正则",1711 "ext_regex_new_preset_script": "新建预设正则",
1712 "ext_regex_new_scoped_script_desc": "新增「局部」正则表达式",
1713 "ext_regex_new_scoped_script": "新建局部正则",
1667 "ext_regex_import_script": "导入正则",1714 "ext_regex_import_script": "导入正则",
1668 "ext_regex_bulk_edit": "批量编辑",1715 "ext_regex_bulk_edit": "批量编辑",
1669 "ext_regex_debugger_desc": "高级正则调试工具",1716 "ext_regex_debugger_desc": "高级正则调试工具",
1670 "ext_regex_debugger": "调试工具",1717 "ext_regex_debugger": "调试工具",
1718 "ext_regex_move_to_global": "移至全局",
1719 "ext_regex_move_to_preset": "移至预设",
1720 "ext_regex_move_to_scoped": "移至局部",
1671 "Export": "导出",1721 "Export": "导出",
1722 "ext_regex_presets": "正则预设",
1723 "ext_regex_presets_desc": "可以轻松保存并切换多组正则开关状态。",
1724 "ext_regex_preset_create": "创建新预设",
1725 "ext_regex_preset_update": "更新已有预设",
1726 "ext_regex_preset_apply": "重新应用当前预设",
1727 "ext_regex_preset_delete": "删除当前预设",
1672 "ext_regex_global_scripts": "全局正则脚本",1728 "ext_regex_global_scripts": "全局正则脚本",
1673 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",1729 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",
1674 "No scripts found": "没有找到脚本",1730 "No scripts found": "没有找到脚本",
1675 "ext_regex_scoped_scripts": "局部正则脚本",
1676 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
1677 "ext_regex_preset_scripts": "预设正则脚本",1731 "ext_regex_preset_scripts": "预设正则脚本",
1732 "ext_regex_disallow_preset": "不允许使用预设正则",
1733 "ext_regex_allow_preset": "允许使用预设正则",
1678 "ext_regex_preset_scripts_desc": "只影响当前预设,保存在预设中",1734 "ext_regex_preset_scripts_desc": "只影响当前预设,保存在预设中",
1735 "ext_regex_scoped_scripts": "局部正则脚本",
1679 "ext_regex_disallow_scoped": "不允许使用局部正则",1736 "ext_regex_disallow_scoped": "不允许使用局部正则",
1680 "ext_regex_allow_scoped": "允许使用局部正则",1737 "ext_regex_allow_scoped": "允许使用局部正则",
1681 "ext_regex_disallow_preset": "不允许使用预设正则",1738 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
1682 "ext_regex_allow_preset": "允许使用预设正则",
1683 "Regex Editor": "正则表达式编辑器",1739 "Regex Editor": "正则表达式编辑器",
1684 "Test Mode": "测试模式",1740 "Test Mode": "测试模式",
1685 "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。",1741 "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。",
@@ -1725,22 +1781,17 @@
1725 "Would you like to allow using them?": "你想要启用它们吗?",1781 "Would you like to allow using them?": "你想要启用它们吗?",
1726 "If you want to do it later, select 'Regex' from the extensions menu.": "你可以稍后在扩展栏的 \"正则\" 区域管理它们。",1782 "If you want to do it later, select 'Regex' from the extensions menu.": "你可以稍后在扩展栏的 \"正则\" 区域管理它们。",
1727 "ext_regex_import_target": "导入至:",1783 "ext_regex_import_target": "导入至:",
1784 "This preset has embedded regex script(s).": "此预设包含内置正则脚本。",
1728 "ext_regex_disable_script": "禁用脚本",1785 "ext_regex_disable_script": "禁用脚本",
1729 "ext_regex_enable_script": "启用脚本",1786 "ext_regex_enable_script": "启用脚本",
1730 "ext_regex_edit_script": "编辑脚本",1787 "Show more options": "展示更多选项",
1731 "ext_regex_move_to_global": "移至全局",
1732 "ext_regex_move_to_scoped": "移至局部",
1733 "ext_regex_move_to_preset": "移至预设",
1734 "ext_regex_export_script": "导出脚本",1788 "ext_regex_export_script": "导出脚本",
1789 "ext_regex_edit_script": "编辑脚本",
1735 "ext_regex_delete_script": "删除脚本",1790 "ext_regex_delete_script": "删除脚本",
1736 "This preset has embedded regex script(s).": "此预设包含内置正则脚本。",
1737 "Preset '${0}' contains enabled regex scripts": "预设 '${0}' 包含被启用的正则脚本",
1738 "Reload the chat for regex to take effect": "重新加载聊天以使正则生效",
1739 "Click here to reload immediately": "点击此处立即重新加载",
1740 "If you want to do it later, select \"Regex\" from the extensions menu.": "您可稍后从扩展菜单中的(Regex)启用。",
1741 "Trigger Stable Diffusion": "触发Stable Diffusion",1791 "Trigger Stable Diffusion": "触发Stable Diffusion",
1742 "Abort current image generation task": "中止当前图像生成",1792 "Abort current image generation task": "中止当前图像生成",
1743 "Stop Image Generation": "停止图像生成",1793 "Stop Image Generation": "停止图像生成",
1794 "Send me a picture of:": "给我发一张……的照片:",
1744 "sd_Yourself": "你自己",1795 "sd_Yourself": "你自己",
1745 "sd_Your_Face": "你的脸",1796 "sd_Your_Face": "你的脸",
1746 "sd_Me": "我",1797 "sd_Me": "我",
@@ -1751,8 +1802,8 @@
1751 "Image Generation": "图像生成",1802 "Image Generation": "图像生成",
1752 "sd_refine_mode": "允许在将提示词发送到生成 API 之前手动编辑提示词",1803 "sd_refine_mode": "允许在将提示词发送到生成 API 之前手动编辑提示词",
1753 "sd_refine_mode_txt": "生成之前编辑提示词",1804 "sd_refine_mode_txt": "生成之前编辑提示词",
1754 "sd_function_tool": "Use the function tool to automatically detect intents to generate images.",1805 "sd_function_tool": "使用函数工具自动检测生成图像的意图。",
1755 "sd_function_tool_txt": "Use function tool",1806 "sd_function_tool_txt": "使用函数工具",
1756 "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。",1807 "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。",
1757 "sd_interactive_mode_txt": "交互模式",1808 "sd_interactive_mode_txt": "交互模式",
1758 "sd_multimodal_captioning": "使用多模态字幕根据用户和角色的头像生成提示词。",1809 "sd_multimodal_captioning": "使用多模态字幕根据用户和角色的头像生成提示词。",
@@ -1770,35 +1821,45 @@
1770 "sd_auto_auth_warning_2": "注意!服务器必须可从 SillyTavern 主机访问。",1821 "sd_auto_auth_warning_2": "注意!服务器必须可从 SillyTavern 主机访问。",
1771 "sd_drawthings_url": "例如:{{drawthings_url}}",1822 "sd_drawthings_url": "例如:{{drawthings_url}}",
1772 "sd_drawthings_auth_txt": "运行 DrawThings 应用程序并在 UI 中启用 HTTP API 开关!必须可以从 SillyTavern 主机访问服务器。",1823 "sd_drawthings_auth_txt": "运行 DrawThings 应用程序并在 UI 中启用 HTTP API 开关!必须可以从 SillyTavern 主机访问服务器。",
1773 "Model ID": "Model ID",1824 "Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.": "提示:在 Hugging Face(文本补全)API 设置中保存一个 API 密钥以在此处使用。",
1825 "Model ID": "模型 ID",
1774 "e.g. black-forest-labs/FLUX.1-dev": "例如:black-forest-labs/FLUX.1-dev",1826 "e.g. black-forest-labs/FLUX.1-dev": "例如:black-forest-labs/FLUX.1-dev",
1827 "Hint: Save an API key in the Chutes (Chat Completion) API settings to use it here.": "提示:在 Chutes(聊天补全)API 设置中保存一个 API 密钥以在此处使用。",
1828 "Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.": "提示:在 Electron Hub(聊天补全)API 设置中保存一个 API 密钥以在此处使用。",
1829 "Image Quality": "画面质量",
1830 "Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.": "提示:在 NanoGPT(聊天补全)API 设置中保存一个 API 密钥以在此处使用。",
1775 "sd_vlad_url": "例如:{{vlad_url}}",1831 "sd_vlad_url": "例如:{{vlad_url}}",
1776 "The server must be accessible from the SillyTavern host machine.": "必须能够从 SillyTavern 主机访问该服务器。",1832 "The server must be accessible from the SillyTavern host machine.": "必须能够从 SillyTavern 主机访问该服务器。",
1777 "Hint: Save an API key in AI Horde API settings to use it here.": "提示:在 Horde AI API 设置中保存一个 API 密钥以便在此处使用它。",1833 "Hint: Save an API key in AI Horde API settings to use it here.": "提示:在 Horde AI API 设置中保存一个 API 密钥以在此处使用。",
1778 "Allow NSFW images from Horde": "允许来自 Horde 的 NSFW 图片",1834 "Allow NSFW images from Horde": "允许来自 Horde 的 NSFW 图片",
1779 "Sanitize prompts (recommended)": "净化提示词(推荐)",1835 "Sanitize prompts (recommended)": "净化提示词(推荐)",
1780 "Automatically adjust generation parameters to ensure free image generations.": "自动调整生成参数,确保图像生成自由。",1836 "Automatically adjust generation parameters to ensure free image generations.": "自动调整生成参数,确保图像生成自由。",
1781 "Avoid spending Anlas": "避免花费 Anlas",1837 "Avoid spending Anlas": "避免花费 Anlas",
1782 "Opus tier": "(作品层)",1838 "Opus tier": "(Opus 级别)",
1783 "View my Anlas": "查看我的目录",1839 "View my Anlas": "查看我的 Anlas",
1840 "Hint: Save an API key in the NovelAI API settings to use it here.": "提示:在 NovelAI API 设置中保存一个 API 密钥以在此处使用。",
1784 "Click to set": "点击设置",1841 "Click to set": "点击设置",
1785 "These settings only apply to DALL-E 3": "这些设置仅适用于 DALL-E 3",
1786 "Image Style": "图像风格",1842 "Image Style": "图像风格",
1787 "Image Quality": "画面质量",
1788 "Standard": "标准",1843 "Standard": "标准",
1789 "HD": "高清",1844 "HD": "高清",
1845 "Duration": "持续时间",
1846 "Short (4 seconds)": "短(4秒)",
1847 "Medium (8 seconds)": "中(8秒)",
1848 "Long (16 seconds)": "长(16秒)",
1790 "sd_comfy_url": "例如:{{comfy_url}}",1849 "sd_comfy_url": "例如:{{comfy_url}}",
1850 "sd_comfy_runpod_url": "eg: https://api.runpod.ai/v2/<your endpoint id>",
1791 "Open workflow editor": "打开工作流编辑器",1851 "Open workflow editor": "打开工作流编辑器",
1792 "Create new workflow": "创建新的工作流",1852 "Create new workflow": "创建新的工作流",
1793 "Delete workflow": "删除工作流",1853 "Delete workflow": "删除工作流",
1854 "Enables prompt enhancing (passes prompts through an LLM to add detail).": "允许提示词增强(通过大语言模型处理提示词以添加细节)。",
1794 "Enhance": "提高",1855 "Enhance": "提高",
1795 "You can find your API key in the Stability AI dashboard.": "您可以在 Stability AI 仪表板中找到您的 API 密钥。",1856 "You can find your API key in the Stability AI dashboard.": "您可以在 Stability AI 仪表板中找到您的 API 密钥。",
1796 "Style Preset": "风格预设",1857 "Style Preset": "风格预设",
1797 "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.": "是否对提示词使用提示词增强(Upsampling)。若开启,则会自动修改提示词,使回复更有创造力。",1858 "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.": "是否对提示词使用提示词增强(Upsampling)。若开启,则会自动修改提示词,使回复更有创造力。",
1798 "Prompt Upsampling": "提示词增强(Upsampling)",1859 "Prompt Upsampling": "提示词增强(Upsampling)",
1860 "Duration (Veo)": "持续时间(Veo)",
1799 "Sampling method": "采样方法",1861 "Sampling method": "采样方法",
1800 "Scheduler": "调度器",1862 "Scheduler": "调度器",
1801 "Resolution": "分辨率",
1802 "Upscaler": "图像扩大器",1863 "Upscaler": "图像扩大器",
1803 "Sampling steps": "采样步数",1864 "Sampling steps": "采样步数",
1804 "Width": "宽度",1865 "Width": "宽度",
@@ -1812,15 +1873,15 @@
1812 "Hires. Fix": "高清修复",1873 "Hires. Fix": "高清修复",
1813 "Karras": "Karras",1874 "Karras": "Karras",
1814 "Not all samplers supported.": "并非所有采样器都受支持。",1875 "Not all samplers supported.": "并非所有采样器都受支持。",
1815 "sd_adetailer_face": "Use ADetailer with face model during the generation. The ADetailer extension must be installed on the backend.",1876 "sd_adetailer_face": "使用 ADetailer 脸部模型进行生成。后端必须安装 ADetailer 扩展。",
1816 "Use ADetailer (Face)": "使用 ADetailer(脸部)",1877 "Use ADetailer (Face)": "使用 ADetailer(脸部)",
1817 "SMEA versions of samplers are modified to perform better at high resolution.": "SMEA 版本的采样器经过修改,在高分辨率下性能更佳。",1878 "SMEA versions of samplers are modified to perform better at high resolution.": "SMEA 版本的采样器经过修改,在高分辨率下性能更佳。",
1818 "SMEA": "中小企业协会",1879 "SMEA": "中小企业协会",
1819 "DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.": "SMEA 采样器的 DYN 变体通常会产生更加多样化的输出,但在非常高的分辨率下可能会失败。",1880 "DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.": "SMEA 采样器的 DYN 变体通常会产生更加多样化的输出,但在非常高的分辨率下可能会失败。",
1820 "DYN": "动态",1881 "DYN": "动态",
1821 "Decrisper": "去伪器",1882 "Decrisper": "去伪器",
1822 "Enable guidance only after body has been formed, to improve diversity and saturation of samples. May reduce relevance": "Enable guidance only after body has been formed, to improve diversity and saturation of samples. May reduce relevance",1883 "Enable guidance only after body has been formed, to improve diversity and saturation of samples. May reduce relevance": "仅在图像主体形成后启用引导,以提高样本的多样性和饱和度。可能会降低相关性",
1823 "Variety+": "Variety+",1884 "Variety+": "多样性+",
1824 "(-1 for random)": "(“-1”为随机)",1885 "(-1 for random)": "(“-1”为随机)",
1825 "Preset for prompt prefix and negative prompt": "提示词前缀和负面提示词的预设",1886 "Preset for prompt prefix and negative prompt": "提示词前缀和负面提示词的预设",
1826 "Style": "风格",1887 "Style": "风格",
@@ -1863,6 +1924,7 @@
1863 "ext_translate_target_lang": "目标语言",1924 "ext_translate_target_lang": "目标语言",
1864 "ext_translate_clear": "清空设置",1925 "ext_translate_clear": "清空设置",
1865 "Select TTS Provider": "选择 文本转语音 的服务提供商",1926 "Select TTS Provider": "选择 文本转语音 的服务提供商",
1927 "tts_refresh": "刷新",
1866 "tts_enabled": "已启用",1928 "tts_enabled": "已启用",
1867 "Narrate user messages": "朗读用户消息",1929 "Narrate user messages": "朗读用户消息",
1868 "Auto Generation": "自动生成",1930 "Auto Generation": "自动生成",
@@ -1875,18 +1937,22 @@
1875 "Skip codeblocks": "跳过代码块",1937 "Skip codeblocks": "跳过代码块",
1876 "Skip tagged blocks": "跳过标签块里的内容(<标签>跳过这里</标签>)",1938 "Skip tagged blocks": "跳过标签块里的内容(<标签>跳过这里</标签>)",
1877 "Pass Asterisks to TTS Engine": "将星号传递给文本转语音服务",1939 "Pass Asterisks to TTS Engine": "将星号传递给文本转语音服务",
1878 "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.": "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.",1940 "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.": "最佳效果:启用“将星号传递给文本转语音服务”,同时禁用“只朗读引号内文本”和“忽略*星号内文本*(即使其被引号包裹)”功能。",
1879 "Different voices for quotes and text inside asterisks": "Different voices for \"quotes\", *text inside asterisks* and other text",1941 "Different voices for quotes and text inside asterisks": "为“引号内文本”、*星号内文本*使用不同的声音",
1880 "Audio Playback Speed": "音频播放速度",1942 "Audio Playback Speed": "音频播放速度",
1943 "Available voices": "可用声音",
1881 "Vector Storage": "向量存储",1944 "Vector Storage": "向量存储",
1882 "Vectorization Source": "向量化源",1945 "Vectorization Source": "向量化源",
1883 "Local (Transformers)": "本地(Transformers)",1946 "Local (Transformers)": "本地(Transformers)",
1884 "Secondary Embedding endpoint URL": "Secondary Embedding endpoint URL",
1885 "Vectorization Model": "向量化模型",1947 "Vectorization Model": "向量化模型",
1948 "Hint: Set your Chutes API key in API Connections.": "提示:在 API 连接设置中设置 Chutes API 密钥。",
1949 "Hint: Set your Electron Hub API key in API Connections.": "提示:在 API 连接设置中设置 Electron Hub API 密钥。",
1950 "Secondary Embedding endpoint URL": "次级向量化端点 URL",
1886 "Keep model in memory": "将模型保存在内存中",1951 "Keep model in memory": "将模型保存在内存中",
1887 "Hint: Set the URL in the API connection settings.": "提示:在 API 连接设置中设置 URL。",1952 "Hint: Set the URL in the API connection settings.": "提示:在 API 连接设置中设置 URL。",
1888 "The server MUST be started with the --embedding flag to use this feature!": "服务器必须使用 --embedding 标志启动才能使用此功能!",1953 "The server MUST be started with the --embedding flag to use this feature!": "服务器必须使用 --embedding 标志启动才能使用此功能!",
1889 "NomicAI API Key": "NomicAI API 密钥",1954 "NomicAI API Key": "NomicAI API 密钥",
1955 "Hint: Set your OpenRouter API key in API Connections.": "提示:在 API 连接设置中设置 OpenRouter API 密钥。",
1890 "Query messages": "查询消息",1956 "Query messages": "查询消息",
1891 "Score threshold": "分数阈值",1957 "Score threshold": "分数阈值",
1892 "Chunk boundary": "区块边界",1958 "Chunk boundary": "区块边界",
@@ -2115,106 +2181,16 @@
2115 "World Info:": "世界书:",2181 "World Info:": "世界书:",
2116 "Chat History:": "聊天记录:",2182 "Chat History:": "聊天记录:",
2117 "Extensions:": "扩展程序:",2183 "Extensions:": "扩展程序:",
2118 "Bias:": "Bias:",2184 "Bias:": "偏置:",
2119 "Total Tokens in Prompt:": "提示词的总Token数量:",2185 "Total Tokens in Prompt:": "提示词的总Token数量:",
2120 "Max Context": "最大上下文:",2186 "Max Context": "最大上下文:",
2121 "(Context Size - Response Length)": "(上下文长度 - 回复长度)",2187 "(Context Size - Response Length)": "(上下文长度 - 回复长度)",
2122 "System-wide Replacement Macros (in order of evaluation):": "系统范围的替换宏(按评估顺序):",
2123 "help_macros_1": "仅适用于斜线命令批处理。替换为上一个命令的返回结果。",
2124 "help_macros_2": "仅插入一个换行符。",
2125 "help_macros_3": "修剪此宏周围的换行符。",
2126 "help_macros_4": "没有操作,只是一个空字符串。",
2127 "help_macros_5": "API 设置中定义的全局提示。仅在高级定义提示覆盖中有效。",
2128 "help_macros_6": "用户输入",
2129 "help_macros_7": "角色的主提示覆盖",
2130 "help_macros_8": "角色越狱提示覆盖",
2131 "help_macros_9": "角色描述",
2132 "help_macros_10": "人物性格",
2133 "help_macros_11": "角色场景",
2134 "help_macros_12": "您当前的角色描述",
2135 "help_macros_13": "角色对话示例",
2136 "help_macros_14": "未格式化的对话示例",
2137 "help_macros_summary": "“总结”扩展程序生成的最新聊天总结(如果有)。",
2138 "help_macros_15": "您当前的用户设定名称",
2139 "help_macros_16": "角色的名字",
2140 "help_macros_17": "角色的版本号",
2141 "help_macros_charDepthPrompt": "角色的 @ 深度注释",
2142 "help_macros_18": "以逗号分隔的群成员名称列表或单人聊天中的角色名称。别名:{{charIfNotGroup}}",
2143 "help_groupNotMuted": "与 {{group}} 相同,但排除被禁言的成员",
2144 "help_macros_19": "当前选定的 API 的文本生成模型名称。",
2145 "Can be inaccurate!": "不一定准确!",
2146 "help_macros_20": "最新聊天消息的文本。",
2147 "help_macros_lastUser": "最后的用户聊天消息文本。",
2148 "help_macros_lastChar": "最后的角色聊天消息文本。",
2149 "help_macros_21": "最新聊天消息的索引号。对于斜线命令批处理很有用。",
2150 "help_macros_22": "上下文中包含的第一条消息的 ID。要求在当前会话中至少运行一次生成。",
2151 "help_macros_firstDisplayedMessageId": "第一条载入可见聊天的消息的ID",
2152 "help_macros_23": "最后一条聊天消息中当前滑动的 ID(以 1 为基数)。如果最后一条消息是用户或提示隐藏的,则为空字符串。",
2153 "help_macros_24": "最后一条聊天消息中的滑动次数。如果最后一条消息是用户隐藏或提示隐藏的,则为空字符串。",
2154 "help_macros_reverse": "反转宏的内容。",
2155 "help_macros_25": "您可以在此处留言,宏将被替换为空白内容。AI 看不到。",
2156 "help_macros_26": "当前时间",
2157 "help_macros_27": "当前日期",
2158 "help_macros_28": "当前工作日",
2159 "help_macros_29": "当前 ISO 时间(24 小时制)",
2160 "help_macros_30": "当前 ISO 日期 (YYYY-MM-DD)",
2161 "help_macros_31": "指定格式的当前日期/时间,例如德国日期/时间:",
2162 "help_macros_32": "指定 UTC 时区偏移量的当前时间,例如 UTC-4 或 UTC+2",
2163 "help_macros_33": "time1 和 time2 之间的时间差。接受时间和日期宏。(例如:{{timeDiff::{{isodate}} {{time}}::2024/5/11 12:30:00}})",
2164 "help_macros_34": "距离上次用户消息发送的时间",
2165 "help_macros_35": "为 AI 设置行为偏差,直到下一个用户输入。文本周围的引号很重要。",
2166 "help_macros_36": "掷骰子。(例如:",
2167 "space_ will roll a 6-sided dice and return a number between 1 and 6)": "将掷一个 6 面骰子并返回 1 到 6 之间的数字)",
2168 "help_macros_37": "从列表中返回一个随机项目。(例如:",
2169 "space_ will return 1 of the 4 numbers at random. Works with text lists too.": "将随机返回 4 个数字中的 1 个。也适用于文本列表。",
2170 "help_macros_38": "随机的替代语法允许在列表项中使用逗号。",
2171 "help_macros_39": "从列表中随机挑选一项。工作原理与 {{random}} 相同,具有相同的语法选项,但一旦挑选,挑选将一直持续到本次聊天,不会在连续消息和提示处理中重新滚动。",
2172 "help_macros_40": "如果使用文本生成 WebUI 后端,则动态地将引号中的文本添加到禁用单词序列中。对其他后端不执行任何操作。可以在任何地方使用(角色描述、WI、AN 等)。文本周围的引号很重要。",
2173 "help_macros_isMobile": "当为移动端时为\"true\",反之为\"false\"",
2174 "Instruct Mode and Context Template Macros:": "指导模式和上下文模板宏:",
2175 "(enabled in the Advanced Formatting settings)": "(在高级格式设置中启用)",
2176 "help_macros_41": "令牌中允许的最大提示长度 = (上下文长度 - 响应长度)",
2177 "help_macros_42": "上下文模板示例对话分隔符",
2178 "help_macros_43": "上下文模板聊天开始行",
2179 "help_macros_44": "主系统提示(如果选择,则覆盖字符提示,或 instructSystemPrompt)",
2180 "help_macros_45": "指示系统提示",
2181 "help_macros_46": "指示系统提示前缀序列",
2182 "help_macros_47": "指示系统提示后缀序列",
2183 "help_macros_48": "指示用户前缀序列",
2184 "help_macros_49": "指示用户后缀序列",
2185 "help_macros_50": "指导助理前缀序列",
2186 "help_macros_51": "指导助理后缀序列",
2187 "help_macros_52": "指导助理第一个输出序列",
2188 "help_macros_53": "指导助手最后输出序列",
2189 "help_macros_54": "指示系统消息前缀序列",
2190 "help_macros_55": "指示系统消息后缀序列",
2191 "help_macros_56": "指示系统指令前缀",
2192 "help_macros_57": "指示第一个用户消息填充器",
2193 "help_macros_58": "指示停止顺序",
2194 "help_macros_first_user": "指示用户第一个输入序列",
2195 "help_macros_last_user": "指示用户最后输入序列",
2196 "Chat variables Macros:": "聊天变量宏:",
2197 "Local variables = unique to the current chat": "局部变量 = 当前聊天所独有",
2198 "Global variables = works in any chat for any character": "全局变量 = 适用于任何角色的任何聊天",
2199 "Scoped variables = works in STscript": "范围变量 = 在 STscript 中有效",
2200 "help_macros_59": "替换为局部变量“name”的值",
2201 "help_macros_60": "替换为空字符串,将局部变量“name”设置为“value”",
2202 "help_macros_61": "替换为空字符串,将“increment”的数值添加到局部变量“name”",
2203 "help_macros_62": "替换为变量“name”的值增加 1 的结果",
2204 "help_macros_63": "替换为变量“name”的值减 1 的结果",
2205 "help_macros_64": "替换为全局变量“name”的值",
2206 "help_macros_65": "替换为空字符串,将全局变量“name”设置为“value”",
2207 "help_macros_66": "替换为空字符串,将“increment”的数值添加到全局变量“name”",
2208 "help_macros_67": "替换为全局变量“name”的值增加 1 的结果",
2209 "help_macros_68": "替换为全局变量“name”的值减 1 的结果",
2210 "help_macros_69": "替换为范围变量“name”的值",
2211 "help_macros_70": "用范围变量“name”的索引处的项目值(对于数组/列表或对象/字典)替换",
2212 "Choose what to export": "选择您想要导出什么:",2188 "Choose what to export": "选择您想要导出什么:",
2213 "Choose what to import": "选择您想要导入什么:",2189 "Choose what to import": "选择您想要导入什么:",
2214 "If necessary, you can later restore this chat file from the /backups folder": "若需要,您可稍后在 /backups 文件夹中恢复此聊天文件。",2190 "If necessary, you can later restore this chat file from the /backups folder": "若需要,您可稍后在 /backups 文件夹中恢复此聊天文件。",
2215 "Also delete the current chat file": "同时删除当前聊天文件",2191 "Also delete the current chat file": "同时删除当前聊天文件",
2216 "Persona Lorebook for": "Persona Lorebook for",2192 "Persona Lorebook for": "Ta 的角色世界书:",
2217 "persona_world_template_txt": "A selected World Info will be bound to this persona. When generating an AI reply,\n it will be combined with the entries from global, character and chat lorebooks.",2193 "persona_world_template_txt": "将世界书绑定到此角色。生成 AI 回复时,\n 它将与全局、角色和聊天世界书中的条目结合使用。",
2218 "Insert prompt": "插入提示词",2194 "Insert prompt": "插入提示词",
2219 "Import a prompt list": "导入提示词列表",2195 "Import a prompt list": "导入提示词列表",
2220 "Export this prompt list": "导出此提示词列表",2196 "Export this prompt list": "导出此提示词列表",
@@ -2234,13 +2210,21 @@
2234 "Don't forget to save a snapshot of your settings before proceeding.": "在继续之前,不要忘记保存您的设置快照。",2210 "Don't forget to save a snapshot of your settings before proceeding.": "在继续之前,不要忘记保存您的设置快照。",
2235 "Enter your password below to confirm:": "输入您的密码以确认:",2211 "Enter your password below to confirm:": "输入您的密码以确认:",
2236 "Reset custom sampler selection": "重置自定义采样器选择",2212 "Reset custom sampler selection": "重置自定义采样器选择",
2213 "Reset": "重置",
2214 "Prioritize showing samplers manually selected from this popup.": "优先显示从此弹出窗口手动选择的采样器。",
2215 "Toggle on to force the samplers selected in this menu to be shown when switching to the current API Type (API Connections Panel). By default, SillyTavern automatically selects samplers used or needed by the selected API Type.": "开启此选项,以在切换到当前 API 配置(API 连接面板)时强制显示现在所选择的采样器。默认情况下,SillyTavern 会自动选择所选 API 配置使用或需要的采样器。",
2237 "Here you can toggle the display of individual samplers. (WIP)": "在此可以切换单个采样器的显示。(开发中)",2216 "Here you can toggle the display of individual samplers. (WIP)": "在此可以切换单个采样器的显示。(开发中)",
2238 "Chat Scenario Override": "聊天场景覆盖",2217 "Chat Character Settings Override": "聊天角色设置覆盖",
2239 "Remove": "移除",2218 "Remove": "移除",
2240 "Unique to this chat.": "仅对此聊天生效。",2219 "Unique to this chat.": "仅对此聊天生效。",
2241 "All group members will use the following scenario text instead of what is specified in their character cards.": "All group members will use the following scenario text instead of what is specified in their character cards.",2220 "All group members will use the following values instead of what is specified in their character cards.": "所有群成员将使用以下值,而不是其角色卡中指定的值。",
2242 "The following scenario text will be used instead of the value set in the character card.": "The following scenario text will be used instead of the value set in the character card.",2221 "The following values will be used instead of the value set in the character card.": "以下值将替代角色卡中设置的值。",
2243 "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.",2222 "Checkpoints inherit the overrides from their parent, and can be changed individually after that.": "检查点继承其父级的覆盖设置,并可后续单独更改。",
2223 "Type Scenario here...": "在此输入场景...",
2224 "Type Example Messages here...": "在此输入示例消息...",
2225 "Prefer Char. Prompt": "优先角色提示词",
2226 "MUST be enabled!": "必须启用!",
2227 "Type System Prompt here...": "在此输入系统提示词...",
2244 "API:": "API:",2228 "API:": "API:",
2245 "Key:": "密钥:",2229 "Key:": "密钥:",
2246 "Add Secret": "添加密钥",2230 "Add Secret": "添加密钥",
@@ -2256,7 +2240,7 @@
2256 "Extra parameters for downloading/HuggingFace API": "下载/HuggingFace API 的额外参数。如果不确定,请将其留空。",2240 "Extra parameters for downloading/HuggingFace API": "下载/HuggingFace API 的额外参数。如果不确定,请将其留空。",
2257 "Revision": "修订",2241 "Revision": "修订",
2258 "Folder Name": "输出文件夹名称",2242 "Folder Name": "输出文件夹名称",
2259 "HF Token": "HF代币",2243 "HF Token": "HF 令牌",
2260 "Include Patterns": "包含模式",2244 "Include Patterns": "包含模式",
2261 "Glob patterns of files to include in the download.": "要包含在下载中的文件的全局模式。每个模式用换行符分隔。",2245 "Glob patterns of files to include in the download.": "要包含在下载中的文件的全局模式。每个模式用换行符分隔。",
2262 "Exclude Patterns": "排除模式",2246 "Exclude Patterns": "排除模式",
@@ -2267,14 +2251,12 @@
2267 "Save your tags to a file": "将标签保存为文件",2251 "Save your tags to a file": "将标签保存为文件",
2268 "Restore tags from a file": "从文件中恢复标签",2252 "Restore tags from a file": "从文件中恢复标签",
2269 "Create a new tag": "新建一个标签",2253 "Create a new tag": "新建一个标签",
2270 "Drag handle to reorder. Click name to rename. Click color to change display.": "拖拽左侧三条横线以排序,点击名字以重命名,点击调色盘以切换颜色。",
2271 "Click on the folder icon to use this tag as a folder.": "点击文件夹图标来将此标签作为一个文件夹。",
2272 "Use alphabetical sorting": "按字母顺序排列",
2273 "Sort mode": "排序模式",2254 "Sort mode": "排序模式",
2274 "Manual (Drag & Drop)": "手动 (拖放)",2255 "Manual (Drag & Drop)": "手动 (拖放)",
2275 "Alphabetical (A-Z)": "按字母 (A-Z)",2256 "Alphabetical (A-Z)": "按字母 (A-Z)",
2276 "Most Used (By Count)": "按使用次数",2257 "Most Used (By Count)": "按使用次数",
2277 "tags_sorting_desc": "启用后,标签在创建或重命名时会自动按字母顺序排序。\n禁用后,新标签会追加到末尾。\n\n如果通过拖动手动重新排列标签,则自动排序将被禁用。",2258 "Drag handle to reorder. Click name to rename. Click color to change display.": "拖拽左侧三条横线以排序,点击名字以重命名,点击调色盘以切换颜色。",
2259 "Click on the folder icon to use this tag as a folder.": "点击文件夹图标来将此标签作为一个文件夹。",
2278 "Are you sure you want to delete the theme?": "你确定要删除这个主题吗?",2260 "Are you sure you want to delete the theme?": "你确定要删除这个主题吗?",
2279 "Hi,": "嗨,",2261 "Hi,": "嗨,",
2280 "To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器",2262 "To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器",
@@ -2297,7 +2279,7 @@
2297 "Wipe all user data and reset your account to factory settings.": "删除所有用户数据并将您的账号重置为默认设置。",2279 "Wipe all user data and reset your account to factory settings.": "删除所有用户数据并将您的账号重置为默认设置。",
2298 "Reset Everything": "重置一切",2280 "Reset Everything": "重置一切",
2299 "This will delete all your settings and data. There will be no undo button. Make sure you have a backup before proceeding.": "这将删除您所有的设置和数据,不可撤销。请确保您已备份数据。",2281 "This will delete all your settings and data. There will be no undo button. Make sure you have a backup before proceeding.": "这将删除您所有的设置和数据,不可撤销。请确保您已备份数据。",
2300 "Account reset code has been posted to the server console.": "账户重置代码已发布到服务器控制台。",2282 "Account reset code has been posted to the server console.": "账户重置代码已发送至服务器控制台。",
2301 "Reset Code:": "重置代码:",2283 "Reset Code:": "重置代码:",
2302 "Want to update?": "获取最新版本",2284 "Want to update?": "获取最新版本",
2303 "How to start chatting?": "如何快速开始聊天?",2285 "How to start chatting?": "如何快速开始聊天?",
public/script.js+536 -472
@@ -8,6 +8,7 @@ import {
8 Popper,8 Popper,
9 initLibraryShims,9 initLibraryShims,
10 default as libs,10 default as libs,
11 lodash,
11} from './lib.js';12} from './lib.js';
1213
13import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods } from './scripts/RossAscends-mods.js';14import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods } from './scripts/RossAscends-mods.js';
@@ -188,6 +189,7 @@ import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids,
188189
189import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js';190import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js';
190import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, initSlashCommandAutoComplete, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';191import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, initSlashCommandAutoComplete, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
192import { initMacroAutoComplete } from './scripts/autocomplete/MacroAutoComplete.js';
191import {193import {
192 tag_map,194 tag_map,
193 tags,195 tags,
@@ -271,7 +273,7 @@ import { extractReasoningFromData, extractReasoningSignatureFromData, initReason
271import { accountStorage } from './scripts/util/AccountStorage.js';273import { accountStorage } from './scripts/util/AccountStorage.js';
272import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';274import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';
273import { initDataMaid } from './scripts/data-maid.js';275import { initDataMaid } from './scripts/data-maid.js';
274import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts } from './scripts/itemized-prompts.js';276import { clearItemizedPrompts, deleteItemizedPromptForMessage, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts, swapItemizedPrompts } from './scripts/itemized-prompts.js';
275import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js';277import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js';
276import { event_types, eventSource } from './scripts/events.js';278import { event_types, eventSource } from './scripts/events.js';
277import { initAccessibility } from './scripts/a11y.js';279import { initAccessibility } from './scripts/a11y.js';
@@ -282,6 +284,7 @@ import { AudioPlayer } from './scripts/audio-player.js';
282import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js';284import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js';
283import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';285import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';
284import { addChatBackupsBrowser } from './scripts/chat-backups.js';286import { addChatBackupsBrowser } from './scripts/chat-backups.js';
287import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js';
285288
286// API OBJECT FOR EXTERNAL WIRING289// API OBJECT FOR EXTERNAL WIRING
287globalThis.SillyTavern = {290globalThis.SillyTavern = {
@@ -386,7 +389,7 @@ let chatSaveTimeout;
386let importFlashTimeout;389let importFlashTimeout;
387export let isChatSaving = false;390export let isChatSaving = false;
388let firstRun = false;391let firstRun = false;
389let settingsReady = false;392export let settingsReady = false;
390let currentVersion = '0.0.0';393let currentVersion = '0.0.0';
391export let displayVersion = 'SillyTavern';394export let displayVersion = 'SillyTavern';
392395
@@ -701,7 +704,6 @@ async function firstLoadInit() {
701 initDynamicStyles();704 initDynamicStyles();
702 initTags();705 initTags();
703 initBookmarks();706 initBookmarks();
704 initMacros();
705 await getUserAvatars(true, user_avatar);707 await getUserAvatars(true, user_avatar);
706 await getCharacters();708 await getCharacters();
707 await getBackgrounds();709 await getBackgrounds();
@@ -710,6 +712,7 @@ async function firstLoadInit() {
710 initAuthorsNote();712 initAuthorsNote();
711 await initPersonas();713 await initPersonas();
712 await initSlashCommandAutoComplete();714 await initSlashCommandAutoComplete();
715 initMacroAutoComplete();
713 initWorldInfo();716 initWorldInfo();
714 initHorde();717 initHorde();
715 initRossMods();718 initRossMods();
@@ -729,6 +732,7 @@ async function firstLoadInit() {
729 initAccessibility();732 initAccessibility();
730 addDebugFunctions();733 addDebugFunctions();
731 doDailyExtensionUpdatesCheck();734 doDailyExtensionUpdatesCheck();
735 await eventSource.emit(event_types.APP_INITIALIZED);
732 await hideLoader();736 await hideLoader();
733 await fixViewport();737 await fixViewport();
734 await eventSource.emit(event_types.APP_READY);738 await eventSource.emit(event_types.APP_READY);
@@ -833,13 +837,14 @@ export async function selectCharacterById(id, { switchMenu = true } = {}) {
833 if (selected_group || String(this_chid) !== String(id)) {837 if (selected_group || String(this_chid) !== String(id)) {
834 //if clicked on a different character from what was currently selected838 //if clicked on a different character from what was currently selected
835 if (!is_send_press) {839 if (!is_send_press) {
836 await clearChat();840 setCharacterId(undefined);
837 cancelTtsPlay();841 setCharacterName('');
838 resetSelectedGroup();842 resetSelectedGroup();
843 await clearChat({ clearData: true });
844 cancelTtsPlay();
839 this_edit_mes_id = undefined;845 this_edit_mes_id = undefined;
840 selected_button = 'character_edit';846 selected_button = 'character_edit';
841 setCharacterId(id);847 setCharacterId(id);
842 chat.length = 0;
843 chat_metadata = {};848 chat_metadata = {};
844 await getChat();849 await getChat();
845 }850 }
@@ -952,7 +957,8 @@ export async function printCharacters(fullRefresh = false) {
952957
953 // We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date958 // We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date
954 printTagFilters(tag_filter_type.character);959 printTagFilters(tag_filter_type.character);
955 printTagFilters(tag_filter_type.group_member);960 printTagFilters(tag_filter_type.group_members_list);
961 printTagFilters(tag_filter_type.group_candidates_list);
956962
957 // We are also always reprinting the lists on character/group edit window, as these ones doesn't get updated otherwise963 // We are also always reprinting the lists on character/group edit window, as these ones doesn't get updated otherwise
958 applyTagsOnCharacterSelect();964 applyTagsOnCharacterSelect();
@@ -1175,8 +1181,8 @@ export async function getOneCharacter(avatarUrl) {
11751181
1176 if (response.ok) {1182 if (response.ok) {
1177 const getData = await response.json();1183 const getData = await response.json();
1178 getData['name'] = DOMPurify.sanitize(getData['name']);1184 getData.name = DOMPurify.sanitize(getData.name);
1179 getData['chat'] = String(getData['chat']);1185 getData.chat = String(getData.chat);
11801186
1181 const indexOf = characters.findIndex(x => x.avatar === avatarUrl);1187 const indexOf = characters.findIndex(x => x.avatar === avatarUrl);
11821188
@@ -1188,7 +1194,7 @@ export async function getOneCharacter(avatarUrl) {
1188 }1194 }
1189}1195}
11901196
1191function getCharacterSource(chId = this_chid) {1197export function getCharacterSource(chId = this_chid) {
1192 const character = characters[chId];1198 const character = characters[chId];
11931199
1194 if (!character) {1200 if (!character) {
@@ -1247,14 +1253,14 @@ export async function getCharacters() {
1247 const getData = await response.json();1253 const getData = await response.json();
1248 for (let i = 0; i < getData.length; i++) {1254 for (let i = 0; i < getData.length; i++) {
1249 characters[i] = getData[i];1255 characters[i] = getData[i];
1250 characters[i]['name'] = DOMPurify.sanitize(characters[i]['name']);1256 characters[i].name = DOMPurify.sanitize(characters[i].name);
12511257
1252 // For dropped-in cards1258 // For dropped-in cards
1253 if (!characters[i]['chat']) {1259 if (!characters[i].chat) {
1254 characters[i]['chat'] = `${characters[i]['name']} - ${humanizedDateTime()}`;1260 characters[i].chat = `${characters[i].name} - ${humanizedDateTime()}`;
1255 }1261 }
12561262
1257 characters[i]['chat'] = String(characters[i]['chat']);1263 characters[i].chat = String(characters[i].chat);
1258 }1264 }
12591265
1260 if (previousAvatar) {1266 if (previousAvatar) {
@@ -1346,8 +1352,7 @@ export async function deleteCharacterChatByName(characterId, fileName) {
1346}1352}
13471353
1348export async function replaceCurrentChat() {1354export async function replaceCurrentChat() {
1349 await clearChat();1355 await clearChat({ clearData: true });
1350 chat.length = 0;
13511356
1352 const chatsResponse = await fetch('/api/characters/chats', {1357 const chatsResponse = await fetch('/api/characters/chats', {
1353 method: 'POST',1358 method: 'POST',
@@ -1390,18 +1395,26 @@ export async function showMoreMessages(messagesToLoad = null) {
13901395
1391 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);1396 console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
1392 const prevHeight = chatElement.prop('scrollHeight');1397 const prevHeight = chatElement.prop('scrollHeight');
1393 const isButtonInView = isElementInViewport($('#show_more_messages')[0]);1398 const showMoreButton = $('#show_more_messages');
13941399 const isButtonInView = isElementInViewport(showMoreButton[0]);
1395 while (messageId > 0 && count > 0) {1400
1396 let newMessageId = messageId - 1;1401 const firstId = clamp(messageId - count, 0, Infinity);
1397 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId, showSwipes: false });1402 const messageElements = [];
1398 count--;1403 chat.slice(firstId, messageId).forEach((message, id) => {
1399 messageId--;1404 messageElements.push(updateMessageElement(message, { messageId: firstId + id }));
1405 });
1406 // This could be faster: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement
1407 // Fallback to chatElement if the button isn't where it's expected to be.
1408 if (showMoreButton[0]) {
1409 showMoreButton.after(messageElements);
1410 } else {
1411 chatElement.prepend(messageElements);
1400 }1412 }
1413
1401 refreshSwipeButtons();1414 refreshSwipeButtons();
14021415
1403 if (messageId == 0) {1416 if (firstId === 0) {
1404 $('#show_more_messages').remove();1417 showMoreButton.remove();
1405 }1418 }
14061419
1407 if (isButtonInView) {1420 if (isButtonInView) {
@@ -1422,19 +1435,54 @@ export async function printMessages() {
1422 chatElement.append('<div id="show_more_messages">Show more messages</div>');1435 chatElement.append('<div id="show_more_messages">Show more messages</div>');
1423 }1436 }
14241437
1425 for (let i = startIndex; i < chat.length; i++) {1438 await redisplayChat({ startIndex, fade: false });
1426 const item = chat[i];
1427 addOneMessage(item, { scroll: false, forceId: i, showSwipes: false });
1428 }
14291439
1430 chatElement.find('.mes').removeClass('last_mes');
1431 chatElement.find('.mes').last().addClass('last_mes');
1432 refreshSwipeButtons(false, false);
1433 applyStylePins();
1434 scrollChatToBottom({ waitForFrame: true });1440 scrollChatToBottom({ waitForFrame: true });
1435 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());1441 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());
1436}1442}
14371443
1444/**
1445 * Visually updates all chat messages including and after index by removing them, then adding them.
1446 * @param {object} [options] Options
1447 * @param {ChatMessage[]} [options.targetChat=chat] All messages in chat before startIndex will remain unchanged.
1448 * @param {Number} [options.startIndex=0] Everything including and after startIndex will be replaced.
1449 * @param {Boolean} [options.fade=true] When false, the swipe chevrons will not fade in.
1450 */
1451export async function redisplayChat({ targetChat = chat, startIndex = 0, fade = true } = {}) {
1452 const messageElements = chatElement.find('.mes');
1453 messageElements.removeClass('last_mes');
1454
1455 //Remove messages after index.
1456 messageElements.filter(`.mes[mesid="${startIndex}"]`).nextAll('.mes').addBack().remove();
1457
1458 const t1 = performance.now();
1459
1460 const messages = targetChat.slice(startIndex);
1461
1462 if (messages.length > 0) {
1463 const newMessageElements = messages.map((message, offset) => {
1464 const i = startIndex + offset;
1465 const messageElement = updateMessageElement(message, { messageId: i });
1466
1467 return messageElement[0];
1468 });
1469
1470 //The last_mes has been removed, add it to the new last message.
1471 newMessageElements.at(-1).classList.add('last_mes');
1472
1473 //Append to chat in one DOM update.
1474 chatElement.append(newMessageElements);
1475
1476 applyCharacterTagsToMessageDivs({ mesIds: lodash.range(startIndex, targetChat.length, 1) });
1477 }
1478
1479 refreshSwipeButtons(false, fade);
1480 applyStylePins();
1481 updateEditArrowClasses();
1482
1483 console.info(`Rendered ${targetChat.length - startIndex} messages in ${((performance.now() - t1) / 1000).toFixed(3)} seconds.`);
1484}
1485
1438export function scrollOnMediaLoad() {1486export function scrollOnMediaLoad() {
1439 const started = Date.now();1487 const started = Date.now();
1440 const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray();1488 const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray();
@@ -1482,7 +1530,12 @@ export function cancelDebouncedChatSave() {
1482 }1530 }
1483}1531}
14841532
1485export async function clearChat() {1533/**
1534 * Visually removes all chat message elements.
1535 * @param {object} [options] Options
1536 * @param {boolean} [options.clearData=false] Optionally clear the chat array's contents.
1537 */
1538export async function clearChat({ clearData = false } = {}) {
1486 cancelDebouncedChatSave();1539 cancelDebouncedChatSave();
1487 cancelDebouncedMetadataSave();1540 cancelDebouncedMetadataSave();
1488 closeMessageEditor();1541 closeMessageEditor();
@@ -1499,9 +1552,12 @@ export async function clearChat() {
14991552
1500 await saveItemizedPrompts(getCurrentChatId());1553 await saveItemizedPrompts(getCurrentChatId());
1501 itemizedPrompts.length = 0;1554 itemizedPrompts.length = 0;
1555
1556 if (clearData) chat.length = 0;
1502}1557}
15031558
1504export async function deleteLastMessage() {1559export async function deleteLastMessage() {
1560 deleteItemizedPromptForMessage(chat.length - 1);
1505 chat.length = chat.length - 1;1561 chat.length = chat.length - 1;
1506 chatElement.children('.mes').last().remove();1562 chatElement.children('.mes').last().remove();
1507 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);1563 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
@@ -1554,9 +1610,10 @@ export async function deleteMessage(id, swipeDeletionIndex = undefined, askConfi
1554 chat.splice(id, 1);1610 chat.splice(id, 1);
1555 messageElement.remove();1611 messageElement.remove();
15561612
1557 chat_metadata['tainted'] = true;1613 chat_metadata.tainted = true;
15581614
1559 const startIndex = [0, minId].includes(id) ? id : null;1615 const startIndex = [0, minId].includes(id) ? id : null;
1616 deleteItemizedPromptForMessage(id);
1560 updateViewMessageIds(startIndex);1617 updateViewMessageIds(startIndex);
1561 saveChatDebounced();1618 saveChatDebounced();
15621619
@@ -1569,10 +1626,17 @@ export async function deleteMessage(id, swipeDeletionIndex = undefined, askConfi
1569 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);1626 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
1570}1627}
15711628
1572export async function reloadCurrentChat() {1629export const reloadChatMutex = new SimpleMutex(reloadCurrentChatUnsafe);
1630export const reloadCurrentChat = reloadChatMutex.update.bind(reloadChatMutex);
1631
1632/**
1633 * Reloads the current chat unsafely, without mutex protection.
1634 * Use `reloadCurrentChat` instead to ensure thread safety.
1635 * @returns {Promise<void>} A promise that resolves when the chat is reloaded.
1636 */
1637export async function reloadCurrentChatUnsafe() {
1573 preserveNeutralChat();1638 preserveNeutralChat();
1574 await clearChat();1639 await clearChat({ clearData: true });
1575 chat.length = 0;
15761640
1577 if (selected_group) {1641 if (selected_group) {
1578 await getGroupChat(selected_group, true);1642 await getGroupChat(selected_group, true);
@@ -1610,13 +1674,14 @@ export async function sendTextareaMessage() {
1610 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last1674 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
1611 // message was sent from a character (not the user or the system).1675 // message was sent from a character (not the user or the system).
1612 const textareaText = String($('#send_textarea').val());1676 const textareaText = String($('#send_textarea').val());
1677 const lastMessage = chat[chat.length - 1];
1613 if (power_user.continue_on_send &&1678 if (power_user.continue_on_send &&
1614 !hasPendingFileAttachment() &&1679 !hasPendingFileAttachment() &&
1615 !textareaText &&1680 !textareaText &&
1616 !selected_group &&1681 !selected_group &&
1617 chat.length &&1682 chat.length &&
1618 !chat[chat.length - 1]['is_user'] &&1683 !lastMessage.is_user &&
1619 !chat[chat.length - 1]['is_system']1684 !lastMessage.is_system
1620 ) {1685 ) {
1621 generateType = 'continue';1686 generateType = 'continue';
1622 }1687 }
@@ -1637,7 +1702,7 @@ export async function sendTextareaMessage() {
1637 * @param {boolean} isSystem If the message was sent by the system1702 * @param {boolean} isSystem If the message was sent by the system
1638 * @param {boolean} isUser If the message was sent by the user1703 * @param {boolean} isUser If the message was sent by the user
1639 * @param {number} messageId Message index in chat array1704 * @param {number} messageId Message index in chat array
1640 * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides1705 * @param {Partial<DOMPurify.Config>} [sanitizerOverrides] DOMPurify sanitizer option overrides
1641 * @param {boolean} [isReasoning] If the message is reasoning output1706 * @param {boolean} [isReasoning] If the message is reasoning output
1642 * @returns {string} HTML string1707 * @returns {string} HTML string
1643 */1708 */
@@ -1786,7 +1851,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
1786 mes = mes.replace(new RegExp(`(^|\n)${escapeRegex(ch_name)}:`, 'g'), '$1');1851 mes = mes.replace(new RegExp(`(^|\n)${escapeRegex(ch_name)}:`, 'g'), '$1');
1787 }1852 }
17881853
1789 /** @type {import('dompurify').Config & { RETURN_DOM_FRAGMENT: false; RETURN_DOM: false }} */1854 /** @type {DOMPurify.Config} */
1790 const config = {1855 const config = {
1791 RETURN_DOM: false,1856 RETURN_DOM: false,
1792 RETURN_DOM_FRAGMENT: false,1857 RETURN_DOM_FRAGMENT: false,
@@ -1810,9 +1875,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
1810 * the value in `extra.api`.1875 * the value in `extra.api`.
1811 *1876 *
1812 * @param {JQuery<HTMLElement>} mes - The message element containing the timestamp where the icon should be inserted or replaced.1877 * @param {JQuery<HTMLElement>} mes - The message element containing the timestamp where the icon should be inserted or replaced.
1813 * @param {Object} extra - Contains the API and model details.1878 * @param {ChatMessageExtra} extra - Contains the API and model details.
1814 * @param {string} extra.api - The name of the API, used to determine which SVG to fetch.
1815 * @param {string} extra.model - The model name, used to check for the substring "claude".
1816 */1879 */
1817function insertSVGIcon(mes, extra) {1880function insertSVGIcon(mes, extra) {
1818 // Determine the SVG filename1881 // Determine the SVG filename
@@ -1860,56 +1923,6 @@ function insertSVGIcon(mes, extra) {
1860 createModelImage('thinking-icon', '.mes_reasoning_header_title', true);1923 createModelImage('thinking-icon', '.mes_reasoning_header_title', true);
1861}1924}
18621925
1863
1864function getMessageFromTemplate({
1865 mesId,
1866 swipeId,
1867 characterName,
1868 isUser,
1869 avatarImg,
1870 bias,
1871 isSystem,
1872 title,
1873 timerValue,
1874 timerTitle,
1875 bookmarkLink,
1876 forceAvatar,
1877 timestamp,
1878 tokenCount,
1879 extra,
1880 type,
1881}) {
1882 const mes = messageTemplate.clone();
1883 mes.attr({
1884 'mesid': mesId,
1885 'swipeid': swipeId,
1886 'ch_name': characterName,
1887 'is_user': isUser,
1888 'is_system': !!isSystem,
1889 'bookmark_link': bookmarkLink,
1890 'force_avatar': !!forceAvatar,
1891 'timestamp': timestamp,
1892 ...(type ? { type } : {}),
1893 });
1894 mes.find('.avatar img').attr('src', avatarImg);
1895 mes.find('.ch_name .name_text').text(characterName);
1896 mes.find('.mes_bias').html(bias);
1897 mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
1898 mes.find('.mesIDDisplay').text(`#${mesId}`);
1899 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
1900 title && mes.attr('title', title);
1901 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
1902 bookmarkLink && updateBookmarkDisplay(mes);
1903
1904 updateReasoningUI(mes);
1905
1906 if (power_user.timestamp_model_icon && extra?.api) {
1907 insertSVGIcon(mes, extra);
1908 }
1909
1910 return mes;
1911}
1912
1913/**1926/**
1914 * Re-renders a message block with updated content.1927 * Re-renders a message block with updated content.
1915 * @param {number} messageId Message ID1928 * @param {number} messageId Message ID
@@ -2382,183 +2395,214 @@ export function addCopyToCodeBlocks(messageElement) {
2382 }2395 }
2383}2396}
23842397
2398/**
2399 * Shows or hides the Prompt display button
2400 * @param {ChatMessage} message Message object
2401 * @param {object} options Options
2402 * @param {number} [options.messageId] Message ID
2403 * @param {JQuery<HTMLElement>} [options.messageElement] Message element
2404 * @return {void}
2405 */
2406function updateMessageItemizedPromptButton(message, { messageId = chat.indexOf(message), messageElement = chatElement.find(`.mes[mesid="${messageId}"]`) }) {
2407 //if we have itemized messages, and the array isn't null..
2408 if (!message.is_user && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) {
2409 const itemizedPrompt = itemizedPrompts.find(x => Number(x.mesId) === Number(messageId));
2410 if (itemizedPrompt) {
2411 messageElement.find('.mes_prompt').show();
2412 }
2413 }
2414}
2415
2416/**
2417 * Gets messageFormatting for a ChatMessage object.
2418 * @param {ChatMessage} message
2419 * @param {object} options Options
2420 * @param {number} [options.messageId] Message ID
2421 * @returns {string} Formatted message HTML
2422 */
2423function getMessageTextHTML(message, { messageId = chat.indexOf(message) }) {
2424 // if mes.extra.uses_system_ui is true, set an override on the sanitizer options
2425 /** @type {Partial<DOMPurify.Config>} */
2426 const sanitizerOverrides = message.extra?.uses_system_ui ? { MESSAGE_ALLOW_SYSTEM_UI: true } : {};
2427
2428 return messageFormatting(
2429 message.extra?.display_text || message.mes,
2430 message.name,
2431 message.is_system,
2432 message.is_user,
2433 messageId,
2434 sanitizerOverrides,
2435 false,
2436 );
2437}
23852438
2386/**2439/**
2387 * Adds a single message to the chat.2440 * Adds a single message to the chat.
2388 * @param {ChatMessage} mes Message object2441 * @param {ChatMessage} mes Message object
2389 * @param {object} [options] Options2442 * @param {object} [options] Options
2390 * @param {string} [options.type='normal'] Message type2443 * @param {string} [options.type=undefined|'swipe'] Deprecated. Use updateMessageElement instead.
2391 * @param {number} [options.insertAfter=null] Message ID to insert the new message after2444 * @param {number} [options.insertAfter=null] Message ID to insert the new message after
2392 * @param {boolean} [options.scroll=true] Whether to scroll to the new message2445 * @param {boolean} [options.scroll=true] Whether to scroll to the new message
2393 * @param {number} [options.insertBefore=null] Message ID to insert the new message before2446 * @param {number} [options.insertBefore=null] Message ID to insert the new message before
2394 * @param {number} [options.forceId=null] Force the message ID2447 * @param {number} [options.forceId=null] Force the message ID
2395 * @param {boolean} [options.showSwipes=true] Whether to refresh the swipe buttons.2448 * @param {boolean} [options.showSwipes=true] Whether to refresh the swipe buttons.
2396 * @returns {void}2449 * @returns {JQuery<HTMLElement>} The newly added message element
2397 */2450 */
2398export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {2451export function addOneMessage(mes, { type = undefined, insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {
2399 let messageText = mes['mes'];2452 // Callers push the new message to chat before calling addOneMessage
2400 const momentDate = timestampToMoment(mes.send_date);2453 const messageId = (() => {
2401 const timestamp = momentDate.isValid() ? momentDate.format('LL LT') : '';2454 if (typeof forceId === 'number') {
24022455 return forceId;
2403 if (mes?.extra?.display_text) {2456 }
2404 messageText = mes.extra.display_text;2457 if (typeof insertBefore === 'number') {
2458 return insertBefore - 1;
2405 }2459 }
2460 if (typeof insertAfter === 'number') {
2461 return insertAfter + 1;
2462 }
2463 const index = chat.indexOf(mes);
2464 if (index !== -1) {
2465 return index;
2466 }
2467 return chat.length - 1;
2468 })();
2469
2470 let messageElement;
24062471
2472 if (type === 'swipe') {
2407 // Forbidden black magic2473 // Forbidden black magic
2408 // This allows to use "continue" on user messages2474 // This allows to use "continue" on user messages
2409 if (type === 'swipe' && mes.swipe_id === undefined) {2475 mes.swipe_id ??= 0;
2410 mes.swipe_id = 0;2476 mes.swipes ??= [mes.mes];
2411 mes.swipes = [mes.mes];2477 //This keeps listeners intact.
2478 messageElement = chatElement.find(`[mesid="${messageId}"]`);
2479 updateMessageElement(mes, { messageId, messageElement, adjustMediaScroll: scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE });
2480 } else {
2481 messageElement = updateMessageElement(mes, { messageId, adjustMediaScroll: scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE });
2482 if (typeof insertAfter === 'number' && insertAfter >= 0) {
2483 const target = chatElement.find(`.mes[mesid="${insertAfter}"]`);
2484 $(messageElement).insertAfter(target);
2485 } else if (typeof insertBefore === 'number' && insertBefore >= 0) {
2486 const target = chatElement.find(`.mes[mesid="${insertBefore}"]`);
2487 $(messageElement).insertBefore(target);
2488 } else {
2489 chatElement.append(messageElement);
2490 }
2491 }
2492
2493
2494 //last_mes should always be updated.
2495 chatElement.find('.mes').removeClass('last_mes');
2496 chatElement.find('.mes').last().addClass('last_mes');
2497
2498 if (showSwipes) refreshSwipeButtons();
2499 // Don't scroll if not inserting last
2500 if (!insertAfter && !insertBefore && scroll) {
2501 scrollChatToBottom({ waitForFrame: true });
2412 }2502 }
24132503
2504 applyCharacterTagsToMessageDivs({ mesIds: messageId });
2505 updateEditArrowClasses();
2506 return messageElement;
2507}
2508
2509/**
2510 * Creates the element of a single message as if it were the last message or at forceMesId
2511 * @param {ChatMessage} mes Message object
2512 * @param {object} [options] Options
2513 * @param {number} [options.messageId=chat.length - 1] Force the message ID
2514 * @param {JQuery<HTMLElement>} [options.messageElement=messageTemplate.clone()] This message element will be updated with the ChatMessage object.
2515 * @param {SCROLL_BEHAVIOR} [options.adjustMediaScroll=SCROLL_BEHAVIOR.NONE] Scroll behavior option passed to appendMediaToMessage.
2516 * @returns {JQuery<HTMLElement>} Rendered HTMLElement.
2517 */
2518export function updateMessageElement(mes, { messageId = chat.length - 1, messageElement = messageTemplate.clone(), adjustMediaScroll = SCROLL_BEHAVIOR.NONE } = {}) {
2519
2414 let avatarImg = getThumbnailUrl('persona', user_avatar);2520 let avatarImg = getThumbnailUrl('persona', user_avatar);
2415 const isSystem = mes.is_system;
2416 const title = mes.title;
24172521
2418 //for non-user mesages2522 //for non-user messages
2419 if (!mes['is_user']) {2523 if (!mes.is_user) {
2420 if (mes.force_avatar) {2524 if (mes.force_avatar) {
2421 avatarImg = mes.force_avatar;2525 avatarImg = mes.force_avatar;
2422 } else if (this_chid === undefined) {2526 } else if (this_chid === undefined) {
2423 avatarImg = system_avatar;2527 avatarImg = system_avatar;
2424 } else {2528 } else if (characters[this_chid] && characters[this_chid].avatar !== 'none') {
2425 if (characters[this_chid].avatar !== 'none') {
2426 avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);2529 avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
2427 } else {2530 } else {
2428 avatarImg = default_avatar;2531 avatarImg = default_avatar;
2429 }2532 }
2430 }
2431 //old processing:2533 //old processing:
2432 //if messge is from sytem, use the name provided in the message JSONL to proceed,2534 //if message is from system, use the name provided in the message JSONL to proceed,
2433 //if not system message, use name2 (char's name) to proceed2535 //if not system message, use name2 (char's name) to proceed
2434 //characterName = mes.is_system || mes.force_avatar ? mes.name : name2;2536 //characterName = mes.is_system || mes.force_avatar ? mes.name : name2;
2435 } else if (mes['is_user'] && mes['force_avatar']) {2537 } else if (mes.is_user && mes.force_avatar) {
2436 // Special case for persona images.2538 // Special case for persona images.
2437 avatarImg = mes['force_avatar'];2539 avatarImg = mes.force_avatar;
2438 }2540 }
2541 const momentDate = timestampToMoment(mes.send_date);
2542 const timestamp = momentDate.isValid() ? momentDate.format('LL LT') : '';
2543 const messageHTML = getMessageTextHTML(mes, { messageId });
2544 const bookmarkLink = mes?.extra?.bookmark_link;
2545 const tokenCount = mes.extra?.token_count;
2546 const { timerValue, timerTitle } = formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration, mes.extra?.time_to_first_token);
2547
2548 messageElement.attr({
2549 'mesid': messageId,
2550 'swipeid': mes.swipe_id ?? 0,
2551 'ch_name': mes.name,
2552 'is_user': mes.is_user,
2553 'is_system': !!mes.is_system,
2554 'bookmark_link': bookmarkLink,
2555 'force_avatar': !!mes.force_avatar,
2556 'timestamp': timestamp,
2557 // ...(type ?? { type }),
2558 'type': mes.extra?.type ?? '',
2559 });
24392560
2440 // if mes.extra.uses_system_ui is true, set an override on the sanitizer options2561 messageElement.find('.avatar img').attr('src', avatarImg);
2441 const sanitizerOverrides = mes.extra?.uses_system_ui ? { MESSAGE_ALLOW_SYSTEM_UI: true } : {};2562 messageElement.find('.ch_name .name_text').text(mes.name);
24422563 messageElement.find('.timestamp').text(timestamp).attr('title', `${mes.extra?.api ? mes.extra.api + ' - ' : ''}${mes.extra?.model ?? ''}`);
2443 messageText = messageFormatting(2564 messageElement.find('.mesIDDisplay').text(`#${messageId}`);
2444 messageText,2565 tokenCount && messageElement.find('.tokenCounterDisplay').text(`${tokenCount}t`);
2445 mes.name,2566 mes.title && messageElement.attr('title', mes.title);
2446 isSystem,2567 timerValue && messageElement.find('.mes_timer').attr('title', timerTitle).text(timerValue);
2447 mes.is_user,2568 bookmarkLink && updateBookmarkDisplay(messageElement);
2448 chat.indexOf(mes),
2449 sanitizerOverrides,
2450 false,
2451 );
2452 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2453 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
2454
2455 let params = {
2456 mesId: forceId ?? chat.length - 1,
2457 swipeId: mes.swipe_id ?? 0,
2458 characterName: mes.name,
2459 isUser: mes.is_user,
2460 avatarImg: avatarImg,
2461 bias: bias,
2462 isSystem: isSystem,
2463 title: title,
2464 bookmarkLink: bookmarkLink,
2465 forceAvatar: mes.force_avatar,
2466 timestamp: timestamp,
2467 extra: mes.extra,
2468 tokenCount: mes.extra?.token_count ?? 0,
2469 type: mes.extra?.type ?? '',
2470 ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration, mes.extra?.time_to_first_token),
2471 };
2472
2473 const renderedMessage = getMessageFromTemplate(params);
24742569
2475 if (type !== 'swipe') {2570 if (mes.extra?.bias !== '') {
2476 if (!insertAfter && !insertBefore) {2571 const bias = messageFormatting(mes.extra?.bias, '', false, false, -1, {}, false);
2477 chatElement.append(renderedMessage);2572 messageElement.find('.mes_bias').html(bias);
2478 }
2479 else if (insertAfter) {
2480 const target = chatElement.find(`.mes[mesid="${insertAfter}"]`);
2481 $(renderedMessage).insertAfter(target);
2482 } else {
2483 const target = chatElement.find(`.mes[mesid="${insertBefore}"]`);
2484 $(renderedMessage).insertBefore(target);
2485 }
2486 }2573 }
24872574
2488 // Callers push the new message to chat before calling addOneMessage2575 updateReasoningUI(messageElement);
2489 const newMessageId = typeof forceId == 'number' ? forceId : chat.length - 1;
24902576
2491 const newMessage = chatElement.find(`[mesid="${newMessageId}"]`);2577 if (power_user.timestamp_model_icon && mes.extra?.api) {
2492 const isSmallSys = mes?.extra?.isSmallSys;2578 insertSVGIcon(messageElement, mes.extra);
2579 }
24932580
2494 if (isSmallSys === true) {2581 if (mes?.extra?.isSmallSys === true) {
2495 newMessage.addClass('smallSysMes');2582 messageElement.addClass('smallSysMes');
2496 }2583 }
24972584
2498 if (Array.isArray(mes?.extra?.tool_invocations)) {2585 if (Array.isArray(mes?.extra?.tool_invocations)) {
2499 newMessage.addClass('toolCall');2586 messageElement.addClass('toolCall');
2500 }2587 }
25012588
2502 //shows or hides the Prompt display button2589 updateMessageItemizedPromptButton(mes, { messageId, messageElement });
2503 let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
2504
2505 //if we have itemized messages, and the array isn't null..
2506 if (params.isUser === false && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) {
2507 const itemizedPrompt = itemizedPrompts.find(x => Number(x.mesId) === Number(mesIdToFind));
2508 if (itemizedPrompt) {
2509 newMessage.find('.mes_prompt').show();
2510 }
2511 }
25122590
2513 newMessage.find('.avatar img').on('error', function () {2591 messageElement.find('.avatar img').on('error', function () {
2514 $(this).hide();2592 $(this).hide();
2515 $(this).parent().html('<div class="missing-avatar fa-solid fa-user-slash"></div>');2593 $(this).parent().html('<div class="missing-avatar fa-solid fa-user-slash"></div>');
2516 });2594 });
25172595
2518 if (type === 'swipe') {2596 appendMediaToMessage(mes, messageElement, adjustMediaScroll);
2519 const swipeMessage = chatElement.find(`[mesid="${newMessageId}"]`);2597 messageElement.find('.mes_text').html(messageHTML);
2520 swipeMessage.attr('swipeid', params.swipeId);2598 addCopyToCodeBlocks(messageElement);
2521 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2522 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
2523 updateReasoningUI(swipeMessage);
2524 appendMediaToMessage(mes, swipeMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);
2525 if (power_user.timestamp_model_icon && params.extra?.api) {
2526 insertSVGIcon(swipeMessage, params.extra);
2527 }
2528
2529 if (mes.swipe_id == mes.swipes.length - 1) {
2530 swipeMessage.find('.mes_timer').text(params.timerValue).attr('title', params.timerTitle);
2531 swipeMessage.find('.tokenCounterDisplay').text(`${params.tokenCount}t`);
2532 } else {
2533 swipeMessage.find('.mes_timer').empty();
2534 swipeMessage.find('.tokenCounterDisplay').empty();
2535 }
2536 } else {
2537 chatElement.find(`[mesid="${newMessageId}"] .mes_text`).append(messageText);
2538 appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);
2539 }
2540
2541 addCopyToCodeBlocks(newMessage);
25422599
2543 // Set the swipes counter for all non-user messages.2600 // Set the swipes counter for all non-user messages.
2544 if (!params.isUser) {2601 if (!mes.is_user) {
2545 updateSwipeCounter(newMessageId);2602 updateSwipeCounter(messageId, { message: mes, messageElement });
2546 }
2547
2548 //last_mes should always be updated.
2549 chatElement.find('.mes').removeClass('last_mes');
2550 chatElement.find('.mes').last().addClass('last_mes');
2551 if (showSwipes) {
2552 refreshSwipeButtons();
2553 }2603 }
25542604
2555 // Don't scroll if not inserting last2605 return messageElement;
2556 if (!insertAfter && !insertBefore && scroll) {
2557 scrollChatToBottom({ waitForFrame: true });
2558 }
2559
2560 applyCharacterTagsToMessageDivs({ mesIds: newMessageId });
2561 updateEditArrowClasses();
2562}2606}
25632607
2564/**2608/**
@@ -2703,6 +2747,20 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou
2703 });2747 });
2704 }2748 }
27052749
2750 // Try to roughly detect experimental macro features to show the onboarding if needed.
2751 // This does not have to be 100% accurate, only best effort what we can quickly check.
2752 // Only do this if the warning wasn't shown yet, to prevent needless regex checks.
2753 if (accountStorage.getItem('slash_command_experimental_engine_warning_shown') !== 'true') {
2754 let feature = /** @type {string|null} */ (null);
2755 if (/{{\s*if/.test(content)) feature = '{{if}} macro';
2756 else if (/{{\s*\//.test(content)) feature = 'scoped macro';
2757 else if (/{{\s*[!?~#/]/.test(content)) feature = 'macro flags';
2758 else if (/{{\s*[.$]/.test(content)) feature = 'variable shorthands';
2759 else if (/\{\{(?:(?!\}\}).)*\{\{(?=[\s\S]*?\}\}[\s\S]*?\}\})/.test(content)) feature = 'nested macro';
2760
2761 if (feature) void onboardingExperimentalMacroEngine(feature);
2762 }
2763
2706 const environment = {};2764 const environment = {};
27072765
2708 if (typeof _original === 'string') {2766 if (typeof _original === 'string') {
@@ -2816,7 +2874,7 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou
2816 * @param {string} [options.original] - The original message for {{original}} substitution.2874 * @param {string} [options.original] - The original message for {{original}} substitution.
2817 * @param {string} [options.groupOverride] - The group members list for {{group}} substitution.2875 * @param {string} [options.groupOverride] - The group members list for {{group}} substitution.
2818 * @param {boolean} [options.replaceCharacterCard=true] - Whether to replace character card macros.2876 * @param {boolean} [options.replaceCharacterCard=true] - Whether to replace character card macros.
2819 * @param {Record<string,string|MacroHandler>} [options.dynamicMacros={}] - Additional environment variables as dynamic macros for substitution. Registered as macro functions.2877 * @param {Record<string, import('./scripts/macros/engine/MacroEnv.types.js').DynamicMacroValue>} [options.dynamicMacros={}] - Additional environment variables as dynamic macros for substitution. Registered as macro functions.
2820 * @param {(x: string) => string} [options.postProcessFn=(x) => x] - Post-processing function for each substituted macro.2878 * @param {(x: string) => string} [options.postProcessFn=(x) => x] - Post-processing function for each substituted macro.
2821 * @returns {string} The string with substituted parameters.2879 * @returns {string} The string with substituted parameters.
2822 */2880 */
@@ -3241,7 +3299,7 @@ export function getCharacterCardFieldsLazy({ chid = undefined } = {}) {
3241 persona: () => baseChatReplace(power_user.persona_description?.trim()),3299 persona: () => baseChatReplace(power_user.persona_description?.trim()),
3242 system: () => {3300 system: () => {
3243 if (!character) return '';3301 if (!character) return '';
3244 const systemPrompt = chat_metadata['system_prompt'] || character.data?.system_prompt || '';3302 const systemPrompt = chat_metadata.system_prompt || character.data?.system_prompt || '';
3245 return power_user.prefer_character_prompt ? baseChatReplace(systemPrompt.trim()) : '';3303 return power_user.prefer_character_prompt ? baseChatReplace(systemPrompt.trim()) : '';
3246 },3304 },
3247 jailbreak: () => {3305 jailbreak: () => {
@@ -3271,13 +3329,13 @@ export function getCharacterCardFieldsLazy({ chid = undefined } = {}) {
3271 scenario: () => {3329 scenario: () => {
3272 if (groupCardsLazy) return groupCardsLazy.scenario;3330 if (groupCardsLazy) return groupCardsLazy.scenario;
3273 if (!character) return '';3331 if (!character) return '';
3274 const scenarioText = chat_metadata['scenario'] || character.scenario || '';3332 const scenarioText = chat_metadata.scenario || character.scenario || '';
3275 return baseChatReplace(scenarioText.trim());3333 return baseChatReplace(scenarioText.trim());
3276 },3334 },
3277 mesExamples: () => {3335 mesExamples: () => {
3278 if (groupCardsLazy) return groupCardsLazy.mesExamples;3336 if (groupCardsLazy) return groupCardsLazy.mesExamples;
3279 if (!character) return '';3337 if (!character) return '';
3280 const exampleDialog = chat_metadata['mes_example'] || character.mes_example || '';3338 const exampleDialog = chat_metadata.mes_example || character.mes_example || '';
3281 return baseChatReplace(exampleDialog.trim());3339 return baseChatReplace(exampleDialog.trim());
3282 },3340 },
3283 };3341 };
@@ -3492,39 +3550,39 @@ class StreamingProcessor {
3492 this.sendTextarea.value = processedText;3550 this.sendTextarea.value = processedText;
3493 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));3551 this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3494 } else {3552 } else {
3495 const mesChanged = chat[messageId]['mes'] !== processedText;3553 const mesChanged = chat[messageId].mes !== processedText;
3496 await this.#checkDomElements(messageId);3554 await this.#checkDomElements(messageId);
3497 this.#updateMessageBlockVisibility();3555 this.#updateMessageBlockVisibility();
3498 const currentTime = new Date();3556 const currentTime = new Date();
3499 chat[messageId]['mes'] = processedText;3557 chat[messageId].mes = processedText;
3500 chat[messageId]['gen_started'] = this.timeStarted;3558 chat[messageId].gen_started = this.timeStarted;
3501 chat[messageId]['gen_finished'] = currentTime;3559 chat[messageId].gen_finished = currentTime;
3502 if (!chat[messageId]['extra']) {3560 if (!chat[messageId].extra) {
3503 chat[messageId]['extra'] = {};3561 chat[messageId].extra = {};
3504 }3562 }
3505 chat[messageId]['extra']['time_to_first_token'] = this.timeToFirstToken;3563 chat[messageId].extra.time_to_first_token = this.timeToFirstToken;
35063564
3507 // Update reasoning3565 // Update reasoning
3508 await this.reasoningHandler.process(messageId, mesChanged, this.promptReasoning);3566 await this.reasoningHandler.process(messageId, mesChanged, this.promptReasoning);
3509 processedText = chat[messageId]['mes'];3567 processedText = chat[messageId].mes;
35103568
3511 // Token count update.3569 // Token count update.
3512 const tokenCountText = this.reasoningHandler.reasoning + processedText;3570 const tokenCountText = this.reasoningHandler.reasoning + processedText;
3513 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? await getTokenCountAsync(tokenCountText, 0) : 0;3571 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? await getTokenCountAsync(tokenCountText, 0) : 0;
3514 if (currentTokenCount) {3572 if (currentTokenCount) {
3515 chat[messageId]['extra']['token_count'] = currentTokenCount;3573 chat[messageId].extra.token_count = currentTokenCount;
3516 if (this.messageTokenCounterDom instanceof HTMLElement) {3574 if (this.messageTokenCounterDom instanceof HTMLElement) {
3517 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;3575 this.messageTokenCounterDom.textContent = `${currentTokenCount}t`;
3518 }3576 }
3519 }3577 }
35203578
3521 if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId]['swipes'])) {3579 if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId].swipes)) {
3522 chat[messageId]['swipes'][chat[messageId]['swipe_id']] = processedText;3580 chat[messageId].swipes[chat[messageId].swipe_id] = processedText;
3523 chat[messageId]['swipe_info'][chat[messageId]['swipe_id']] = {3581 chat[messageId].swipe_info[chat[messageId].swipe_id] = {
3524 'send_date': chat[messageId]['send_date'],3582 'send_date': chat[messageId].send_date,
3525 'gen_started': chat[messageId]['gen_started'],3583 'gen_started': chat[messageId].gen_started,
3526 'gen_finished': chat[messageId]['gen_finished'],3584 'gen_finished': chat[messageId].gen_finished,
3527 'extra': structuredClone(chat[messageId]['extra']),3585 'extra': structuredClone(chat[messageId].extra),
3528 };3586 };
3529 }3587 }
35303588
@@ -3633,13 +3691,13 @@ class StreamingProcessor {
36333691
3634 setFirstSwipe(messageId) {3692 setFirstSwipe(messageId) {
3635 if (this.type !== 'swipe' && this.type !== 'impersonate') {3693 if (this.type !== 'swipe' && this.type !== 'impersonate') {
3636 if (Array.isArray(chat[messageId]['swipes']) && chat[messageId]['swipes'].length === 1 && chat[messageId]['swipe_id'] === 0) {3694 if (Array.isArray(chat[messageId].swipes) && chat[messageId].swipes.length === 1 && chat[messageId].swipe_id === 0) {
3637 chat[messageId]['swipes'][0] = chat[messageId]['mes'];3695 chat[messageId].swipes[0] = chat[messageId].mes;
3638 chat[messageId]['swipe_info'][0] = {3696 chat[messageId].swipe_info[0] = {
3639 'send_date': chat[messageId]['send_date'],3697 'send_date': chat[messageId].send_date,
3640 'gen_started': chat[messageId]['gen_started'],3698 'gen_started': chat[messageId].gen_started,
3641 'gen_finished': chat[messageId]['gen_finished'],3699 'gen_finished': chat[messageId].gen_finished,
3642 'extra': structuredClone(chat[messageId]['extra']),3700 'extra': structuredClone(chat[messageId].extra),
3643 };3701 };
3644 }3702 }
3645 }3703 }
@@ -4082,7 +4140,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4082 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';4140 const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
4083 const isImpersonate = type == 'impersonate';4141 const isImpersonate = type == 'impersonate';
40844142
4085 if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {4143 if (!(dryRun || depth || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
4086 const interruptedByCommand = await processCommands(String($('#send_textarea').val()));4144 const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
40874145
4088 if (interruptedByCommand) {4146 if (interruptedByCommand) {
@@ -4119,7 +4177,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4119 // Hide swipes if not in a dry run.4177 // Hide swipes if not in a dry run.
4120 hideSwipeButtons();4178 hideSwipeButtons();
4121 // If generated any message, set the flag to indicate it can't be recreated again.4179 // If generated any message, set the flag to indicate it can't be recreated again.
4122 chat_metadata['tainted'] = true;4180 chat_metadata.tainted = true;
4123 }4181 }
41244182
4125 if (selected_group && !is_group_generating) {4183 if (selected_group && !is_group_generating) {
@@ -4168,17 +4226,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4168 return Promise.resolve();4226 return Promise.resolve();
4169 }4227 }
41704228
4229 const lastMessage = chat[chat.length - 1];
4230
4171 let textareaText;4231 let textareaText;
4172 if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) {4232 if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun && !depth) {
4173 is_send_press = true;4233 is_send_press = true;
4174 textareaText = String($('#send_textarea').val());4234 textareaText = String($('#send_textarea').val());
4175 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));4235 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
4176 } else {4236 } else {
4177 textareaText = '';4237 textareaText = '';
4178 if (chat.length && chat[chat.length - 1]['is_user']) {4238 if (chat.length && lastMessage.is_user) {
4179 //do nothing? why does this check exist?4239 //do nothing? why does this check exist?
4180 }4240 }
4181 else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && chat.length) {4241 else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && !depth && chat.length) {
4242 deleteItemizedPromptForMessage(chat.length - 1);
4182 chat.length = chat.length - 1;4243 chat.length = chat.length - 1;
4183 await removeLastMessage();4244 await removeLastMessage();
4184 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);4245 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
@@ -4189,13 +4250,13 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
41894250
4190 // Rewrite the generation timer to account for the time passed for all the continuations.4251 // Rewrite the generation timer to account for the time passed for all the continuations.
4191 if (isContinue && chat.length) {4252 if (isContinue && chat.length) {
4192 const prevFinished = chat[chat.length - 1]['gen_finished'];4253 const prevFinished = lastMessage.gen_finished;
4193 const prevStarted = chat[chat.length - 1]['gen_started'];4254 const prevStarted = lastMessage.gen_started;
41944255
4195 if (prevFinished && prevStarted) {4256 if (prevFinished && prevStarted) {
4196 const timePassed = Number(prevFinished) - Number(prevStarted);4257 const timePassed = Number(prevFinished) - Number(prevStarted);
4197 generation_started = new Date(Date.now() - timePassed);4258 generation_started = new Date(Date.now() - timePassed);
4198 chat[chat.length - 1]['gen_started'] = generation_started;4259 lastMessage.gen_started = generation_started;
4199 }4260 }
4200 }4261 }
42014262
@@ -4218,7 +4279,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4218 'continue',4279 'continue',
4219 ];4280 ];
4220 //for normal messages sent from user..4281 //for normal messages sent from user..
4221 if ((textareaText != '' || (hasPendingFileAttachment() && !noAttachTypes.includes(type))) && !automatic_trigger && type !== 'quiet' && !dryRun) {4282 if ((textareaText != '' || (hasPendingFileAttachment() && !noAttachTypes.includes(type))) && !automatic_trigger && type !== 'quiet' && !dryRun && !depth) {
4222 // If user message contains no text other than bias - send as a system message4283 // If user message contains no text other than bias - send as a system message
4223 if (messageBias && !removeMacros(textareaText)) {4284 if (messageBias && !removeMacros(textareaText)) {
4224 sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });4285 sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });
@@ -4227,7 +4288,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4227 await sendMessageAsUser(textareaText, messageBias);4288 await sendMessageAsUser(textareaText, messageBias);
4228 }4289 }
4229 }4290 }
4230 else if (textareaText == '' && !automatic_trigger && !dryRun && [undefined, 'normal'].includes(type) && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0) {4291 else if (textareaText == '' && !automatic_trigger && !dryRun && [undefined, 'normal'].includes(type) && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0 && !depth) {
4231 // Use send_if_empty if set and the user message is empty. Only when sending messages normally4292 // Use send_if_empty if set and the user message is empty. Only when sending messages normally
4232 await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias);4293 await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias);
4233 }4294 }
@@ -4877,7 +4938,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4877 let thisPromptContextSize = await getTokenCountAsync(prompt, power_user.token_padding);4938 let thisPromptContextSize = await getTokenCountAsync(prompt, power_user.token_padding);
48784939
4879 if (thisPromptContextSize > this_max_context) { //if the prepared prompt is larger than the max context size...4940 if (thisPromptContextSize > this_max_context) { //if the prepared prompt is larger than the max context size...
4880 if (count_exm_add > 0) { // ..and we have example mesages..4941 if (count_exm_add > 0) { // ..and we have example messages..
4881 count_exm_add--; // remove the example messages...4942 count_exm_add--; // remove the example messages...
4882 await checkPromptSize(); // and try agin...4943 await checkPromptSize(); // and try agin...
4883 } else if (mesSend.length > 0) { // if the chat history is longer than 04944 } else if (mesSend.length > 0) { // if the chat history is longer than 0
@@ -5117,7 +5178,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5117 chatInjects: injectedIndices?.map(index => arrMes[arrMes.length - index - 1])?.join('') || '',5178 chatInjects: injectedIndices?.map(index => arrMes[arrMes.length - index - 1])?.join('') || '',
5118 summarizeString: (extension_prompts['1_memory']?.value || ''),5179 summarizeString: (extension_prompts['1_memory']?.value || ''),
5119 authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''),5180 authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''),
5120 smartContextString: (extension_prompts['chromadb']?.value || ''),5181 smartContextString: (extension_prompts.chromadb?.value || ''),
5121 chatVectorsString: (extension_prompts['3_vectors']?.value || ''),5182 chatVectorsString: (extension_prompts['3_vectors']?.value || ''),
5122 dataBankVectorsString: (extension_prompts['4_vectors_data_bank']?.value || ''),5183 dataBankVectorsString: (extension_prompts['4_vectors_data_bank']?.value || ''),
5123 worldInfoString: worldInfoString,5184 worldInfoString: worldInfoString,
@@ -5605,7 +5666,7 @@ export function getBiasStrings(textareaText, type) {
5605function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) {5666function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) {
5606 const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR;5667 const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR;
5607 const characterName = chatItem?.name ? chatItem.name : name2;5668 const characterName = chatItem?.name ? chatItem.name : name2;
5608 const itemName = chatItem.is_user ? chatItem['name'] : characterName;5669 const itemName = chatItem.is_user ? chatItem.name : characterName;
5609 const shouldPrependName = !isNarratorType;5670 const shouldPrependName = !isNarratorType;
56105671
5611 // If this symbol flag is set, completely ignore the message.5672 // If this symbol flag is set, completely ignore the message.
@@ -5674,7 +5735,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
5674 await populateFileAttachment(message);5735 await populateFileAttachment(message);
5675 statMesProcess(message, 'user', characters, this_chid, '');5736 statMesProcess(message, 'user', characters, this_chid, '');
56765737
5677 chat_metadata['tainted'] = true;5738 chat_metadata.tainted = true;
56785739
5679 if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) {5740 if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) {
5680 chat.splice(insertAt, 0, message);5741 chat.splice(insertAt, 0, message);
@@ -5825,7 +5886,7 @@ function setInContextMessages(msgInContextCount, type) {
58255886
5826 // Update last id to chat. No metadata save on purpose, gets hopefully saved via another call5887 // Update last id to chat. No metadata save on purpose, gets hopefully saved via another call
5827 const lastMessageId = Math.max(0, chat.length - msgInContextCount);5888 const lastMessageId = Math.max(0, chat.length - msgInContextCount);
5828 chat_metadata['lastInContextMessageId'] = lastMessageId;5889 chat_metadata.lastInContextMessageId = lastMessageId;
5829}5890}
58305891
5831/**5892/**
@@ -6364,18 +6425,20 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6364 [type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls, reasoningSignature] = arguments;6425 [type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls, reasoningSignature] = arguments;
6365 }6426 }
63666427
6367 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined ||6428 const lastMessage = chat[chat.length - 1];
6368 chat[chat.length - 1]['is_user'])) {6429
6430 if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (lastMessage.swipe_id === undefined ||
6431 lastMessage.is_user)) {
6369 type = 'normal';6432 type = 'normal';
6370 }6433 }
63716434
6372 if (chat.length && (!chat[chat.length - 1]['extra'] || typeof chat[chat.length - 1]['extra'] !== 'object')) {6435 if (chat.length && (!lastMessage.extra || typeof lastMessage.extra !== 'object')) {
6373 chat[chat.length - 1]['extra'] = {};6436 lastMessage.extra = {};
6374 }6437 }
63756438
6376 // Coerce null/undefined to empty string6439 // Coerce null/undefined to empty string
6377 if (chat.length && !chat[chat.length - 1]['extra']['reasoning']) {6440 if (chat.length && !lastMessage.extra.reasoning) {
6378 chat[chat.length - 1]['extra']['reasoning'] = '';6441 lastMessage.extra.reasoning = '';
6379 }6442 }
63806443
6381 if (!reasoning) {6444 if (!reasoning) {
@@ -6385,70 +6448,70 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6385 let oldMessage = '';6448 let oldMessage = '';
6386 const generationFinished = new Date();6449 const generationFinished = new Date();
6387 if (type === 'swipe') {6450 if (type === 'swipe') {
6388 oldMessage = chat[chat.length - 1]['mes'];6451 oldMessage = lastMessage.mes;
6389 chat[chat.length - 1]['swipes'].length++;6452 lastMessage.swipes.length++;
6390 if (chat[chat.length - 1]['swipe_id'] === chat[chat.length - 1]['swipes'].length - 1) {6453 if (lastMessage.swipe_id === lastMessage.swipes.length - 1) {
6391 chat[chat.length - 1]['title'] = title;6454 lastMessage.title = title;
6392 chat[chat.length - 1]['mes'] = getMessage;6455 lastMessage.mes = getMessage;
6393 chat[chat.length - 1]['gen_started'] = generation_started;6456 lastMessage.gen_started = generation_started;
6394 chat[chat.length - 1]['gen_finished'] = generationFinished;6457 lastMessage.gen_finished = generationFinished;
6395 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();6458 lastMessage.send_date = getMessageTimeStamp();
6396 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();6459 lastMessage.extra.api = getGeneratingApi();
6397 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6460 lastMessage.extra.model = getGeneratingModel();
6398 chat[chat.length - 1]['extra']['reasoning'] = reasoning;6461 lastMessage.extra.reasoning = reasoning;
6399 chat[chat.length - 1]['extra']['reasoning_duration'] = null;6462 lastMessage.extra.reasoning_duration = null;
6400 chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;6463 lastMessage.extra.reasoning_signature = reasoningSignature;
6401 await processImageAttachment(chat[chat.length - 1], { imageUrls });6464 await processImageAttachment(lastMessage, { imageUrls });
6402 if (power_user.message_token_count_enabled) {6465 if (power_user.message_token_count_enabled) {
6403 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];6466 const tokenCountText = (reasoning || '') + lastMessage.mes;
6404 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);6467 lastMessage.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
6405 }6468 }
6406 const chat_id = (chat.length - 1);6469 const chat_id = (chat.length - 1);
6407 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);6470 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
6408 addOneMessage(chat[chat_id], { type: 'swipe' });6471 addOneMessage(chat[chat_id], { type: 'swipe' });
6409 !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);6472 !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
6410 } else {6473 } else {
6411 chat[chat.length - 1]['mes'] = getMessage;6474 lastMessage.mes = getMessage;
6412 }6475 }
6413 } else if (type === 'append' || type === 'continue') {6476 } else if (type === 'append' || type === 'continue') {
6414 console.debug('Trying to append.');6477 console.debug('Trying to append.');
6415 oldMessage = chat[chat.length - 1]['mes'];6478 oldMessage = lastMessage.mes;
6416 chat[chat.length - 1]['title'] = title;6479 lastMessage.title = title;
6417 chat[chat.length - 1]['mes'] += getMessage;6480 lastMessage.mes += getMessage;
6418 chat[chat.length - 1]['gen_started'] = generation_started;6481 lastMessage.gen_started = generation_started;
6419 chat[chat.length - 1]['gen_finished'] = generationFinished;6482 lastMessage.gen_finished = generationFinished;
6420 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();6483 lastMessage.send_date = getMessageTimeStamp();
6421 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();6484 lastMessage.extra.api = getGeneratingApi();
6422 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6485 lastMessage.extra.model = getGeneratingModel();
6423 chat[chat.length - 1]['extra']['reasoning'] = reasoning;6486 lastMessage.extra.reasoning = reasoning;
6424 chat[chat.length - 1]['extra']['reasoning_duration'] = null;6487 lastMessage.extra.reasoning_duration = null;
6425 chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;6488 lastMessage.extra.reasoning_signature = reasoningSignature;
6426 await processImageAttachment(chat[chat.length - 1], { imageUrls });6489 await processImageAttachment(lastMessage, { imageUrls });
6427 if (power_user.message_token_count_enabled) {6490 if (power_user.message_token_count_enabled) {
6428 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];6491 const tokenCountText = (reasoning || '') + lastMessage.mes;
6429 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);6492 lastMessage.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
6430 }6493 }
6431 const chat_id = (chat.length - 1);6494 const chat_id = (chat.length - 1);
6432 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);6495 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
6433 addOneMessage(chat[chat_id], { type: 'swipe' });6496 addOneMessage(chat[chat_id], { type: 'swipe' });
6434 !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);6497 !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
6435 } else if (type === 'appendFinal') {6498 } else if (type === 'appendFinal') {
6436 oldMessage = chat[chat.length - 1]['mes'];6499 oldMessage = lastMessage.mes;
6437 console.debug('Trying to appendFinal.');6500 console.debug('Trying to appendFinal.');
6438 chat[chat.length - 1]['title'] = title;6501 lastMessage.title = title;
6439 chat[chat.length - 1]['mes'] = getMessage;6502 lastMessage.mes = getMessage;
6440 chat[chat.length - 1]['gen_started'] = generation_started;6503 lastMessage.gen_started = generation_started;
6441 chat[chat.length - 1]['gen_finished'] = generationFinished;6504 lastMessage.gen_finished = generationFinished;
6442 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();6505 lastMessage.send_date = getMessageTimeStamp();
6443 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();6506 lastMessage.extra.api = getGeneratingApi();
6444 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6507 lastMessage.extra.model = getGeneratingModel();
6445 chat[chat.length - 1]['extra']['reasoning'] += reasoning;6508 lastMessage.extra.reasoning += reasoning;
6446 chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;6509 lastMessage.extra.reasoning_signature = reasoningSignature;
6447 await processImageAttachment(chat[chat.length - 1], { imageUrls });6510 await processImageAttachment(lastMessage, { imageUrls });
6448 // We don't know if the reasoning duration extended, so we don't update it here on purpose.6511 // We don't know if the reasoning duration extended, so we don't update it here on purpose.
6449 if (power_user.message_token_count_enabled) {6512 if (power_user.message_token_count_enabled) {
6450 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];6513 const tokenCountText = (reasoning || '') + lastMessage.mes;
6451 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);6514 lastMessage.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
6452 }6515 }
6453 const chat_id = (chat.length - 1);6516 const chat_id = (chat.length - 1);
6454 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);6517 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
@@ -6457,27 +6520,28 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
64576520
6458 } else {6521 } else {
6459 console.debug('entering chat update routine for non-swipe post');6522 console.debug('entering chat update routine for non-swipe post');
6460 chat[chat.length] = {};6523 const newMessage = {};
6461 chat[chat.length - 1]['extra'] = {};6524 chat.push(newMessage);
6462 chat[chat.length - 1]['name'] = name2;6525 newMessage.extra = {};
6463 chat[chat.length - 1]['is_user'] = false;6526 newMessage.name = name2;
6464 chat[chat.length - 1]['send_date'] = getMessageTimeStamp();6527 newMessage.is_user = false;
6465 chat[chat.length - 1]['extra']['api'] = getGeneratingApi();6528 newMessage.send_date = getMessageTimeStamp();
6466 chat[chat.length - 1]['extra']['model'] = getGeneratingModel();6529 newMessage.extra.api = getGeneratingApi();
6467 chat[chat.length - 1]['extra']['reasoning'] = reasoning;6530 newMessage.extra.model = getGeneratingModel();
6468 chat[chat.length - 1]['extra']['reasoning_duration'] = null;6531 newMessage.extra.reasoning = reasoning;
6469 chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature;6532 newMessage.extra.reasoning_duration = null;
6533 newMessage.extra.reasoning_signature = reasoningSignature;
6470 if (power_user.trim_spaces) {6534 if (power_user.trim_spaces) {
6471 getMessage = getMessage.trim();6535 getMessage = getMessage.trim();
6472 }6536 }
6473 chat[chat.length - 1]['mes'] = getMessage;6537 newMessage.mes = getMessage;
6474 chat[chat.length - 1]['title'] = title;6538 newMessage.title = title;
6475 chat[chat.length - 1]['gen_started'] = generation_started;6539 newMessage.gen_started = generation_started;
6476 chat[chat.length - 1]['gen_finished'] = generationFinished;6540 newMessage.gen_finished = generationFinished;
64776541
6478 if (power_user.message_token_count_enabled) {6542 if (power_user.message_token_count_enabled) {
6479 const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes'];6543 const tokenCountText = (reasoning || '') + newMessage.mes;
6480 chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);6544 newMessage.extra.token_count = await getTokenCountAsync(tokenCountText, 0);
6481 }6545 }
64826546
6483 if (selected_group) {6547 if (selected_group) {
@@ -6486,12 +6550,12 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6486 if (characters[this_chid].avatar != 'none') {6550 if (characters[this_chid].avatar != 'none') {
6487 avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);6551 avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
6488 }6552 }
6489 chat[chat.length - 1]['force_avatar'] = avatarImg;6553 newMessage.force_avatar = avatarImg;
6490 chat[chat.length - 1]['original_avatar'] = characters[this_chid].avatar;6554 newMessage.original_avatar = characters[this_chid].avatar;
6491 chat[chat.length - 1]['extra']['gen_id'] = group_generation_id;6555 newMessage.extra.gen_id = group_generation_id;
6492 }6556 }
64936557
6494 await processImageAttachment(chat[chat.length - 1], { imageUrls });6558 await processImageAttachment(newMessage, { imageUrls });
6495 const chat_id = (chat.length - 1);6559 const chat_id = (chat.length - 1);
64966560
6497 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);6561 !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
@@ -6500,27 +6564,27 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6500 }6564 }
65016565
6502 const item = chat[chat.length - 1];6566 const item = chat[chat.length - 1];
6503 if (item['swipe_info'] === undefined) {6567 if (item.swipe_info === undefined) {
6504 item['swipe_info'] = [];6568 item.swipe_info = [];
6505 }6569 }
6506 if (item['swipe_id'] !== undefined) {6570 if (item.swipe_id !== undefined) {
6507 const swipeId = item['swipe_id'];6571 const swipeId = item.swipe_id;
6508 item['swipes'][swipeId] = item['mes'];6572 item.swipes[swipeId] = item.mes;
6509 item['swipe_info'][swipeId] = {6573 item.swipe_info[swipeId] = {
6510 send_date: item['send_date'],6574 send_date: item.send_date,
6511 gen_started: item['gen_started'],6575 gen_started: item.gen_started,
6512 gen_finished: item['gen_finished'],6576 gen_finished: item.gen_finished,
6513 extra: structuredClone(item['extra']),6577 extra: structuredClone(item.extra),
6514 };6578 };
6515 } else {6579 } else {
6516 item['swipe_id'] = 0;6580 item.swipe_id = 0;
6517 item['swipes'] = [];6581 item.swipes = [];
6518 item['swipes'][0] = chat[chat.length - 1]['mes'];6582 item.swipes[0] = item.mes;
6519 item['swipe_info'][0] = {6583 item.swipe_info[0] = {
6520 send_date: chat[chat.length - 1]['send_date'],6584 send_date: item.send_date,
6521 gen_started: chat[chat.length - 1]['gen_started'],6585 gen_started: item.gen_started,
6522 gen_finished: chat[chat.length - 1]['gen_finished'],6586 gen_finished: item.gen_finished,
6523 extra: structuredClone(chat[chat.length - 1]['extra']),6587 extra: structuredClone(item.extra),
6524 };6588 };
6525 }6589 }
65266590
@@ -6541,7 +6605,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6541 item.swipe_info.push(...swipeInfoArray);6605 item.swipe_info.push(...swipeInfoArray);
6542 }6606 }
65436607
6544 statMesProcess(chat[chat.length - 1], type, characters, this_chid, oldMessage);6608 statMesProcess(item, type, characters, this_chid, oldMessage);
6545 return { type, getMessage };6609 return { type, getMessage };
6546}6610}
65476611
@@ -6644,7 +6708,10 @@ export function syncMesToSwipe(messageId = null) {
6644 return false;6708 return false;
6645 }6709 }
66466710
6711 // Only sync swipes if the chat is not pristine, so that macros in the greeting can resolve again on swipe
6712 if (chat_metadata.tainted || chat.length > 1) {
6647 targetMessage.swipes[targetMessage.swipe_id] = targetMessage.mes;6713 targetMessage.swipes[targetMessage.swipe_id] = targetMessage.mes;
6714 }
66486715
6649 targetSwipeInfo.send_date = targetMessage.send_date;6716 targetSwipeInfo.send_date = targetMessage.send_date;
6650 targetSwipeInfo.gen_started = targetMessage.gen_started;6717 targetSwipeInfo.gen_started = targetMessage.gen_started;
@@ -7126,7 +7193,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false }
7126 return;7193 return;
7127 }7194 }
71287195
7129 characters[this_chid]['date_last_chat'] = Date.now();7196 characters[this_chid].date_last_chat = Date.now();
71307197
7131 const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)7198 const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length)
7132 ? chat.slice(0, Number(mesId) + 1)7199 ? chat.slice(0, Number(mesId) + 1)
@@ -7343,41 +7410,49 @@ export async function unshallowCharacter(characterId) {
7343}7410}
73447411
7345export async function getChat() {7412export async function getChat() {
7346 //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);
7347 try {7413 try {
7348 await unshallowCharacter(this_chid);7414 await unshallowCharacter(this_chid);
73497415
7350 const response = await $.ajax({7416 const response = await fetch('/api/chats/get', {
7351 type: 'POST',7417 method: 'POST',
7352 url: '/api/chats/get',7418 headers: getRequestHeaders(),
7353 data: JSON.stringify({7419 cache: 'no-cache',
7420 body: JSON.stringify({
7354 ch_name: characters[this_chid].name,7421 ch_name: characters[this_chid].name,
7355 file_name: characters[this_chid].chat,7422 file_name: characters[this_chid].chat,
7356 avatar_url: characters[this_chid].avatar,7423 avatar_url: characters[this_chid].avatar,
7357 }),7424 }),
7358 dataType: 'json',
7359 contentType: 'application/json',
7360 });7425 });
7361 if (response[0] !== undefined) {
7362 chat.splice(0, chat.length, ...response);
7363 chat_metadata = chat[0]['chat_metadata'] ?? {};
73647426
7365 chat.shift();7427 if (!response.ok) {
7428 throw new Error('Chat could not be loaded');
7429 }
7430
7431 const data = await response.json();
7432 if (Array.isArray(data) && data.length > 0) {
7433 /** @type {ChatHeader} */
7434 const chatHeader = data.shift();
7435 chat_metadata = chatHeader?.chat_metadata ?? {};
7436 chat.splice(0, chat.length, ...data);
7366 chat.forEach(ensureMessageMediaIsArray);7437 chat.forEach(ensureMessageMediaIsArray);
7438 } else {
7439 // An empty/corrupted chat file
7440 chat.splice(0, chat.length);
7441 chat_metadata = {};
7367 }7442 }
7368 if (!chat_metadata['integrity']) {7443 if (!chat_metadata.integrity) {
7369 chat_metadata['integrity'] = uuidv4();7444 chat_metadata.integrity = uuidv4();
7370 }7445 }
7371 await getChatResult();7446 await getChatResult();
7372 eventSource.emit('chatLoaded', { detail: { id: this_chid, character: characters[this_chid] } });7447 eventSource.emit(event_types.CHAT_LOADED, { detail: { id: this_chid, character: characters[this_chid] } });
73737448
7374 // Focus on the textarea if not already focused on a visible text input7449 // Focus on the textarea if not already focused on a visible text input
7375 setTimeout(function () {7450 delay(debounce_timeout.short).then(() => {
7376 if ($(document.activeElement).is('input:visible, textarea:visible')) {7451 if ($(document.activeElement).is('input:visible, textarea:visible')) {
7377 return;7452 return;
7378 }7453 }
7379 $('#send_textarea').trigger('click').trigger('focus');7454 $('#send_textarea').trigger('click').trigger('focus');
7380 }, 200);7455 });
7381 } catch (error) {7456 } catch (error) {
7382 await getChatResult();7457 await getChatResult();
7383 console.log(error);7458 console.log(error);
@@ -7431,9 +7506,9 @@ function getFirstMessage() {
7431 message.mes = swipes[0];7506 message.mes = swipes[0];
7432 }7507 }
74337508
7434 message['swipe_id'] = 0;7509 message.swipe_id = 0;
7435 message['swipes'] = swipes;7510 message.swipes = swipes;
7436 message['swipe_info'] = swipes.map(_ => ({7511 message.swipe_info = swipes.map(_ => ({
7437 send_date: message.send_date,7512 send_date: message.send_date,
7438 gen_started: void 0,7513 gen_started: void 0,
7439 gen_finished: void 0,7514 gen_finished: void 0,
@@ -7446,9 +7521,8 @@ function getFirstMessage() {
74467521
7447export async function openCharacterChat(file_name) {7522export async function openCharacterChat(file_name) {
7448 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);7523 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);
7449 await clearChat();7524 await clearChat({ clearData: true });
7450 characters[this_chid]['chat'] = file_name;7525 characters[this_chid].chat = file_name;
7451 chat.length = 0;
7452 chat_metadata = {};7526 chat_metadata = {};
7453 await getChat();7527 await getChat();
7454 $('#selected_chat_pole').val(file_name);7528 $('#selected_chat_pole').val(file_name);
@@ -7717,6 +7791,10 @@ export async function getSettings() {
77177791
7718 selected_button = settings.selected_button;7792 selected_button = settings.selected_button;
77197793
7794 // TODO: Move me into firstLoadInit when experimental toggle is removed
7795 // power_user.experimental_macro_engine
7796 initMacros();
7797
7720 if (data.enable_extensions) {7798 if (data.enable_extensions) {
7721 const enableAutoUpdate = Boolean(data.enable_extensions_auto_update);7799 const enableAutoUpdate = Boolean(data.enable_extensions_auto_update);
7722 const isVersionChanged = settings.currentVersion !== currentVersion;7800 const isVersionChanged = settings.currentVersion !== currentVersion;
@@ -7833,7 +7911,7 @@ function updateMessage(div) {
7833 const mes = chat[mesElement.attr('mesid')];7911 const mes = chat[mesElement.attr('mesid')];
78347912
7835 // editing old messages7913 // editing old messages
7836 mes['extra'] ??= {};7914 mes.extra ??= {};
78377915
7838 let regexPlacement;7916 let regexPlacement;
7839 if (mes?.is_user) {7917 if (mes?.is_user) {
@@ -7864,10 +7942,10 @@ function updateMessage(div) {
7864 if (bias) {7942 if (bias) {
7865 text = removeMacros(text);7943 text = removeMacros(text);
7866 }7944 }
7867 mes['mes'] = text;7945 mes.mes = text;
7868 if (mes['swipe_id'] !== undefined) {7946 if (mes.swipe_id !== undefined) {
7869 ensureSwipes(mes);7947 ensureSwipes(mes);
7870 mes['swipes'][mes['swipe_id']] = text;7948 mes.swipes[mes.swipe_id] = text;
7871 }7949 }
78727950
7873 if (mes?.is_system || mes?.is_user || mes.extra?.type === system_message_types.NARRATOR) {7951 if (mes?.is_system || mes?.is_user || mes.extra?.type === system_message_types.NARRATOR) {
@@ -7876,7 +7954,7 @@ function updateMessage(div) {
7876 mes.extra.bias = null;7954 mes.extra.bias = null;
7877 }7955 }
78787956
7879 chat_metadata['tainted'] = true;7957 chat_metadata.tainted = true;
78807958
7881 return { mesBlock, text, mes, bias };7959 return { mesBlock, text, mes, bias };
7882}7960}
@@ -7960,6 +8038,7 @@ export async function messageEdit(editMessageId) {
7960 const editTextArea = document.createElement('textarea');8038 const editTextArea = document.createElement('textarea');
7961 editTextArea.id = 'curEditTextarea';8039 editTextArea.id = 'curEditTextarea';
7962 editTextArea.className = 'edit_textarea mdHotkeys';8040 editTextArea.className = 'edit_textarea mdHotkeys';
8041 editTextArea.dataset.macros = '';
7963 messageText.append(editTextArea);8042 messageText.append(editTextArea);
79648043
7965 const text = trimSpaces(editMessage.mes || '');8044 const text = trimSpaces(editMessage.mes || '');
@@ -7990,7 +8069,7 @@ export async function messageEdit(editMessageId) {
7990 * @param {number} [messageId=this_edit_mes_id]8069 * @param {number} [messageId=this_edit_mes_id]
7991 */8070 */
7992async function messageEditCancel(messageId = this_edit_mes_id) {8071async function messageEditCancel(messageId = this_edit_mes_id) {
7993 let text = chat[messageId]['mes'];8072 let text = chat[messageId].mes;
7994 let thisMesDiv;8073 let thisMesDiv;
7995 // If this is the button then select it's parent. Otherwise, select by messageId.8074 // If this is the button then select it's parent. Otherwise, select by messageId.
7996 if (this?.classList?.contains('mes_edit_cancel')) {8075 if (this?.classList?.contains('mes_edit_cancel')) {
@@ -8076,6 +8155,7 @@ async function messageEditMove(sourceId, targetId) {
8076 this_edit_mes_id = targetId;8155 this_edit_mes_id = targetId;
8077 }8156 }
80788157
8158 swapItemizedPrompts(sourceId, targetId);
8079 updateViewMessageIds();8159 updateViewMessageIds();
8080 refreshSwipeButtons();8160 refreshSwipeButtons();
8081 await saveChatConditional();8161 await saveChatConditional();
@@ -8089,9 +8169,6 @@ async function messageEditDone(div) {
8089 }8169 }
80908170
8091 let { mesBlock, text, mes, bias } = updateMessage(div);8171 let { mesBlock, text, mes, bias } = updateMessage(div);
8092 if (this_edit_mes_id == 0) {
8093 text = substituteParams(text);
8094 }
80958172
8096 await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id);8173 await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id);
8097 text = chat[this_edit_mes_id]?.mes ?? text;8174 text = chat[this_edit_mes_id]?.mes ?? text;
@@ -8138,7 +8215,7 @@ async function messageEditDone(div) {
8138export async function getChatsFromFiles(data, isGroupChat) {8215export async function getChatsFromFiles(data, isGroupChat) {
8139 const context = getContext();8216 const context = getContext();
8140 let chat_dict = {};8217 let chat_dict = {};
8141 let chat_list = Object.values(data).sort((a, b) => a['file_name'].localeCompare(b['file_name'])).reverse();8218 let chat_list = Object.values(data).sort((a, b) => a.file_name.localeCompare(b.file_name)).reverse();
81428219
8143 let chat_promise = chat_list.map(({ file_name }) => {8220 let chat_promise = chat_list.map(({ file_name }) => {
8144 return new Promise(async (res, rej) => {8221 return new Promise(async (res, rej) => {
@@ -8215,7 +8292,7 @@ export async function getPastCharacterChats(characterId = null) {
8215 }8292 }
82168293
8217 const chats = Object.values(data);8294 const chats = Object.values(data);
8218 return chats.sort((a, b) => a['file_name'].localeCompare(b['file_name'])).reverse();8295 return chats.sort((a, b) => a.file_name.localeCompare(b.file_name)).reverse();
8219}8296}
82208297
8221/**8298/**
@@ -8227,9 +8304,9 @@ export function getCurrentChatDetails() {
8227 }8304 }
82288305
8229 const group = selected_group ? groups.find(x => x.id === selected_group) : null;8306 const group = selected_group ? groups.find(x => x.id === selected_group) : null;
8230 const currentChat = selected_group ? group?.chat_id : characters[this_chid]['chat'];8307 const currentChat = selected_group ? group?.chat_id : characters[this_chid].chat;
8231 const displayName = selected_group ? group?.name : characters[this_chid].name;8308 const displayName = selected_group ? group?.name : characters[this_chid].name;
8232 const avatarImg = selected_group ? group?.avatar_url : getThumbnailUrl('avatar', characters[this_chid]['avatar']);8309 const avatarImg = selected_group ? group?.avatar_url : getThumbnailUrl('avatar', characters[this_chid].avatar);
8233 return { sessionName: currentChat, group: group, characterName: displayName, avatarImgURL: avatarImg };8310 return { sessionName: currentChat, group: group, characterName: displayName, avatarImgURL: avatarImg };
8234}8311}
82358312
@@ -8272,8 +8349,6 @@ export async function displayPastChats(hightlightNames = []) {
82728349
8273async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) {8350async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) {
8274 try {8351 try {
8275 const trimExtension = (fileName) => String(fileName).replace('.jsonl', '');
8276
8277 const response = await fetch('/api/chats/search', {8352 const response = await fetch('/api/chats/search', {
8278 method: 'POST',8353 method: 'POST',
8279 headers: getRequestHeaders(),8354 headers: getRequestHeaders(),
@@ -8294,7 +8369,7 @@ async function displayChats(searchQuery, currentChat, displayName, avatarImg, se
8294 filteredData.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)));8369 filteredData.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)));
82958370
8296 for (const chat of filteredData) {8371 for (const chat of filteredData) {
8297 const isSelected = trimExtension(currentChat) === trimExtension(chat.file_name);8372 const isSelected = currentChat === chat.file_name;
8298 const template = $('#past_chat_template .select_chat_block_wrapper').clone();8373 const template = $('#past_chat_template .select_chat_block_wrapper').clone();
8299 template.find('.select_chat_block').attr('file_name', chat.file_name);8374 template.find('.select_chat_block').attr('file_name', chat.file_name);
8300 template.find('.avatar img').attr('src', avatarImg);8375 template.find('.avatar img').attr('src', avatarImg);
@@ -8693,9 +8768,9 @@ export async function setCharacterSettingsOverrides() {
8693 return;8768 return;
8694 }8769 }
86958770
8696 const scenarioOverrideValue = chat_metadata['scenario'] || '';8771 const scenarioOverrideValue = chat_metadata.scenario || '';
8697 const exampleMessagesValue = chat_metadata['mes_example'] || '';8772 const exampleMessagesValue = chat_metadata.mes_example || '';
8698 const systemPromptValue = chat_metadata['system_prompt'] || '';8773 const systemPromptValue = chat_metadata.system_prompt || '';
8699 const isGroup = !!selected_group;8774 const isGroup = !!selected_group;
87008775
8701 const $template = $(await renderTemplateAsync('scenarioOverride'));8776 const $template = $(await renderTemplateAsync('scenarioOverride'));
@@ -8742,9 +8817,9 @@ export async function setCharacterSettingsOverrides() {
8742 allowVerticalScrolling: true,8817 allowVerticalScrolling: true,
8743 });8818 });
87448819
8745 chat_metadata['scenario'] = pendingChanges.scenario;8820 chat_metadata.scenario = pendingChanges.scenario;
8746 chat_metadata['mes_example'] = pendingChanges.examples;8821 chat_metadata.mes_example = pendingChanges.examples;
8747 chat_metadata['system_prompt'] = pendingChanges.system_prompt;8822 chat_metadata.system_prompt = pendingChanges.system_prompt;
8748 await saveMetadata();8823 await saveMetadata();
8749}8824}
87508825
@@ -9042,7 +9117,7 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {
9042 // Select the next swipe, or the one before if it was the last one9117 // Select the next swipe, or the one before if it was the last one
9043 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);9118 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);
90449119
9045 chat_metadata['tainted'] = true;9120 chat_metadata.tainted = true;
90469121
9047 messageId = Number(messageId);9122 messageId = Number(messageId);
9048 swipeId = Number(swipeId);9123 swipeId = Number(swipeId);
@@ -9399,6 +9474,11 @@ function addAlternateGreeting(template, greeting, index, getArray, popup) {
9399 * @param {Event} [e] Event that triggered the function call.9474 * @param {Event} [e] Event that triggered the function call.
9400 */9475 */
9401export async function createOrEditCharacter(e) {9476export async function createOrEditCharacter(e) {
9477 if (!settingsReady) {
9478 console.warn('Settings not ready, aborting character creation/editing.');
9479 return;
9480 }
9481
9402 $('#rm_info_avatar').html('');9482 $('#rm_info_avatar').html('');
9403 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));9483 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));
9404 formData.set('fav', String(fav_ch_checked));9484 formData.set('fav', String(fav_ch_checked));
@@ -9556,7 +9636,7 @@ export async function createOrEditCharacter(e) {
9556 !isNewChat &&9636 !isNewChat &&
9557 message.mes &&9637 message.mes &&
9558 !selected_group &&9638 !selected_group &&
9559 !chat_metadata['tainted'] &&9639 !chat_metadata.tainted &&
9560 (chat.length === 0 || (chat.length === 1 && !chat[0].is_user && !chat[0].is_system));9640 (chat.length === 0 || (chat.length === 1 && !chat[0].is_user && !chat[0].is_system));
95619641
9562 if (shouldRegenerateMessage) {9642 if (shouldRegenerateMessage) {
@@ -9576,23 +9656,6 @@ export async function createOrEditCharacter(e) {
9576}9656}
95779657
9578/**9658/**
9579 * Visually updates all chat messages including andd after index by removing them, then adding them.
9580 * @param {ChatMessage[]} chat All messages in chat before index will remain unchanged.
9581 * @param {Number} index The last unchanged messageId.
9582 */
9583export async function redisplayChat(chat, index) {
9584 //Remove messages after index.
9585 chatElement.children(`.mes[mesid="${index}"]`).nextAll('.mes').addBack().remove();
9586
9587 //Skip to index, then add extra messages.
9588 for (let i = index; i <= chat.length - 1; i++) {
9589 //addOneMessage will update last_mes.
9590 addOneMessage(chat[i], { scroll: false, showSwipes: false, forceId: i });
9591 }
9592 refreshSwipeButtons();
9593}
9594
9595/**
9596 * Formats a counter for a swipe view.9659 * Formats a counter for a swipe view.
9597 * @param {number} current The current number of items.9660 * @param {number} current The current number of items.
9598 * @param {number} total The total number of items.9661 * @param {number} total The total number of items.
@@ -9669,7 +9732,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
9669 console.error(`Message #${mesId}'s DOM element is not valid.`);9732 console.error(`Message #${mesId}'s DOM element is not valid.`);
9670 return;9733 return;
9671 }9734 }
9672 const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0);9735 const originalSwipeId = Number(chat[mesId]?.swipe_id ?? 0);
9673 let newSwipeId = Number(forceSwipeId ?? originalSwipeId);9736 let newSwipeId = Number(forceSwipeId ?? originalSwipeId);
96749737
9675 /**9738 /**
@@ -9716,7 +9779,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
9716 }9779 }
97179780
9718 //Clamp Id between swipes.9781 //Clamp Id between swipes.
9719 let clampedId = clamp(chat[mesId]['swipe_id'], 0, Math.max(0, chat[mesId]['swipes'].length - 1));9782 let clampedId = clamp(chat[mesId].swipe_id, 0, Math.max(0, chat[mesId].swipes.length - 1));
97209783
9721 await updateSwipeCounter(mesId);9784 await updateSwipeCounter(mesId);
9722 //Fallback.9785 //Fallback.
@@ -9746,7 +9809,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
97469809
9747 //Update the chat.9810 //Update the chat.
9748 await loadFromSwipeId(mesId, chat[mesId].swipe_id);9811 await loadFromSwipeId(mesId, chat[mesId].swipe_id);
9749 await redisplayChat(chat, mesId);9812 await redisplayChat({ startIndex: mesId });
9750 }9813 }
9751 else {9814 else {
9752 await Popup.show.confirm(9815 await Popup.show.confirm(
@@ -9808,7 +9871,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
9808 */9871 */
9809 async function loadFromSwipeId(mesId, newSwipeId) {9872 async function loadFromSwipeId(mesId, newSwipeId) {
9810 //Update the swipe_id.9873 //Update the swipe_id.
9811 chat[mesId]['swipe_id'] = newSwipeId;9874 chat[mesId].swipe_id = newSwipeId;
98129875
9813 clearMessageData(chat[mesId]);9876 clearMessageData(chat[mesId]);
98149877
@@ -9880,7 +9943,8 @@ export async function swipe(event, direction, { source, repeated, message = chat
9880 return true;9943 return true;
9881 };9944 };
9882 //Wait for the animation's end. https://developer.mozilla.org/en-US/docs/Web/API/Animation/finished9945 //Wait for the animation's end. https://developer.mozilla.org/en-US/docs/Web/API/Animation/finished
9883 const animation = swipedElementsDiv[0]?.getAnimations().filter((a) => a['animationName'] == 'slide')[0];9946 const animations = swipedElementsDiv[0]?.getAnimations() ?? [];
9947 const animation = animations.filter((a) => a instanceof globalThis.CSSAnimation && a.animationName == 'slide')[0];
9884 try {9948 try {
9885 await Promise.race([animation?.finished, createTimeout(duration * 2, `The ${duration}ms swipe animation has not ended after ${duration * 2}ms. It has been skipped.`)].filter(Boolean));9949 await Promise.race([animation?.finished, createTimeout(duration * 2, `The ${duration}ms swipe animation has not ended after ${duration * 2}ms. It has been skipped.`)].filter(Boolean));
9886 } catch (error) {9950 } catch (error) {
@@ -9968,7 +10032,7 @@ export async function swipe(event, direction, { source, repeated, message = chat
996810032
9969 const tokenCountText = (chat[mesId]?.extra?.reasoning || '') + chat[mesId].mes;10033 const tokenCountText = (chat[mesId]?.extra?.reasoning || '') + chat[mesId].mes;
9970 const tokenCount = await getTokenCountAsync(tokenCountText, 0);10034 const tokenCount = await getTokenCountAsync(tokenCountText, 0);
9971 chat[mesId]['extra']['token_count'] = tokenCount;10035 chat[mesId].extra.token_count = tokenCount;
9972 thisMesDiv.find('.tokenCounterDisplay').text(`${tokenCount}t`);10036 thisMesDiv.find('.tokenCounterDisplay').text(`${tokenCount}t`);
9973 }10037 }
9974 }10038 }
@@ -9977,7 +10041,9 @@ export async function swipe(event, direction, { source, repeated, message = chat
9977 thisMesDiv.css('height', thisMesDivHeight);10041 thisMesDiv.css('height', thisMesDivHeight);
9978 expandNewMessage(thisMesDiv);10042 expandNewMessage(thisMesDiv);
997910043
10044 if (run_generate) {
9980 appendMediaToMessage(chat[mesId], thisMesDiv);10045 appendMediaToMessage(chat[mesId], thisMesDiv);
10046 }
998110047
9982 await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId));10048 await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId));
998310049
@@ -10007,20 +10073,20 @@ export async function swipe(event, direction, { source, repeated, message = chat
10007 // Make sure ad-hoc changes to extras are saved before swiping away10073 // Make sure ad-hoc changes to extras are saved before swiping away
10008 syncMesToSwipe(mesId);10074 syncMesToSwipe(mesId);
1000910075
10010 if (chat[mesId]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array10076 if (chat[mesId].swipe_id === undefined) { // if there is no swipe-message in the last spot of the chat array
10011 chat[mesId]['swipe_id'] = 0; // set it to id 010077 chat[mesId].swipe_id = 0; // set it to id 0
10012 chat[mesId]['swipes'] = []; // empty the array10078 chat[mesId].swipes = []; // empty the array
10013 chat[mesId]['swipe_info'] = [];10079 chat[mesId].swipe_info = [];
10014 chat[mesId]['swipes'][0] = chat[mesId]['mes']; //assign swipe array with last chat[mesId] from chat10080 chat[mesId].swipes[0] = chat[mesId].mes; //assign swipe array with last chat[mesId] from chat
10015 chat[mesId]['swipe_info'][0] = {10081 chat[mesId].swipe_info[0] = {
10016 'send_date': chat[mesId]['send_date'],10082 'send_date': chat[mesId].send_date,
10017 'gen_started': chat[mesId]['gen_started'],10083 'gen_started': chat[mesId].gen_started,
10018 'gen_finished': chat[mesId]['gen_finished'],10084 'gen_finished': chat[mesId].gen_finished,
10019 'extra': structuredClone(chat[mesId]['extra']),10085 'extra': structuredClone(chat[mesId].extra),
10020 };10086 };
10021 }10087 }
10022 // If the user is holding down the key and we're at the last or first swipe, don't do anything.10088 // If the user is holding down the key and we're at the last or first swipe, don't do anything.
10023 let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId]['swipes'].length - 1)) : chat[mesId].swipe_id === 0;10089 let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId].swipes.length - 1)) : chat[mesId].swipe_id === 0;
10024 if (source === SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) {10090 if (source === SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) {
10025 await endSwipe();10091 await endSwipe();
10026 return;10092 return;
@@ -10036,12 +10102,12 @@ export async function swipe(event, direction, { source, repeated, message = chat
10036 if (forceSwipeId == null) newSwipeId--;10102 if (forceSwipeId == null) newSwipeId--;
10037 //Loop to last swipe if negative.10103 //Loop to last swipe if negative.
10038 if (newSwipeId < 0) {10104 if (newSwipeId < 0) {
10039 newSwipeId = Math.max(0, chat[mesId]['swipes'].length - 1);10105 newSwipeId = Math.max(0, chat[mesId].swipes.length - 1);
10040 }10106 }
10041 //Limit swipe_id to swipes.10107 //Limit swipe_id to swipes.
10042 if (newSwipeId > chat[mesId]['swipes'].length - 1) {10108 if (newSwipeId > chat[mesId].swipes.length - 1) {
10043 toastr.warning(`The swipe_id for message #${mesId} was ${newSwipeId}. It has been reset to ${chat[mesId]['swipes'].length - 1}.`);10109 toastr.warning(`The swipe_id for message #${mesId} was ${newSwipeId}. It has been reset to ${chat[mesId].swipes.length - 1}.`);
10044 chat[mesId]['swipe_id'] = chat[mesId]['swipes'].length - 1;10110 chat[mesId].swipe_id = chat[mesId].swipes.length - 1;
10045 await endSwipe();10111 await endSwipe();
10046 return;10112 return;
10047 }10113 }
@@ -10056,24 +10122,24 @@ export async function swipe(event, direction, { source, repeated, message = chat
10056 //Minimum of zero.10122 //Minimum of zero.
10057 if (newSwipeId < 0) {10123 if (newSwipeId < 0) {
10058 toastr.warning(`The swipe_id for message #${mesId} was ${newSwipeId}. It has been reset to zero.`);10124 toastr.warning(`The swipe_id for message #${mesId} was ${newSwipeId}. It has been reset to zero.`);
10059 chat[mesId]['swipe_id'] = 0;10125 chat[mesId].swipe_id = 0;
10060 await endSwipe();10126 await endSwipe();
10061 return;10127 return;
10062 }10128 }
1006310129
10064 //If overswiping.10130 //If overswiping.
10065 if (newSwipeId >= chat[mesId]['swipes'].length) {10131 if (newSwipeId >= chat[mesId].swipes.length) {
10066 newSwipeId = chat[mesId]['swipes'].length;10132 newSwipeId = chat[mesId].swipes.length;
1006710133
10068 //Update the swipe_id.10134 //Update the swipe_id.
10069 chat[mesId]['swipe_id'] = newSwipeId;10135 chat[mesId].swipe_id = newSwipeId;
1007010136
10071 const overswipe = getOverswipeBehavior(mesId);10137 const overswipe = getOverswipeBehavior(mesId);
1007210138
10073 //Cancel the generation.10139 //Cancel the generation.
10074 if (overswipe == OVERSWIPE_BEHAVIOR.NONE) {10140 if (overswipe == OVERSWIPE_BEHAVIOR.NONE) {
10075 //Cancel swipe.10141 //Cancel swipe.
10076 chat[mesId]['swipe_id'] = originalSwipeId;10142 chat[mesId].swipe_id = originalSwipeId;
10077 await endSwipe();10143 await endSwipe();
10078 return;10144 return;
10079 }10145 }
@@ -10294,8 +10360,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
1029410360
10295 //Fix it; New chat doesn't create while open create character menu10361 //Fix it; New chat doesn't create while open create character menu
10296 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);10362 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);
10297 await clearChat();10363 await clearChat({ clearData: true });
10298 chat.length = 0;
1029910364
10300 chat_file_for_del = getCurrentChatDetails()?.sessionName;10365 chat_file_for_del = getCurrentChatDetails()?.sessionName;
1030110366
@@ -10414,8 +10479,7 @@ export async function renameChat(oldFileName, newName) {
10414export async function closeCurrentChat() {10479export async function closeCurrentChat() {
10415 if (is_send_press == false) {10480 if (is_send_press == false) {
10416 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);10481 await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);
10417 await clearChat();10482 await clearChat({ clearData: true });
10418 chat.length = 0;
10419 resetSelectedGroup();10483 resetSelectedGroup();
10420 setCharacterId(undefined);10484 setCharacterId(undefined);
10421 setCharacterName('');10485 setCharacterName('');
@@ -10946,7 +11010,7 @@ jQuery(async function () {
10946 if (group) {11010 if (group) {
10947 await deleteGroupChat(group, chatFile);11011 await deleteGroupChat(group, chatFile);
10948 } else {11012 } else {
10949 await delChat(chatFile);11013 await delChat(`${chatFile}.jsonl`);
10950 }11014 }
1095111015
10952 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.11016 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.
@@ -10963,18 +11027,18 @@ jQuery(async function () {
1096311027
10964 $(document).on('click', '.PastChat_cross', async function (e, { fromSlashCommand = false } = {}) {11028 $(document).on('click', '.PastChat_cross', async function (e, { fromSlashCommand = false } = {}) {
10965 e.stopPropagation();11029 e.stopPropagation();
10966 chat_file_for_del = $(this).attr('file_name');11030 const deleteFileName = $(this).attr('file_name');
10967 console.debug('detected cross click for' + chat_file_for_del);11031 console.debug('detected cross click for' + deleteFileName);
1096811032
10969 // Skip confirmation if called from a slash command.11033 // Skip confirmation if called from a slash command.
10970 if (fromSlashCommand) {11034 if (fromSlashCommand) {
10971 await handleDeleteChat(chat_file_for_del, selected_group, true);11035 await handleDeleteChat(deleteFileName, selected_group, true);
10972 return;11036 return;
10973 }11037 }
1097411038
10975 const result = await callGenericPopup('<h3>' + t`Delete the Chat File?` + '</h3>', POPUP_TYPE.CONFIRM);11039 const result = await callGenericPopup('<h3>' + t`Delete the Chat File?` + '</h3>', POPUP_TYPE.CONFIRM);
10976 if (result === POPUP_RESULT.AFFIRMATIVE) {11040 if (result === POPUP_RESULT.AFFIRMATIVE) {
10977 await handleDeleteChat(chat_file_for_del, selected_group, false);11041 await handleDeleteChat(deleteFileName, selected_group, false);
10978 }11042 }
10979 });11043 });
1098011044
@@ -11008,8 +11072,7 @@ jQuery(async function () {
11008 $('#character_popup').css('display', 'none');11072 $('#character_popup').css('display', 'none');
11009 });11073 });
1101011074
11011 $('#dialogue_popup_ok').on('click', async function (_e, customData) {11075 $('#dialogue_popup_ok').on('click', async function (_e) {
11012 const fromSlashCommand = customData?.fromSlashCommand || false;
11013 dialogueCloseStop = false;11076 dialogueCloseStop = false;
11014 $('#shadow_popup').transition({11077 $('#shadow_popup').transition({
11015 opacity: 0,11078 opacity: 0,
@@ -11023,10 +11086,6 @@ jQuery(async function () {
11023 $('#dialogue_popup').removeClass('wide_dialogue_popup');11086 $('#dialogue_popup').removeClass('wide_dialogue_popup');
11024 }, animation_duration);11087 }, animation_duration);
1102511088
11026 if (popup_type == 'del_chat') {
11027 await handleDeleteChat(chat_file_for_del, selected_group, fromSlashCommand);
11028 }
11029
11030 if (dialogueResolve) {11089 if (dialogueResolve) {
11031 if (popup_type == 'input') {11090 if (popup_type == 'input') {
11032 dialogueResolve($('#dialogue_popup_input').val());11091 dialogueResolve($('#dialogue_popup_input').val());
@@ -11139,8 +11198,7 @@ jQuery(async function () {
1113911198
11140 $(document).on('click', '.renameChatButton', async function (e) {11199 $(document).on('click', '.renameChatButton', async function (e) {
11141 e.stopPropagation();11200 e.stopPropagation();
11142 const oldFileNameFull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();11201 const oldFileName = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();
11143 const oldFileName = oldFileNameFull.replace('.jsonl', '');
1114411202
11145 const popupText = await renderTemplateAsync('chatRename');11203 const popupText = await renderTemplateAsync('chatRename');
11146 const newName = await callGenericPopup(popupText, POPUP_TYPE.INPUT, oldFileName);11204 const newName = await callGenericPopup(popupText, POPUP_TYPE.INPUT, oldFileName);
@@ -11161,10 +11219,9 @@ jQuery(async function () {
11161 e.stopPropagation();11219 e.stopPropagation();
11162 const format = $(this).data('format') || 'txt';11220 const format = $(this).data('format') || 'txt';
11163 await saveChatConditional();11221 await saveChatConditional();
11164 const filenamefull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();11222 const filename = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();
11165 console.log(`exporting ${filenamefull} in ${format} format`);11223 console.log(`exporting ${filename} in ${format} format`);
1116611224
11167 const filename = filenamefull.replace('.jsonl', '');
11168 const body = {11225 const body = {
11169 is_group: !!selected_group,11226 is_group: !!selected_group,
11170 avatar_url: characters[this_chid]?.avatar,11227 avatar_url: characters[this_chid]?.avatar,
@@ -11398,10 +11455,13 @@ jQuery(async function () {
11398 });11455 });
1139911456
11400 if (this_del_mes >= 0) {11457 if (this_del_mes >= 0) {
11458 for (let i = (chat.length - 1); i >= this_del_mes; i--) {
11459 deleteItemizedPromptForMessage(i);
11460 }
11401 chatElement.find(`.mes[mesid="${this_del_mes}"]`).nextAll('div').remove();11461 chatElement.find(`.mes[mesid="${this_del_mes}"]`).nextAll('div').remove();
11402 chatElement.find(`.mes[mesid="${this_del_mes}"]`).remove();11462 chatElement.find(`.mes[mesid="${this_del_mes}"]`).remove();
11403 chat.length = this_del_mes;11463 chat.length = this_del_mes;
11404 chat_metadata['tainted'] = true;11464 chat_metadata.tainted = true;
11405 await saveChatConditional();11465 await saveChatConditional();
11406 chatElement.scrollTop(chatElement[0].scrollHeight);11466 chatElement.scrollTop(chatElement[0].scrollHeight);
11407 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);11467 await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
@@ -11488,7 +11548,7 @@ jQuery(async function () {
11488 if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) {11548 if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) {
11489 try {11549 try {
11490 const messageId = $(this).closest('.mes').attr('mesid');11550 const messageId = $(this).closest('.mes').attr('mesid');
11491 const text = chat[messageId]['mes'];11551 const text = chat[messageId].mes;
11492 await copyText(text);11552 await copyText(text);
11493 toastr.info('Copied!', '', { timeOut: 2000 });11553 toastr.info('Copied!', '', { timeOut: 2000 });
11494 } catch (err) {11554 } catch (err) {
@@ -11515,8 +11575,8 @@ jQuery(async function () {
11515 let mes_edited = chatElement.find(`[mesid="${this_edit_mes_id}"]`).find('.mes_edit_done');11575 let mes_edited = chatElement.find(`[mesid="${this_edit_mes_id}"]`).find('.mes_edit_done');
11516 if (Number(edit_mes_id) == chat.length - 1) { //if the generating swipe (...)11576 if (Number(edit_mes_id) == chat.length - 1) { //if the generating swipe (...)
11517 let run_edit = true;11577 let run_edit = true;
11518 if (chat[edit_mes_id]['swipe_id'] !== undefined) {11578 if (chat[edit_mes_id].swipe_id !== undefined) {
11519 if (chat[edit_mes_id]['swipes'].length === chat[edit_mes_id]['swipe_id']) {11579 if (chat[edit_mes_id].swipes.length === chat[edit_mes_id].swipe_id) {
11520 run_edit = false;11580 run_edit = false;
11521 }11581 }
11522 }11582 }
@@ -11637,14 +11697,16 @@ jQuery(async function () {
11637 const oldScroll = chatElement[0].scrollTop;11697 const oldScroll = chatElement[0].scrollTop;
11638 const clone = structuredClone(chat[this_edit_mes_id]);11698 const clone = structuredClone(chat[this_edit_mes_id]);
11639 clone.send_date = Date.now();11699 clone.send_date = Date.now();
11640 clone.mes = $(this).closest('.mes').find('.edit_textarea').val().toString();11700 const this_edit_mes_element = $(this).closest('.mes');
11701 clone.mes = this_edit_mes_element.find('.edit_textarea').val().toString();
1164111702
11642 if (power_user.trim_spaces) {11703 if (power_user.trim_spaces) {
11643 clone.mes = clone.mes.trim();11704 clone.mes = clone.mes.trim();
11644 }11705 }
1164511706
11646 chat.splice(Number(this_edit_mes_id) + 1, 0, clone);11707 chat.splice(Number(this_edit_mes_id) + 1, 0, clone);
11647 addOneMessage(clone, { insertAfter: this_edit_mes_id });11708 const newMessageElement = updateMessageElement(clone);
11709 this_edit_mes_element.after(newMessageElement);
1164811710
11649 updateViewMessageIds();11711 updateViewMessageIds();
11650 await saveChatConditional();11712 await saveChatConditional();
@@ -11655,8 +11717,8 @@ jQuery(async function () {
11655 $(document).on('click', '.mes_edit_delete', async function (event, customData) {11717 $(document).on('click', '.mes_edit_delete', async function (event, customData) {
11656 const fromSlashCommand = customData?.fromSlashCommand || false;11718 const fromSlashCommand = customData?.fromSlashCommand || false;
11657 const message = chat[this_edit_mes_id];11719 const message = chat[this_edit_mes_id];
11658 const selectedSwipe = message['swipe_id'] ?? undefined;11720 const selectedSwipe = message.swipe_id ?? undefined;
11659 const swipesArray = Array.isArray(message['swipes']) ? message['swipes'] : [];11721 const swipesArray = Array.isArray(message.swipes) ? message.swipes : [];
11660 const canDeleteSwipe = power_user.confirm_message_delete && !fromSlashCommand && !message.is_user && swipesArray.length > 1 && this_edit_mes_id === chat.length - 1 && selectedSwipe !== undefined;11722 const canDeleteSwipe = power_user.confirm_message_delete && !fromSlashCommand && !message.is_user && swipesArray.length > 1 && this_edit_mes_id === chat.length - 1 && selectedSwipe !== undefined;
11661 await deleteMessage(Number(this_edit_mes_id), canDeleteSwipe ? selectedSwipe : undefined, power_user.confirm_message_delete && fromSlashCommand !== true);11723 await deleteMessage(Number(this_edit_mes_id), canDeleteSwipe ? selectedSwipe : undefined, power_user.confirm_message_delete && fromSlashCommand !== true);
11662 });11724 });
@@ -12012,7 +12074,9 @@ jQuery(async function () {
12012 }12074 }
12013 if (this_edit_mes_id === undefined && $('#mes_stop').is(':visible')) {12075 if (this_edit_mes_id === undefined && $('#mes_stop').is(':visible')) {
12014 $('#mes_stop').trigger('click');12076 $('#mes_stop').trigger('click');
12015 if (chat.length && Array.isArray(chat[chat.length - 1].swipes) && chat[chat.length - 1].swipe_id == chat[chat.length - 1].swipes.length) {12077 if (chat.length === 0) return;
12078 const lastMessage = chat[chat.length - 1];
12079 if (Array.isArray(lastMessage.swipes) && lastMessage.swipe_id == lastMessage.swipes.length) {
12016 $('.last_mes .swipe_left').trigger('click');12080 $('.last_mes .swipe_left').trigger('click');
12017 }12081 }
12018 }12082 }
@@ -12071,7 +12135,7 @@ jQuery(async function () {
12071 });12135 });
1207212136
12073 // Remember the chat currently selected, so we can reload it after the replacement12137 // Remember the chat currently selected, so we can reload it after the replacement
12074 const currentChatFile = characters[this_chid]['chat'];12138 const currentChatFile = characters[this_chid].chat;
12075 async function postReplace() {12139 async function postReplace() {
12076 await openCharacterChat(currentChatFile);12140 await openCharacterChat(currentChatFile);
12077 }12141 }
public/scripts/PromptManager.js+1 -1
@@ -765,7 +765,7 @@ class PromptManager {
765 eventSource.on(event_types.CHATCOMPLETION_MODEL_CHANGED, () => this.renderDebounced());765 eventSource.on(event_types.CHATCOMPLETION_MODEL_CHANGED, () => this.renderDebounced());
766766
767 // Re-render when the character changes.767 // Re-render when the character changes.
768 eventSource.on('chatLoaded', (event) => {768 eventSource.on(event_types.CHAT_LOADED, (event) => {
769 this.handleCharacterSelected(event);769 this.handleCharacterSelected(event);
770 this.saveServiceSettings().then(() => this.renderDebounced());770 this.saveServiceSettings().then(() => this.renderDebounced());
771 });771 });
public/scripts/RossAscends-mods.js+5 -5
@@ -408,7 +408,7 @@ function RA_autoconnect(PrevApi) {
408 || (secret_state[SECRET_KEYS.FIREWORKS] && oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS)408 || (secret_state[SECRET_KEYS.FIREWORKS] && oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS)
409 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)409 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
410 || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)410 || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)
411 || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)411 || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
412 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)412 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
413 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)413 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
414 ) {414 ) {
@@ -996,7 +996,7 @@ export function initRossMods() {
996 }996 }
997997
998 //Enter to send when send_textarea in focus998 //Enter to send when send_textarea in focus
999 if (document.activeElement == hotkeyTargets['send_textarea']) {999 if (document.activeElement == hotkeyTargets.send_textarea) {
1000 const sendOnEnter = shouldSendOnEnter();1000 const sendOnEnter = shouldSendOnEnter();
1001 if (!event.isComposing && !event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {1001 if (!event.isComposing && !event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {
1002 event.preventDefault();1002 event.preventDefault();
@@ -1004,7 +1004,7 @@ export function initRossMods() {
1004 return;1004 return;
1005 }1005 }
1006 }1006 }
1007 if (document.activeElement == hotkeyTargets['dialogue_popup_input'] && !isMobile()) {1007 if (document.activeElement == hotkeyTargets.dialogue_popup_input && !isMobile()) {
1008 if (!event.shiftKey && !event.ctrlKey && event.key == 'Enter') {1008 if (!event.shiftKey && !event.ctrlKey && event.key == 'Enter') {
1009 event.preventDefault();1009 event.preventDefault();
1010 $('#dialogue_popup_ok').trigger('click');1010 $('#dialogue_popup_ok').trigger('click');
@@ -1139,7 +1139,7 @@ export function initRossMods() {
11391139
1140 if (event.ctrlKey && event.key == 'ArrowUp') { //edits last USER message if chatbar is empty and focused1140 if (event.ctrlKey && event.key == 'ArrowUp') { //edits last USER message if chatbar is empty and focused
1141 if (1141 if (
1142 hotkeyTargets['send_textarea'].value === '' &&1142 hotkeyTargets.send_textarea.value === '' &&
1143 chatbarInFocus === true &&1143 chatbarInFocus === true &&
1144 ($('.swipe_right:last').css('display') === 'flex' || $('.last_mes').attr('is_system') === 'true') &&1144 ($('.swipe_right:last').css('display') === 'flex' || $('.last_mes').attr('is_system') === 'true') &&
1145 $('#character_popup').css('display') === 'none' &&1145 $('#character_popup').css('display') === 'none' &&
@@ -1158,7 +1158,7 @@ export function initRossMods() {
1158 if (event.key == 'ArrowUp') { //edits last message if chatbar is empty and focused1158 if (event.key == 'ArrowUp') { //edits last message if chatbar is empty and focused
1159 console.log('got uparrow input');1159 console.log('got uparrow input');
1160 if (1160 if (
1161 hotkeyTargets['send_textarea'].value === '' &&1161 hotkeyTargets.send_textarea.value === '' &&
1162 chatbarInFocus === true &&1162 chatbarInFocus === true &&
1163 //$('.swipe_right:last').css('display') === 'flex' &&1163 //$('.swipe_right:last').css('display') === 'flex' &&
1164 $('.last_mes .mes_buttons').is(':visible') &&1164 $('.last_mes .mes_buttons').is(':visible') &&
public/scripts/authors-note.js+1 -1
@@ -594,7 +594,7 @@ function registerAuthorsNoteMacros() {
594 handler: () => chat_metadata[metadata_keys.prompt] ?? '',594 handler: () => chat_metadata[metadata_keys.prompt] ?? '',
595 });595 });
596 macros.register('charAuthorsNote', {596 macros.register('charAuthorsNote', {
597 category: MacroCategory.CHARACTER,597 category: MacroCategory.PROMPTS,
598 description: t`The contents of the Character Author's Note`,598 description: t`The contents of the Character Author's Note`,
599 handler: () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '',599 handler: () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '',
600 });600 });
public/scripts/autocomplete/AutoComplete.js+60 -8
@@ -155,6 +155,10 @@ export class AutoComplete {
155 */155 */
156 updateName(item) {156 updateName(item) {
157 const chars = Array.from(item.dom.querySelector('.name').children);157 const chars = Array.from(item.dom.querySelector('.name').children);
158 if (item.forceFullNameMatch) {
159 chars.forEach(c => c.classList.toggle('matched', true));
160 return;
161 }
158 switch (this.matchType) {162 switch (this.matchType) {
159 case 'strict': {163 case 'strict': {
160 chars.forEach((it, idx) => {164 chars.forEach((it, idx) => {
@@ -275,6 +279,7 @@ export class AutoComplete {
275 //TODO check if isInput and isForced are both required279 //TODO check if isInput and isForced are both required
276 this.text = this.textarea.value;280 this.text = this.textarea.value;
277 this.isReplaceable = false;281 this.isReplaceable = false;
282 this.isShowForced = isForced; // Store forced state for checkIfActivate to access
278283
279 if (document.activeElement != this.textarea) {284 if (document.activeElement != this.textarea) {
280 // only show with textarea in focus285 // only show with textarea in focus
@@ -311,8 +316,8 @@ export class AutoComplete {
311 this.name = this.parserResult.name.toLowerCase() ?? '';316 this.name = this.parserResult.name.toLowerCase() ?? '';
312317
313 const isCursorInNamePart = this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0);318 const isCursorInNamePart = this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0);
314 if (isForced || isInput) {319 if (isForced || isInput || isSelect) {
315 // if forced (ctrl+space) or user input...320 // if forced (ctrl+space) or user input or just selected an option...
316 if (isCursorInNamePart) {321 if (isCursorInNamePart) {
317 // ...and cursor is somewhere in the name part (including right behind the final char)322 // ...and cursor is somewhere in the name part (including right behind the final char)
318 // -> show autocomplete for the (partial if cursor in the middle) name323 // -> show autocomplete for the (partial if cursor in the middle) name
@@ -393,8 +398,20 @@ export class AutoComplete {
393 this.updateName(option);398 this.updateName(option);
394 return option;399 return option;
395 })400 })
396 // sort by fuzzy score or alphabetical401 // sort by priority first, then by fuzzy score or alphabetical
397 .toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name));402 .toSorted((a, b) => {
403 // First compare by sortPriority (lower = higher priority)
404 const priorityA = a.sortPriority ?? 100;
405 const priorityB = b.sortPriority ?? 100;
406 if (priorityA !== priorityB) {
407 return priorityA - priorityB;
408 }
409 // Then by fuzzy score or alphabetical
410 if (this.matchType == 'fuzzy') {
411 return this.fuzzyScoreCompare(a, b);
412 }
413 return a.name.localeCompare(b.name);
414 });
398415
399416
400417
@@ -430,7 +447,7 @@ export class AutoComplete {
430 } else if (!this.isReplaceable && this.result.length > 1) {447 } else if (!this.isReplaceable && this.result.length > 1) {
431 return this.hide();448 return this.hide();
432 }449 }
433 this.selectedItem = this.result[0];450 this.selectedItem = this.selectDefaultItem(this.result);
434 this.isActive = true;451 this.isActive = true;
435 this.wasForced = isForced;452 this.wasForced = isForced;
436 this.renderDebounced();453 this.renderDebounced();
@@ -588,7 +605,22 @@ export class AutoComplete {
588 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {605 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
589 return this.hide();606 return this.hide();
590 }607 }
591 const left = Math.max(rect.left, location.left) - layerRect.left;608 let left = Math.max(rect.left, location.left) - layerRect.left;
609
610 // Check if the autocomplete list is constrained by the right edge of the viewport.
611 // If so, adjust the details panel position to align with the actual list position.
612 // Only do this when the list is actually visible (isReplaceable).
613 if (this.isReplaceable) {
614 const listRect = this.dom.getBoundingClientRect();
615 const listActualLeft = listRect.left - layerRect.left;
616 const isConstrainedRight = listActualLeft < left - 5; // 5px tolerance
617
618 if (isConstrainedRight) {
619 // Use the actual list position instead of cursor position
620 left = listActualLeft;
621 }
622 }
623
592 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);624 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
593 if (this.isReplaceable) {625 if (this.isReplaceable) {
594 this.detailsWrap.classList.remove('full');626 this.detailsWrap.classList.remove('full');
@@ -680,8 +712,10 @@ export class AutoComplete {
680 */712 */
681 async select() {713 async select() {
682 if (this.isReplaceable && this.selectedItem.value !== null) {714 if (this.isReplaceable && this.selectedItem.value !== null) {
683 this.textarea.value = `${this.text.slice(0, this.effectiveParserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;715 // Apply per-option replacement offset (e.g., for closing tags that need to replace leading whitespace)
684 this.textarea.selectionStart = this.effectiveParserResult.start + this.selectedItem.replacer.length;716 const effectiveStart = this.effectiveParserResult.start + (this.selectedItem.replacementStartOffset ?? 0);
717 this.textarea.value = `${this.text.slice(0, effectiveStart)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;
718 this.textarea.selectionStart = effectiveStart + this.selectedItem.replacer.length;
685 this.textarea.selectionEnd = this.textarea.selectionStart;719 this.textarea.selectionEnd = this.textarea.selectionStart;
686 this.show(false, false, true);720 this.show(false, false, true);
687 } else {721 } else {
@@ -697,6 +731,24 @@ export class AutoComplete {
697731
698732
699 /**733 /**
734 * Select the default item for the autocomplete list.
735 * Selects the first selectable item if any is present, or falls back to the last item.
736 * (To preserve context of where we are with multiple non-selectable options, if they are present for info)
737 * @param {AutoCompleteOption[]} result The list of autocomplete options.
738 * @returns {AutoCompleteOption} The item to select.
739 */
740 selectDefaultItem(result) {
741 if (result.length === 0) return null;
742
743 // Find first selectable item
744 const firstSelectable = result.find(it => it.isSelectable);
745 if (firstSelectable) return firstSelectable;
746
747 // Fall back to last item
748 return result[result.length - 1];
749 }
750
751 /**
700 * Mark the item at newIdx in the autocomplete list as selected.752 * Mark the item at newIdx in the autocomplete list as selected.
701 * @param {number} newIdx753 * @param {number} newIdx
702 */754 */
public/scripts/autocomplete/AutoCompleteNameResultBase.js+2 -2
@@ -24,7 +24,7 @@ export class AutoCompleteNameResultBase {
24 this.start = start;24 this.start = start;
25 this.optionList = optionList;25 this.optionList = optionList;
26 this.canBeQuoted = canBeQuoted;26 this.canBeQuoted = canBeQuoted;
27 this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;27 if (makeNoMatchText) this.makeNoMatchText = makeNoMatchText;
28 this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;28 if (makeNoOptionsText) this.makeNoOptionsText = makeNoOptionsText;
29 }29 }
30}30}
public/scripts/autocomplete/AutoCompleteOption.js+16 -0
@@ -13,6 +13,22 @@ export class AutoCompleteOption {
13 /** @type {(input:string)=>boolean} */ matchProvider;13 /** @type {(input:string)=>boolean} */ matchProvider;
14 /** @type {(input:string)=>string} */ valueProvider;14 /** @type {(input:string)=>string} */ valueProvider;
15 /** @type {boolean} */ makeSelectable = false;15 /** @type {boolean} */ makeSelectable = false;
16 /** @type {boolean} */ forceFullNameMatch = false;
17
18 /**
19 * Offset to adjust the replacement start position.
20 * Negative values start replacement earlier (e.g., -2 to include 2 chars before normal start).
21 * Used by closing tag autocomplete to replace leading whitespace.
22 * @type {number}
23 */
24 replacementStartOffset = 0;
25
26 /**
27 * Priority for sorting. Lower values = higher priority (sorted first).
28 * Default is 100 (normal priority). Use lower values for items that should appear at the top.
29 * @type {number}
30 */
31 sortPriority = 100;
1632
1733
18 /**34 /**
public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js+1658 -46
@@ -9,8 +9,11 @@ import {
9 createSourceIndicator,9 createSourceIndicator,
10 createAliasIndicator,10 createAliasIndicator,
11 renderMacroDetails,11 renderMacroDetails,
12} from '../macros/MacroBrowser.js';12} from '../macros/engine/MacroBrowser.js';
13import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';13import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';
14import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js';
15import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js';
16import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js';
1417
15/** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */18/** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */
1619
@@ -19,9 +22,46 @@ import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js'
19 * @typedef {Object} MacroAutoCompleteContext22 * @typedef {Object} MacroAutoCompleteContext
20 * @property {string} fullText - The full macro text being typed (without {{ }}).23 * @property {string} fullText - The full macro text being typed (without {{ }}).
21 * @property {number} cursorOffset - Cursor position within the macro text.24 * @property {number} cursorOffset - Cursor position within the macro text.
25 * @property {string} paddingBefore - Padding before the macro identifier/flags.
22 * @property {string} identifier - The macro identifier (name).26 * @property {string} identifier - The macro identifier (name).
27 * @property {number} identifierStart - Start position of the identifier within the macro text.
28 * @property {string[]} flags - Array of flag symbols typed (e.g., ['!', '?']).
29 * @property {string|null} currentFlag - The flag symbol cursor is currently on (last typed flag), or null.
30 * @property {boolean} isInFlagsArea - Whether cursor is in the flags area (before identifier starts).
23 * @property {string[]} args - Array of arguments typed so far.31 * @property {string[]} args - Array of arguments typed so far.
24 * @property {number} currentArgIndex - Index of the argument being typed (-1 if on identifier).32 * @property {number} currentArgIndex - Index of the argument being typed (-1 if on identifier).
33 * @property {boolean} isTypingSeparator - Whether cursor is on a partial separator (single ':').
34 * @property {boolean} isTypingClosingBrace - Whether cursor is typing the first closing brace on a standalone macro.
35 * @property {boolean} hasSpaceAfterIdentifier - Whether there's a space after the identifier (for space-separated args).
36 * @property {boolean} hasSpaceArgContent - Whether there's actual content after the space (not just whitespace).
37 * @property {number} separatorCount - Number of '::' separators found.
38 * @property {boolean} [isInScopedContent] - Whether cursor is in scoped content (after }} but before closing tag).
39 * @property {boolean} [isScopedContentOptional] - Whether the scoped content is optional (for display purposes).
40 * @property {string} [scopedMacroName] - Name of the scoped macro if in scoped content.
41 * @property {boolean} isVariableShorthand - Whether this is a variable shorthand (starts with . or $).
42 * @property {'.'|'$'|null} variablePrefix - The variable prefix (. for local, $ for global), or null.
43 * @property {string} variableName - The variable name being typed (after the prefix).
44 * @property {number} variableNameEnd - The end of the variable name (for partial matches).
45 * @property {string|null} variableOperator - The operator typed (=, ++, --, +=), or null.
46 * @property {number} variableOperatorEnd - The end of the variable operator (for partial matches).
47 * @property {string} variableValue - The value after the operator (for = and +=).
48 * @property {boolean} isTypingVariableName - Whether cursor is in the variable name area.
49 * @property {boolean} isTypingOperator - Whether cursor is at/after variable name, ready for operator.
50 * @property {boolean} isTypingValue - Whether cursor is after an operator that requires a value.
51 * @property {boolean} [hasInvalidTrailingChars] - Whether there are invalid characters after the variable name.
52 * @property {string} [invalidTrailingChars] - The invalid trailing characters (for error display).
53 * @property {string} [partialOperator] - Partial operator prefix being typed ('+' or '-').
54 * @property {boolean} [isOperatorComplete] - Whether a complete operator (++ or --) was typed that doesn't need a value.
55 */
56
57/**
58 * @typedef {Object} EnhancedMacroAutoCompleteOptions
59 * @property {boolean} [noBraces=false] - If true, display without {{ }} braces (for use as values, e.g., in {{if}} conditions).
60 * @property {string} [paddingAfter=''] - Whitespace to add before closing }} (for matching opening whitespace style).
61 * @property {boolean} [closeWithBraces=false] - If true, the completion will add }} to close the macro.
62 * @property {string[]} [flags=[]] - The currently already written flags for this autocomplete.
63 * @property {string} [currentFlag] - The current flag that is present, if any.
64 * @property {string} [fullText] - The currently written full text.
25 */65 */
2666
27export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {67export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
@@ -31,17 +71,62 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
31 /** @type {MacroAutoCompleteContext|null} */71 /** @type {MacroAutoCompleteContext|null} */
32 #context = null;72 #context = null;
3373
74 /** @type {EnhancedMacroAutoCompleteOptions|null} */
75 #options = null;
76
77 /** @type {boolean} */
78 #noBraces = false;
79
80 /** @type {string} */
81 #paddingAfter = '';
82
34 /**83 /**
35 * @param {MacroDefinition} macro - The macro definition from MacroRegistry.84 * @param {MacroDefinition} macro - The macro definition from MacroRegistry.
36 * @param {MacroAutoCompleteContext} [context] - Optional context for argument hints.85 * @param {MacroAutoCompleteContext|EnhancedMacroAutoCompleteOptions|null} [contextOrOptions] - Context for argument hints, or options object.
37 */86 */
38 constructor(macro, context = null) {87 constructor(macro, contextOrOptions = null) {
39 // Use the macro name as the autocomplete key88 // Use the macro name as the autocomplete key
40 super(macro.name, enumIcons.macro);89 super(macro.name, enumIcons.macro);
41 this.#macro = macro;90 this.#macro = macro;
42 this.#context = context;91
92 // Detect if second argument is context or options
93 // Context has 'identifier' property, options may have 'noBraces'
94 if (contextOrOptions && typeof contextOrOptions === 'object') {
95 if ('noBraces' in contextOrOptions || 'paddingAfter' in contextOrOptions || 'closeWithBraces' in contextOrOptions) {
96 // It's an options object
97 this.#options = /** @type {EnhancedMacroAutoCompleteOptions} */ (contextOrOptions);
98 this.#noBraces = this.#options.noBraces ?? false;
99 this.#paddingAfter = this.#options.paddingAfter ?? '';
100
101 // If noBraces mode with closeWithBraces, complete with name + padding + }}
102 if (this.#options.closeWithBraces) {
103 this.valueProvider = () => `${macro.name}${this.#paddingAfter}}}`;
104 this.makeSelectable = true;
105 }
106 } else {
107 // It's a context object
108 this.#context = /** @type {MacroAutoCompleteContext} */ (contextOrOptions);
109 }
110 }
111
43 // nameOffset = 2 to skip the {{ prefix in the display (formatMacroSignature includes braces)112 // nameOffset = 2 to skip the {{ prefix in the display (formatMacroSignature includes braces)
44 this.nameOffset = 2;113 // When noBraces is true, nameOffset = 0 since we don't show braces
114 this.nameOffset = this.#noBraces ? 0 : 2;
115
116 // For macros that take no arguments, auto-complete with closing }} (unless already set by options)
117 if (!this.valueProvider) {
118 const takesNoArgs = macro.minArgs === 0 && macro.maxArgs === 0 && macro.list === null;
119 if (takesNoArgs) {
120 this.valueProvider = () => `${macro.name}${this.#paddingAfter}}}`;
121 this.makeSelectable = true; // Required when using valueProvider
122 }
123 }
124
125 // {{//}} needs special handling. If we autocomplete right after **one** slash is already typed, we need to replace that, as it's treated as a flag otherwise.
126 const fullText = this.#options?.fullText ?? this.#context?.fullText ?? '';
127 if (macro.name === '//' && fullText.endsWith('/')) {
128 this.replacementStartOffset = (this.replacementStartOffset ?? 0) - 1; // Cut the leading slash
129 }
45 }130 }
46131
47 /** @returns {MacroDefinition} */132 /** @returns {MacroDefinition} */
@@ -74,8 +159,9 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
74 const nameEl = document.createElement('span');159 const nameEl = document.createElement('span');
75 nameEl.classList.add('name', 'monospace');160 nameEl.classList.add('name', 'monospace');
76161
77 // Build signature with individual character spans (includes {{ }})162 // Build signature with individual character spans
78 const sigText = formatMacroSignature(this.#macro);163 // When noBraces is true, show just the macro name without {{ }}
164 const sigText = this.#noBraces ? this.#macro.name : formatMacroSignature(this.#macro);
79 for (const char of sigText) {165 for (const char of sigText) {
80 const span = document.createElement('span');166 const span = document.createElement('span');
81 span.textContent = char;167 span.textContent = char;
@@ -121,17 +207,36 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
121 renderDetails() {207 renderDetails() {
122 const frag = document.createDocumentFragment();208 const frag = document.createDocumentFragment();
123209
210 // Check for arity warnings
211 const warning = this.#getArityWarning();
212 if (warning) {
213 const warningEl = this.#renderWarning(warning);
214 frag.append(warningEl);
215 }
216
217 // Show scoped content info banner if we're in scoped content
218 if (this.#context?.isInScopedContent) {
219 const scopedInfo = this.#renderScopedContentInfo();
220 if (scopedInfo) frag.append(scopedInfo);
221 }
222
124 // Determine current argument index for highlighting223 // Determine current argument index for highlighting
125 const currentArgIndex = this.#context?.currentArgIndex ?? -1;224 const currentArgIndex = this.#context?.currentArgIndex ?? -1;
126225
226 // For most warnings, we can still highlight which argument we are currently at.
227 // This even goes for "too many arguments" when navigating the cursor back to
228 // a valid argument.
229 // Extend this in the future, if *some* warnings don't make sense to still highlight args.
230 const hightlightArgsHint = currentArgIndex >= 0;
231
127 // Render argument hint banner if we're typing an argument232 // Render argument hint banner if we're typing an argument
128 if (currentArgIndex >= 0) {233 if (hightlightArgsHint && currentArgIndex >= 0) {
129 const hint = this.#renderArgumentHint();234 const hint = this.#renderArgumentHint();
130 if (hint) frag.append(hint);235 if (hint) frag.append(hint);
131 }236 }
132237
133 // Reuse MacroBrowser's renderMacroDetails with options238 // Reuse MacroBrowser's renderMacroDetails with options
134 const details = renderMacroDetails(this.#macro, { currentArgIndex });239 const details = renderMacroDetails(this.#macro, { currentArgIndex: hightlightArgsHint ? currentArgIndex : -1 });
135240
136 // Add class for autocomplete-specific styling overrides241 // Add class for autocomplete-specific styling overrides
137 details.classList.add('macro-ac-details');242 details.classList.add('macro-ac-details');
@@ -141,6 +246,113 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
141 }246 }
142247
143 /**248 /**
249 * Checks for arity-related warnings based on the current context.
250 * @returns {string|null} Warning message, or null if no warning.
251 */
252 #getArityWarning() {
253 if (!this.#context) return null;
254
255 const argCount = this.#context.args.length;
256 const maxArgs = this.#macro.maxArgs;
257 //const minArgs = this.#macro.minArgs;
258 const hasList = this.#macro.list !== null;
259
260 // Check for too many arguments (only if no list args)
261 if (!hasList && argCount > maxArgs) {
262 return `Too many arguments: this macro accepts ${maxArgs === 0 ? 'no arguments' : `up to ${maxArgs} argument${maxArgs === 1 ? '' : 's'}`}, but ${argCount} provided.`;
263 }
264
265 // Check for space-separated arg on macro that doesn't support it
266 // Space-separated syntax provides 1 arg; with scoped content you can provide a 2nd arg
267 // So it's valid for macros with maxArgs <= 2 (or with list args)
268 if (this.#context.hasSpaceArgContent) {
269 if (maxArgs === 0 && !hasList) {
270 return 'This macro does not accept any arguments. Remove the space or use a different macro.';
271 }
272 if (!hasList && maxArgs > 2) {
273 return `Space-separated syntax only works for macros with up to 2 arguments. Use :: separators instead: {{${this.#macro.name}::arg1::arg2}}`;
274 }
275 }
276
277 // Check if trying to add args to a no-arg macro via ::
278 // List-arg macros can accept args even if maxArgs === 0
279 if (this.#context.separatorCount > 0 && maxArgs === 0 && !hasList) {
280 return 'This macro does not accept any arguments.';
281 }
282
283 // Check list bounds (min/max) if the macro has a list with constraints
284 if (hasList && typeof this.#macro.list === 'object') {
285 const listItemCount = Math.max(0, argCount - maxArgs);
286 const listMin = this.#macro.list.min ?? 0;
287 const listMax = this.#macro.list.max ?? null;
288
289 if (listItemCount < listMin) {
290 const needed = listMin - listItemCount;
291 return `Not enough list items yet: this macro requires at least ${listMin} item${listMin === 1 ? '' : 's'}, but only ${listItemCount} provided. Add ${needed} more.`;
292 }
293
294 if (listMax !== null && listItemCount > listMax) {
295 return `Too many list items: this macro accepts at most ${listMax} item${listMax === 1 ? '' : 's'}, but ${listItemCount} provided.`;
296 }
297 }
298
299 return null;
300 }
301
302 /**
303 * Renders a warning banner.
304 * @param {string} message - The warning message.
305 * @returns {HTMLElement}
306 */
307 #renderWarning(message) {
308 const warning = document.createElement('div');
309 warning.classList.add('macro-ac-warning');
310
311 const icon = document.createElement('i');
312 icon.classList.add('fa-solid', 'fa-triangle-exclamation');
313 warning.append(icon);
314
315 const text = document.createElement('span');
316 text.textContent = message;
317 warning.append(text);
318
319 return warning;
320 }
321
322 /**
323 * Renders the scoped content info banner.
324 * Shows when cursor is inside scoped content of an unclosed macro.
325 * @returns {HTMLElement|null}
326 */
327 #renderScopedContentInfo() {
328 if (!this.#context?.isInScopedContent) return null;
329
330 const info = document.createElement('div');
331 info.classList.add('macro-ac-scoped-info');
332
333 // If the scoped content is optional, show a prominent OPTIONAL badge
334 if (this.#context.isScopedContentOptional) {
335 const optionalBadge = document.createElement('span');
336 optionalBadge.classList.add('macro-ac-optional-badge');
337 optionalBadge.textContent = 'OPTIONAL';
338 info.append(optionalBadge);
339 }
340
341 const icon = document.createElement('i');
342 icon.classList.add('fa-solid', 'fa-layer-group');
343 info.append(icon);
344
345 const text = document.createElement('span');
346 const closingHint = this.#context.isScopedContentOptional
347 ? `Can optionally close with <code>{{/${this.#context.scopedMacroName}}}</code>`
348 : `Close with <code>{{/${this.#context.scopedMacroName}}}</code>`;
349 text.innerHTML = `Typing <strong>scoped content</strong> for <code>{{${this.#context.scopedMacroName}}}</code>. ${closingHint}`;
350 info.append(text);
351
352 return info;
353 }
354
355 /**
144 * Renders the current argument hint banner.356 * Renders the current argument hint banner.
145 * @returns {HTMLElement|null}357 * @returns {HTMLElement|null}
146 */358 */
@@ -163,8 +375,23 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
163 if (isListArg) {375 if (isListArg) {
164 // List argument hint376 // List argument hint
165 const listIndex = argIndex - this.#macro.maxArgs + 1;377 const listIndex = argIndex - this.#macro.maxArgs + 1;
378 const totalListItems = this.#context.args.length - this.#macro.maxArgs;
379
166 const text = document.createElement('span');380 const text = document.createElement('span');
167 text.innerHTML = `<strong>List item ${listIndex}</strong>`;381 text.innerHTML = `<strong>List item ${listIndex}</strong>${(listIndex < totalListItems ? ` (of ${totalListItems})` : '')}`;
382
383 const listInfo = document.createElement('span');
384 listInfo.classList.add('macro-ac-arg-hint-small');
385 const minMax = [];
386 if (this.#macro.list.min > 0) minMax.push(`min: ${this.#macro.list.min}`);
387 if (this.#macro.list.max !== null) minMax.push(`max: ${this.#macro.list.max}`);
388 if (minMax.length > 0) {
389 listInfo.textContent = ` (list, ${minMax.join(', ')})`;
390 } else {
391 listInfo.textContent = ' (variable-length list)';
392 }
393 text.appendChild(listInfo);
394
168 hint.append(text);395 hint.append(text);
169 } else {396 } else {
170 // Unnamed argument hint (required or optional)397 // Unnamed argument hint (required or optional)
@@ -210,51 +437,1436 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
210}437}
211438
212/**439/**
213 * Parses the macro text to determine current argument context.440 * Autocomplete option for macro execution flags.
214 * @param {string} macroText - The text inside {{ }}, e.g., "roll::1d20" or "random::a::b".441 * Shows flag symbol, name, and description.
215 * @param {number} cursorOffset - Cursor position within macroText.442 * Uses default AutoCompleteOption rendering for consistent styling.
216 * @returns {MacroAutoCompleteContext}
217 */443 */
218export function parseMacroContext(macroText, cursorOffset) {444export class MacroFlagAutoCompleteOption extends AutoCompleteOption {
219 const parts = [];445 /** @type {import('../macros/engine/MacroFlags.js').MacroFlagDefinition} */
220 let currentPart = '';446 #flagDef;
221 let partStart = 0;
222 let i = 0;
223447
224 while (i < macroText.length) {448 /**
225 if (macroText[i] === ':' && macroText[i + 1] === ':') {449 * @param {import('../macros/engine/MacroFlags.js').MacroFlagDefinition} flagDef - The flag definition.
226 parts.push({ text: currentPart, start: partStart, end: i });450 */
227 currentPart = '';451 constructor(flagDef) {
228 i += 2;452 // Use the flag symbol as the name, with a flag icon
229 partStart = i;453 // Display name includes both symbol and name for clarity
454 super(flagDef.type, '🚩');
455 this.#flagDef = flagDef;
456 }
457
458 /** @returns {import('../macros/engine/MacroFlags.js').MacroFlagDefinition} */
459 get flagDefinition() {
460 return this.#flagDef;
461 }
462
463 /**
464 * Renders the autocomplete list item for this flag.
465 * Uses the same structure as other autocomplete options for consistent styling.
466 * @returns {HTMLElement}
467 */
468 renderItem() {
469 // Use base class makeItem for consistent styling
470 const li = this.makeItem(
471 `${this.#flagDef.type} ${this.#flagDef.name}`, // Display: "? Optional"
472 '🚩',
473 true, // noSlash
474 [], // namedArguments
475 [], // unnamedArguments
476 'void', // returnType
477 this.#flagDef.description + (this.#flagDef.implemented ? '' : ' (planned)'), // helpString
478 );
479 li.setAttribute('data-name', this.name);
480 li.setAttribute('data-option-type', 'flag');
481 return li;
482 }
483
484 /**
485 * Renders the details panel for this flag.
486 * @returns {DocumentFragment}
487 */
488 renderDetails() {
489 const frag = document.createDocumentFragment();
490
491 const details = document.createElement('div');
492 details.classList.add('macro-flag-details');
493
494 // Header with flag symbol and name
495 const header = document.createElement('h3');
496 header.classList.add('macro-flag-details-header');
497 header.innerHTML = `<code>${this.#flagDef.type}</code> ${this.#flagDef.name} Flag`;
498 details.append(header);
499
500 // Description
501 const desc = document.createElement('p');
502 desc.classList.add('macro-flag-details-desc');
503 desc.textContent = this.#flagDef.description;
504 details.append(desc);
505
506 // Status
507 const status = document.createElement('p');
508 status.classList.add('macro-flag-details-status');
509 status.innerHTML = `<strong>Status:</strong> ${this.#flagDef.implemented ? 'Implemented' : 'Planned for future release'}`;
510 details.append(status);
511
512 // Parser effect note
513 if (this.#flagDef.affectsParser) {
514 const parserNote = document.createElement('p');
515 parserNote.classList.add('macro-flag-details-note');
516 parserNote.innerHTML = '<em>This flag affects how the macro is parsed.</em>';
517 details.append(parserNote);
518 }
519
520 frag.append(details);
521 return frag;
522 }
523}
524
525/**
526 * Enum of variable shorthand prefix types.
527 * @readonly
528 * @enum {string}
529 */
530export const VariableShorthandType = Object.freeze({
531 /** Local variable prefix (`.`) */
532 LOCAL: '.',
533 /** Global variable prefix (`$`) */
534 GLOBAL: '$',
535});
536
537/**
538 * @typedef {Object} VariableShorthandDefinition
539 * @property {VariableShorthandType} type - The prefix symbol.
540 * @property {string} name - Human-readable name.
541 * @property {string} description - Description of what this prefix does.
542 * @property {string[]} operations - List of supported operations.
543 */
544
545/**
546 * Definitions for variable shorthand prefixes.
547 * @type {Map<string, VariableShorthandDefinition>}
548 */
549export const VariableShorthandDefinitions = new Map([
550 [VariableShorthandType.LOCAL, {
551 type: VariableShorthandType.LOCAL,
552 name: 'Local Variable',
553 description: 'Access or modify a local variable (scoped to current chat).',
554 operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)', 'greater than (>)', 'greater than or equal (>=)', 'less than (<)', 'less than or equal (<=)'],
555 }],
556 [VariableShorthandType.GLOBAL, {
557 type: VariableShorthandType.GLOBAL,
558 name: 'Global Variable',
559 description: 'Access or modify a global variable (shared across all chats).',
560 operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)', 'greater than (>)', 'greater than or equal (>=)', 'less than (<)', 'less than or equal (<=)'],
561 }],
562]);
563
564/**
565 * Set of valid variable shorthand prefix symbols.
566 * @type {Set<string>}
567 */
568export const ValidVariableShorthandSymbols = new Set(Object.values(VariableShorthandType));
569
570/**
571 * Regex pattern for valid variable shorthand names.
572 * Must start with a letter, can contain word chars, underscores and hyphens, but must not end with an underscore or hyphen.
573 * Examples: myVar, my-var, my_var, myVar123, my-long-var-name
574 * Invalid: my-, my--, -var, 123var
575 * @type {RegExp}
576 */
577const VARIABLE_SHORTHAND_NAME_PATTERN = new RegExp(`^${MACRO_VARIABLE_SHORTHAND_PATTERN.source}`);
578
579/**
580 * Checks if a variable name is valid for use with variable shorthand syntax.
581 * @param {string} name - The variable name to validate.
582 * @returns {boolean} True if the name is valid for shorthand syntax.
583 */
584export function isValidVariableShorthandName(name) {
585 if (!name || typeof name !== 'string') return false;
586 return VARIABLE_SHORTHAND_NAME_PATTERN.test(name);
587}
588
589/**
590 * Autocomplete option for variable shorthand prefixes.
591 * Shows prefix symbol, name, and description.
592 * This provides entry into the variable shorthand syntax ({{.varName}} or {{$varName}}).
593 */
594export class VariableShorthandAutoCompleteOption extends AutoCompleteOption {
595 /** @type {VariableShorthandDefinition} */
596 #varDef;
597
598 /**
599 * @param {VariableShorthandDefinition} varDef - The variable shorthand definition.
600 */
601 constructor(varDef) {
602 // Use the prefix symbol as the name, with a variable icon
603 super(varDef.type, '📦');
604 this.#varDef = varDef;
605 }
606
607 /** @returns {VariableShorthandDefinition} */
608 get variableDefinition() {
609 return this.#varDef;
610 }
611
612 /**
613 * Renders the autocomplete list item for this variable shorthand.
614 * @returns {HTMLElement}
615 */
616 renderItem() {
617 const li = this.makeItem(
618 `${this.#varDef.type} ${this.#varDef.name}`,
619 '📦',
620 true, // noSlash
621 [], // namedArguments
622 [], // unnamedArguments
623 'any', // returnType
624 this.#varDef.description,
625 );
626 li.setAttribute('data-name', this.name);
627 li.setAttribute('data-option-type', 'variable-shorthand');
628 return li;
629 }
630
631 /**
632 * Renders the details panel for this variable shorthand.
633 * @returns {DocumentFragment}
634 */
635 renderDetails() {
636 const frag = document.createDocumentFragment();
637
638 const details = document.createElement('div');
639 details.classList.add('macro-variable-details');
640
641 // Header with prefix symbol and name
642 const header = document.createElement('h3');
643 header.classList.add('macro-variable-details-header');
644 header.innerHTML = `<code>${this.#varDef.type}</code> ${this.#varDef.name}`;
645 details.append(header);
646
647 // Description
648 const desc = document.createElement('p');
649 desc.classList.add('macro-variable-details-desc');
650 desc.textContent = this.#varDef.description;
651 details.append(desc);
652
653 // Supported operations
654 const opsHeader = document.createElement('p');
655 opsHeader.innerHTML = '<strong>Supported Operations:</strong>';
656 details.append(opsHeader);
657
658 const opsList = document.createElement('ul');
659 opsList.classList.add('macro-variable-details-ops');
660 for (const op of this.#varDef.operations) {
661 const li = document.createElement('li');
662 li.textContent = op;
663 opsList.append(li);
664 }
665 details.append(opsList);
666
667 // Examples
668 const exampleHeader = document.createElement('p');
669 exampleHeader.innerHTML = '<strong>Examples:</strong>';
670 details.append(exampleHeader);
671
672 const exampleList = document.createElement('ul');
673 exampleList.classList.add('macro-variable-details-examples');
674 const prefix = this.#varDef.type;
675 const examples = [
676 `{{${prefix}myvar}} - Get variable value`,
677 `{{${prefix}myvar = value}} - Set variable (returns nothing)`,
678 `{{${prefix}counter++}} - Increment and get value`,
679 `{{${prefix}counter--}} - Decrement and get value`,
680 `{{${prefix}myvar += text}} - Append/add (returns nothing)`,
681 `{{${prefix}score -= 5}} - Subtract (returns nothing)`,
682 `{{${prefix}myvar || default}} - Get with fallback if falsy`,
683 `{{${prefix}myvar ?? default}} - Get with fallback if undefined`,
684 `{{${prefix}myvar ||= value}} - Set if falsy, get value`,
685 `{{${prefix}myvar ??= value}} - Set if undefined, get value`,
686 `{{${prefix}myvar == test}} - Compare (returns true/false)`,
687 `{{${prefix}myvar != test}} - Compare not equal (returns true/false)`,
688 `{{${prefix}score > 10}} - Greater than (numeric, returns true/false)`,
689 `{{${prefix}score >= 10}} - Greater than or equal (numeric)`,
690 `{{${prefix}score < 10}} - Less than (numeric, returns true/false)`,
691 `{{${prefix}score <= 10}} - Less than or equal (numeric)`,
692 ];
693 for (const ex of examples) {
694 const li = document.createElement('li');
695 li.innerHTML = `<code>${ex.split(' - ')[0]}</code> - ${ex.split(' - ')[1]}`;
696 exampleList.append(li);
697 }
698 details.append(exampleList);
699
700 frag.append(details);
701 return frag;
702 }
703}
704
705/**
706 * Autocomplete option for a specific variable name.
707 * Shows variable name with scope indicator (local/global).
708 */
709export class VariableNameAutoCompleteOption extends AutoCompleteOption {
710 /** @type {string} */
711 #varName;
712
713 /** @type {'local'|'global'} */
714 #scope;
715
716 /** @type {boolean} */
717 #isNewVariable;
718
719 /** @type {boolean} */
720 #isInvalidName;
721
722 /**
723 * @param {string} varName - The variable name.
724 * @param {'local'|'global'} scope - Whether this is a local or global variable.
725 * @param {boolean} [isNewVariable=false] - Whether this is a "create new variable" option.
726 * @param {boolean} [isInvalidName=false] - Whether this name is invalid for shorthand syntax.
727 */
728 constructor(varName, scope, isNewVariable = false, isInvalidName = false) {
729 const icon = scope === 'local' ? 'L' : 'G';
730 super(varName, icon);
731 this.#varName = varName;
732 this.#scope = scope;
733 this.#isNewVariable = isNewVariable;
734 this.#isInvalidName = isInvalidName;
735 }
736
737 /** @returns {string} */
738 get variableName() {
739 return this.#varName;
740 }
741
742 /** @returns {'local'|'global'} */
743 get scope() {
744 return this.#scope;
745 }
746
747 /** @returns {boolean} */
748 get isNewVariable() {
749 return this.#isNewVariable;
750 }
751
752 /** @returns {boolean} */
753 get isInvalidName() {
754 return this.#isInvalidName;
755 }
756
757 /**
758 * Renders the autocomplete list item for this variable.
759 * @returns {HTMLElement}
760 */
761 renderItem() {
762 const scopeLabel = this.#scope === 'local' ? 'Local' : 'Global';
763 let description;
764 if (this.#isInvalidName) {
765 description = '⚠️ Invalid variable name for shorthand';
766 } else if (this.#isNewVariable) {
767 description = `Define new ${scopeLabel.toLowerCase()} variable`;
230 } else {768 } else {
231 currentPart += macroText[i];769 description = `${scopeLabel} variable`;
232 i++;770 }
771
772 const li = this.makeItem(
773 this.#varName,
774 this.typeIcon,
775 true, // noSlash
776 [], // namedArguments
777 [], // unnamedArguments
778 'any', // returnType
779 description,
780 );
781 li.setAttribute('data-name', this.name);
782 li.setAttribute('data-option-type', 'variable-name');
783 if (this.#isNewVariable) {
784 li.classList.add('variable-new');
785 }
786 if (this.#isInvalidName) {
787 li.classList.add('variable-invalid');
233 }788 }
789 return li;
234 }790 }
235 // Push the last part
236 parts.push({ text: currentPart, start: partStart, end: macroText.length });
237791
238 // Determine which part the cursor is in792 /**
239 let currentArgIndex = -1;793 * Renders the details panel for this variable.
240 for (let idx = 0; idx < parts.length; idx++) {794 * @returns {DocumentFragment}
241 const part = parts[idx];795 */
242 if (cursorOffset >= part.start && cursorOffset <= part.end) {796 renderDetails() {
243 currentArgIndex = idx - 1; // -1 because first part is identifier797 const frag = document.createDocumentFragment();
244 break;798
799 const details = document.createElement('div');
800 details.classList.add('macro-variable-name-details');
801
802 const scopeLabel = this.#scope === 'local' ? 'Local' : 'Global';
803 const prefix = this.#scope === 'local' ? '.' : '$';
804
805 // Show big warning for invalid names
806 if (this.#isInvalidName) {
807 const warningBox = document.createElement('div');
808 warningBox.classList.add('variable-invalid-warning');
809 warningBox.style.cssText = 'background: #ff000033; border: 2px solid #ff0000; border-radius: 4px; padding: 10px; margin-bottom: 10px;';
810
811 const warningHeader = document.createElement('h3');
812 warningHeader.style.cssText = 'color: #ff6b6b; margin: 0 0 8px 0;';
813 warningHeader.textContent = '⚠️ Invalid Variable Name';
814 warningBox.append(warningHeader);
815
816 const warningText = document.createElement('p');
817 warningText.style.cssText = 'margin: 0 0 8px 0;';
818 warningText.innerHTML = `The name <code>${this.#varName}</code> cannot be used with variable shorthand syntax.`;
819 warningBox.append(warningText);
820
821 const rulesText = document.createElement('p');
822 rulesText.style.cssText = 'margin: 0; font-size: 0.9em;';
823 rulesText.innerHTML = '<strong>Valid names must:</strong><br>• Start with a letter (a-z, A-Z)<br>• Contain only letters, numbers, underscores, or hyphens<br>• Not end with an underscore or hyphen';
824 warningBox.append(rulesText);
825
826 details.append(warningBox);
827 frag.append(details);
828 return frag;
245 }829 }
830
831 // Header
832 const header = document.createElement('h3');
833 header.innerHTML = this.#isNewVariable
834 ? `<code>${prefix}${this.#varName}</code> (New ${scopeLabel} Variable)`
835 : `<code>${prefix}${this.#varName}</code> ${scopeLabel} Variable`;
836 details.append(header);
837
838 // Description
839 const desc = document.createElement('p');
840 const variableSuggestion = this.#scope === 'local'
841 ? 'Local variables are scoped to the current chat.'
842 : 'Global variables are shared across all chats.';
843 if (this.#isNewVariable) {
844 desc.textContent = `Creates a new ${scopeLabel.toLowerCase()} variable named "${this.#varName}". ${variableSuggestion}`;
845 } else {
846 desc.textContent = `Access or modify the ${scopeLabel.toLowerCase()} variable "${this.#varName}". ${variableSuggestion}`;
246 }847 }
848 details.append(desc);
849
850 // Usage examples
851 const usageHeader = document.createElement('p');
852 usageHeader.innerHTML = '<strong>Usage:</strong>';
853 details.append(usageHeader);
247854
248 // If cursor is after all parts (at the end), we're in the last arg855 const usageList = document.createElement('ul');
249 if (currentArgIndex === -1 && cursorOffset >= parts[parts.length - 1].end) {856 const examples = [
250 currentArgIndex = parts.length - 1;857 `{{${prefix}${this.#varName}}} - Get value`,
858 `{{${prefix}${this.#varName} = value}} - Set value`,
859 `{{${prefix}${this.#varName}++}} - Increment`,
860 `{{${prefix}${this.#varName}--}} - Decrement`,
861 `{{${prefix}${this.#varName} += text}} - Append/add`,
862 `{{${prefix}${this.#varName} -= 5}} - Subtract`,
863 `{{${prefix}${this.#varName} || default}} - Get with fallback if falsy`,
864 `{{${prefix}${this.#varName} ?? default}} - Get with fallback if undefined`,
865 `{{${prefix}${this.#varName} ||= value}} - Set if falsy, get value`,
866 `{{${prefix}${this.#varName} ??= value}} - Set if undefined, get value`,
867 `{{${prefix}${this.#varName} == test}} - Compare (returns true/false)`,
868 `{{${prefix}${this.#varName} != test}} - Compare not equal (returns true/false)`,
869 `{{${prefix}${this.#varName} > 10}} - Greater than (numeric)`,
870 `{{${prefix}${this.#varName} >= 10}} - Greater than or equal (numeric)`,
871 `{{${prefix}${this.#varName} < 10}} - Less than (numeric)`,
872 `{{${prefix}${this.#varName} <= 10}} - Less than or equal (numeric)`,
873 ];
874 for (const ex of examples) {
875 const li = document.createElement('li');
876 li.innerHTML = `<code>${ex.split(' - ')[0]}</code> - ${ex.split(' - ')[1]}`;
877 usageList.append(li);
251 }878 }
879 details.append(usageList);
252880
253 return {881 frag.append(details);
254 fullText: macroText,882 return frag;
255 cursorOffset,883 }
256 identifier: parts[0]?.text.trim() || '',884}
257 args: parts.slice(1).map(p => p.text),885
258 currentArgIndex,886/**
259 };887 * Checks if an operator is a short one that could be a prefix of a longer operator.
888 * For example, '>' is a prefix of '>=', '<' is a prefix of '<='.
889 * @param {string} op - The operator to check.
890 * @returns {boolean} True if the operator could be a prefix of a longer operator.
891 */
892function isShortOperatorPrefix(op) {
893 // These operators could have longer variants typed after them
894 const shortPrefixes = ['>', '<', '=', '|', '?', '+', '-', '!'];
895 return shortPrefixes.includes(op);
896}
897
898/**
899 * Variable shorthand operators with metadata.
900 * @type {Map<string, { symbol: string, name: string, description: string, needsValue: boolean }>}
901 */
902export const VariableOperatorDefinitions = new Map([
903 ['=', {
904 symbol: '=',
905 name: 'Set',
906 description: 'Set the variable to a new value. Returns nothing.',
907 needsValue: true,
908 }],
909 ['++', {
910 symbol: '++',
911 name: 'Increment',
912 description: 'Increment the variable by 1 (numeric). Returns the new value.',
913 needsValue: false,
914 }],
915 ['--', {
916 symbol: '--',
917 name: 'Decrement',
918 description: 'Decrement the variable by 1 (numeric). Returns the new value.',
919 needsValue: false,
920 }],
921 ['+=', {
922 symbol: '+=',
923 name: 'Add',
924 description: 'Add to the variable (numeric addition or string concatenation). Returns nothing.',
925 needsValue: true,
926 }],
927 ['-=', {
928 symbol: '-=',
929 name: 'Subtract',
930 description: 'Subtract a numeric value from the variable. Returns nothing.',
931 needsValue: true,
932 }],
933 ['||', {
934 symbol: '||',
935 name: 'Logical Or',
936 description: 'Return the fallback value if the variable is falsy, otherwise return the variable value.',
937 needsValue: true,
938 }],
939 ['??', {
940 symbol: '??',
941 name: 'Nullish Coalescing',
942 description: 'Return the fallback value only if the variable does not exist, otherwise return the variable value (even if falsy).',
943 needsValue: true,
944 }],
945 ['||=', {
946 symbol: '||=',
947 name: 'Logical Or Assign',
948 description: 'If the variable is falsy, set it to the value and return it; otherwise return the current value.',
949 needsValue: true,
950 }],
951 ['??=', {
952 symbol: '??=',
953 name: 'Nullish Coalescing Assign',
954 description: 'If the variable does not exist, set it to the value and return it; otherwise return the current value.',
955 needsValue: true,
956 }],
957 ['==', {
958 symbol: '==',
959 name: 'Equals',
960 description: 'Compare the variable value to another value. Returns "true" or "false".',
961 needsValue: true,
962 }],
963 ['!=', {
964 symbol: '!=',
965 name: 'Not Equals',
966 description: 'Compare the variable value to another value. Returns "true" if not equal, "false" if equal.',
967 needsValue: true,
968 }],
969 ['>', {
970 symbol: '>',
971 name: 'Greater Than',
972 description: 'Numeric comparison. Returns "true" if variable is greater than value, "false" otherwise.',
973 needsValue: true,
974 }],
975 ['>=', {
976 symbol: '>=',
977 name: 'Greater Than or Equal',
978 description: 'Numeric comparison. Returns "true" if variable is greater than or equal to value, "false" otherwise.',
979 needsValue: true,
980 }],
981 ['<', {
982 symbol: '<',
983 name: 'Less Than',
984 description: 'Numeric comparison. Returns "true" if variable is less than value, "false" otherwise.',
985 needsValue: true,
986 }],
987 ['<=', {
988 symbol: '<=',
989 name: 'Less Than or Equal',
990 description: 'Numeric comparison. Returns "true" if variable is less than or equal to value, "false" otherwise.',
991 needsValue: true,
992 }],
993]);
994
995/**
996 * Autocomplete option for a variable operator.
997 * Shows operator symbol, name, and description.
998 */
999export class VariableOperatorAutoCompleteOption extends AutoCompleteOption {
1000 /** @type {{ symbol: string, name: string, description: string, needsValue: boolean }} */
1001 #operatorDef;
1002
1003 /**
1004 * @param {{ symbol: string, name: string, description: string, needsValue: boolean }} operatorDef - The operator definition.
1005 */
1006 constructor(operatorDef) {
1007 super(operatorDef.symbol, '⚡');
1008 this.#operatorDef = operatorDef;
1009 }
1010
1011 /** @returns {{ symbol: string, name: string, description: string, needsValue: boolean }} */
1012 get operatorDefinition() {
1013 return this.#operatorDef;
1014 }
1015
1016 /**
1017 * Renders the autocomplete list item for this operator.
1018 * @returns {HTMLElement}
1019 */
1020 renderItem() {
1021 const li = this.makeItem(
1022 `${this.#operatorDef.symbol} ${this.#operatorDef.name}`,
1023 '⚡',
1024 true, // noSlash
1025 [], // namedArguments
1026 [], // unnamedArguments
1027 'void', // returnType
1028 this.#operatorDef.description,
1029 );
1030 li.setAttribute('data-name', this.name);
1031 li.setAttribute('data-option-type', 'variable-operator');
1032 return li;
1033 }
1034
1035 /**
1036 * Renders the details panel for this operator.
1037 * @returns {DocumentFragment}
1038 */
1039 renderDetails() {
1040 const frag = document.createDocumentFragment();
1041
1042 const details = document.createElement('div');
1043 details.classList.add('macro-variable-operator-details');
1044
1045 // Header
1046 const header = document.createElement('h3');
1047 header.innerHTML = `<code>${this.#operatorDef.symbol}</code> ${this.#operatorDef.name}`;
1048 details.append(header);
1049
1050 // Description
1051 const desc = document.createElement('p');
1052 desc.textContent = this.#operatorDef.description;
1053 details.append(desc);
1054
1055 // Value note
1056 const valueNote = document.createElement('p');
1057 valueNote.innerHTML = this.#operatorDef.needsValue
1058 ? '<em>This operator requires a value after it.</em>'
1059 : '<em>This operator does not take a value.</em>';
1060 details.append(valueNote);
1061
1062 frag.append(details);
1063 return frag;
1064 }
1065}
1066
1067/**
1068 * Non-selectable autocomplete option that shows context about the value being typed.
1069 * Displays info about what value is expected based on the operator.
1070 */
1071export class VariableValueContextAutoCompleteOption extends AutoCompleteOption {
1072 /** @type {{ symbol: string, name: string, description: string, needsValue: boolean }} */
1073 #operatorDef;
1074
1075 /** @type {string} */
1076 #currentValue;
1077
1078 /**
1079 * @param {{ symbol: string, name: string, description: string, needsValue: boolean }} operatorDef - The operator definition.
1080 * @param {string} [currentValue=''] - The value currently being typed.
1081 */
1082 constructor(operatorDef, currentValue = '') {
1083 super('value', '📝');
1084 this.#operatorDef = operatorDef;
1085 this.#currentValue = currentValue;
1086 this.forceFullNameMatch = true;
1087 }
1088
1089 /** @returns {{ symbol: string, name: string, description: string, needsValue: boolean }} */
1090 get operatorDefinition() {
1091 return this.#operatorDef;
1092 }
1093
1094 /**
1095 * Renders the autocomplete list item for this value context.
1096 * @returns {HTMLElement}
1097 */
1098 renderItem() {
1099 const li = this.makeItem(
1100 '<value>',
1101 '📝',
1102 true, // noSlash
1103 [], // namedArguments
1104 [], // unnamedArguments
1105 'any', // returnType
1106 `${this.#operatorDef.name} (${this.#operatorDef.symbol}) expects a value`,
1107 );
1108 li.setAttribute('data-name', this.name);
1109 li.setAttribute('data-option-type', 'variable-value-context');
1110 return li;
1111 }
1112
1113 /**
1114 * Renders the details panel for this value context.
1115 * @returns {DocumentFragment}
1116 */
1117 renderDetails() {
1118 const frag = document.createDocumentFragment();
1119
1120 const details = document.createElement('div');
1121 details.classList.add('macro-variable-value-context-details');
1122
1123 // Header
1124 const header = document.createElement('h3');
1125 header.innerHTML = `Value for <code>${this.#operatorDef.symbol}</code> (${this.#operatorDef.name})`;
1126 details.append(header);
1127
1128 // Description of what value is expected
1129 const desc = document.createElement('p');
1130 desc.textContent = this.#operatorDef.description;
1131 details.append(desc);
1132
1133 // Current value being typed
1134 if (this.#currentValue) {
1135 const currentNote = document.createElement('p');
1136 currentNote.innerHTML = `<em>Currently typing:</em> <code>${this.#currentValue}</code>`;
1137 details.append(currentNote);
1138 }
1139
1140 // Hint
1141 const hint = document.createElement('p');
1142 hint.classList.add('hint');
1143 hint.innerHTML = '<em>Type your value and close with <code>}}</code> to complete the macro.</em>';
1144 details.append(hint);
1145
1146 frag.append(details);
1147 return frag;
1148 }
1149}
1150
1151/**
1152 * Autocomplete option for closing a scoped macro.
1153 * Suggests {{/macroName}} to close an unclosed scoped macro.
1154 */
1155export class MacroClosingTagAutoCompleteOption extends AutoCompleteOption {
1156 /** @type {string} */
1157 #macroName;
1158
1159 /** @type {string} */
1160 #paddingBefore;
1161
1162 /** @type {string} */
1163 #paddingAfter;
1164
1165 /** @type {boolean} */
1166 #isOptional;
1167
1168 /** @type {number} */
1169 #nestingLevel;
1170
1171 /**
1172 * @param {string} macroName - The name of the macro to close.
1173 * @param {Object} [options] - Optional configuration.
1174 * @param {string} [options.paddingBefore=''] - Whitespace after {{ in opening tag (target padding).
1175 * @param {string} [options.paddingAfter=''] - Whitespace before }} in opening tag (target padding).
1176 * @param {string} [options.currentPadding=''] - Whitespace the user has already typed after {{.
1177 * @param {boolean} [options.isOptional=false] - Whether this closing tag is for an optional scope.
1178 * @param {number} [options.nestingLevel=0] - Nesting level (0 = innermost).
1179 */
1180 constructor(macroName, options = {}) {
1181 // The closing tag is what we're suggesting - use /macroName as the name for matching
1182 const closingTag = `/${macroName}`;
1183 super(closingTag, '{/');
1184 this.#macroName = macroName;
1185 this.#paddingBefore = options.paddingBefore ?? '';
1186 this.#paddingAfter = options.paddingAfter ?? '';
1187 this.#isOptional = options.isOptional ?? false;
1188 this.#nestingLevel = options.nestingLevel ?? 0;
1189
1190 // Calculate the replacement offset to replace any existing whitespace the user typed
1191 // This allows us to normalize the whitespace to match the opening tag's style
1192 const currentPadding = options.currentPadding ?? '';
1193 // Negative offset to start replacement earlier (eating the user's whitespace)
1194 this.replacementStartOffset = -currentPadding.length;
1195
1196 // Custom valueProvider to return the correct replacement text
1197 // Includes the target paddingBefore from the opening tag, replacing any user-typed whitespace
1198 this.valueProvider = () => {
1199 // Return: paddingBefore + /macroName + paddingAfter + }}
1200 return `${this.#paddingBefore}/${macroName}${this.#paddingAfter}}}`;
1201 };
1202
1203 // Make selectable so TAB completion works (valueProvider alone makes it non-selectable)
1204 this.makeSelectable = true;
1205
1206 // nameOffset = 2 to skip the {{ prefix in the display for fuzzy highlighting
1207 // The name is /macroName but display shows {{/macroName}}
1208 this.nameOffset = 2;
1209
1210 // Highest priority - closing tags should always appear at the very top
1211 this.sortPriority = 1;
1212 }
1213
1214 /** @returns {string} */
1215 get macroName() {
1216 return this.#macroName;
1217 }
1218
1219 /**
1220 * Renders the autocomplete list item for this closing tag.
1221 * Uses the same structure as other macro options for consistent styling.
1222 * @returns {HTMLElement}
1223 */
1224 renderItem() {
1225 const li = document.createElement('li');
1226 li.classList.add('item', 'macro-ac-item');
1227
1228 // Type icon (same column as other macros)
1229 const type = document.createElement('span');
1230 type.classList.add('type', 'monospace');
1231 type.textContent = this.typeIcon;
1232 li.append(type);
1233
1234 // Specs container (for fuzzy highlight compatibility)
1235 const specs = document.createElement('span');
1236 specs.classList.add('specs');
1237
1238 // Name element with character spans
1239 const nameEl = document.createElement('span');
1240 nameEl.classList.add('name', 'monospace');
1241 // Display full closing tag like other macros show full syntax
1242 const displayName = `{{/${this.#macroName}}}`;
1243 for (const char of displayName) {
1244 const span = document.createElement('span');
1245 span.textContent = char;
1246 nameEl.append(span);
1247 }
1248 specs.append(nameEl);
1249 li.append(specs);
1250
1251 // Stopgap (spacer for flex layout)
1252 const stopgap = document.createElement('span');
1253 stopgap.classList.add('stopgap');
1254 li.append(stopgap);
1255
1256 // Help text (description)
1257 const help = document.createElement('span');
1258 help.classList.add('help');
1259 const content = document.createElement('span');
1260 content.classList.add('helpContent');
1261
1262 // Build description based on optional status and nesting
1263 if (this.#isOptional) {
1264 const optionalBadge = document.createElement('span');
1265 optionalBadge.classList.add('macro-ac-optional-badge', 'macro-ac-optional-badge-small');
1266 optionalBadge.textContent = 'OPTIONAL';
1267 content.append(optionalBadge);
1268 content.append(' ');
1269
1270 const nestingInfo = this.#nestingLevel > 0 ? ` (nested ${this.#nestingLevel} level${this.#nestingLevel > 1 ? 's' : ''} deep)` : '';
1271 content.append(document.createTextNode(`Optionally close {{${this.#macroName}}}${nestingInfo}`));
1272 } else {
1273 content.textContent = `Close the {{${this.#macroName}}} scoped macro.`;
1274 }
1275
1276 help.append(content);
1277 li.append(help);
1278
1279 return li;
1280 }
1281
1282 /**
1283 * Renders the details panel for this closing tag.
1284 * @returns {DocumentFragment}
1285 */
1286 renderDetails() {
1287 const frag = document.createDocumentFragment();
1288
1289 const details = document.createElement('div');
1290 details.classList.add('macro-closing-tag-details');
1291
1292 // If optional, show badge at the top
1293 if (this.#isOptional) {
1294 const optionalBadge = document.createElement('span');
1295 optionalBadge.classList.add('macro-ac-optional-badge');
1296 optionalBadge.textContent = 'OPTIONAL';
1297 details.append(optionalBadge);
1298 }
1299
1300 // Header
1301 const header = document.createElement('h3');
1302 header.innerHTML = `Close <code>{{${this.#macroName}}}</code>`;
1303 details.append(header);
1304
1305 // Description
1306 const desc = document.createElement('p');
1307 if (this.#isOptional) {
1308 const nestingInfo = this.#nestingLevel > 0 ? ` This scope is nested ${this.#nestingLevel} level${this.#nestingLevel > 1 ? 's' : ''} deep.` : '';
1309 desc.textContent = `Optionally inserts the closing tag {{/${this.#macroName}}}. The scoped content for this macro is optional - you can close it or leave it open.${nestingInfo}`;
1310 } else {
1311 desc.textContent = `Inserts the closing tag {{/${this.#macroName}}} to complete the scoped macro. The content between the opening and closing tags will be passed as the last argument.`;
1312 }
1313 details.append(desc);
1314
1315 frag.append(details);
1316 return frag;
1317 }
1318}
1319
1320/**
1321 * Parses the macro text to determine current argument context.
1322 * Handles leading whitespace and flags before the identifier.
1323 *
1324 * @param {string} macroText - The text inside {{ }}, e.g., "roll::1d20" or "!user" or " description ".
1325 * @param {number} cursorOffset - Cursor position within macroText.
1326 * @returns {MacroAutoCompleteContext}
1327 */
1328export function parseMacroContext(macroText, cursorOffset) {
1329 let i = 0;
1330
1331 // Skip leading whitespace (but NOT newlines - those stop macro parsing for autocomplete)
1332 while (i < macroText.length && /[ \t]/.test(macroText[i])) {
1333 i++;
1334 }
1335
1336 // Extract flags (special symbols before the identifier)
1337 // Track position after each flag to determine which flag cursor is on
1338 // Special case: `/` followed by identifier chars is a closing tag, not a flag
1339 const flags = [];
1340 const flagEndPositions = []; // Position right after each flag (before any whitespace)
1341 while (i < macroText.length) {
1342 const char = macroText[i];
1343 // Check if this looks like a closing tag: `/` followed by an identifier character
1344 if (char === '/' && i + 1 < macroText.length && /[a-zA-Z/]/.test(macroText[i + 1])) {
1345 // This is a closing tag identifier, not a flag - stop parsing flags
1346 break;
1347 }
1348 if (ValidFlagSymbols.has(char)) {
1349 flags.push(char);
1350 i++;
1351 flagEndPositions.push(i); // Position right after this flag
1352 // Skip whitespace between flags (but NOT newlines - those stop macro parsing for autocomplete)
1353 while (i < macroText.length && /[ \t]/.test(macroText[i])) {
1354 i++;
1355 }
1356 } else {
1357 break;
1358 }
1359 }
1360
1361 // Determine which flag cursor is currently on (if any)
1362 // The "current" flag is the last one typed when cursor is still in the flags area
1363 // This ensures the last typed flag shows at the top of the autocomplete list
1364 let currentFlag = null;
1365 if (flags.length > 0) {
1366 // If cursor is at or after the last flag position but before identifier starts,
1367 // the last flag is the "current" one (just typed)
1368 const lastFlagEnd = flagEndPositions[flagEndPositions.length - 1];
1369 if (cursorOffset >= lastFlagEnd - 1) {
1370 currentFlag = flags[flags.length - 1];
1371 }
1372 }
1373
1374 if (flags.length > 0) {
1375 void onboardingExperimentalMacroEngine('macro flags');
1376 }
1377
1378 // Check for variable shorthand prefix (. or $)
1379 // These trigger variable expression mode instead of regular macro parsing
1380 /** @type {'.'|'$'|null} */
1381 let variablePrefix = null;
1382 let variableName = '';
1383 /** @type {string|null} */
1384 let variableOperator = null;
1385 let variableValue = '';
1386 let isVariableShorthand = false;
1387 let isTypingVariableName = false;
1388 let isTypingOperator = false;
1389 let isTypingValue = false;
1390 let variableNameEnd = i;
1391
1392 const remainingAfterFlags = macroText.slice(i);
1393 if (remainingAfterFlags.startsWith('.') || remainingAfterFlags.startsWith('$')) {
1394 isVariableShorthand = true;
1395 variablePrefix = /** @type {'.'|'$'} */ (remainingAfterFlags[0]);
1396 i++; // Move past the prefix
1397
1398 // Variable names: start with letter, can have hyphens inside, must not end with hyphen
1399 const varNameMatch = macroText.slice(i).match(VARIABLE_SHORTHAND_NAME_PATTERN);
1400 if (varNameMatch) {
1401 variableName = varNameMatch[0];
1402 i += variableName.length;
1403 }
1404 variableNameEnd = i;
1405
1406 // Skip whitespace before operator
1407 while (i < macroText.length && /\s/.test(macroText[i])) {
1408 i++;
1409 }
1410
1411 // Check for operators: ++, --, +=, -=, ||=, ??=, ||, ??, ==, =
1412 // Order matters: longer operators must be checked before shorter ones
1413 // Also track partial operator prefixes for autocomplete
1414 const operatorText = macroText.slice(i);
1415 let hasInvalidTrailingChars = false;
1416 let invalidTrailingChars = '';
1417 let partialOperator = '';
1418 if (operatorText.startsWith('++')) {
1419 variableOperator = '++';
1420 i += 2;
1421 } else if (operatorText.startsWith('--')) {
1422 variableOperator = '--';
1423 i += 2;
1424 } else if (operatorText.startsWith('||=')) {
1425 variableOperator = '||=';
1426 i += 3;
1427 } else if (operatorText.startsWith('??=')) {
1428 variableOperator = '??=';
1429 i += 3;
1430 } else if (operatorText.startsWith('||')) {
1431 variableOperator = '||';
1432 i += 2;
1433 } else if (operatorText.startsWith('??')) {
1434 variableOperator = '??';
1435 i += 2;
1436 } else if (operatorText.startsWith('+=')) {
1437 variableOperator = '+=';
1438 i += 2;
1439 } else if (operatorText.startsWith('-=')) {
1440 variableOperator = '-=';
1441 i += 2;
1442 } else if (operatorText.startsWith('==')) {
1443 variableOperator = '==';
1444 i += 2;
1445 } else if (operatorText.startsWith('!=')) {
1446 variableOperator = '!=';
1447 i += 2;
1448 } else if (operatorText.startsWith('>=')) {
1449 variableOperator = '>=';
1450 i += 2;
1451 } else if (operatorText.startsWith('>')) {
1452 variableOperator = '>';
1453 i += 1;
1454 } else if (operatorText.startsWith('<=')) {
1455 variableOperator = '<=';
1456 i += 2;
1457 } else if (operatorText.startsWith('<')) {
1458 variableOperator = '<';
1459 i += 1;
1460 } else if (operatorText.startsWith('=')) {
1461 variableOperator = '=';
1462 i += 1;
1463 } else if (operatorText.startsWith('+') || operatorText.startsWith('-') || operatorText.startsWith('|') || operatorText.startsWith('?') || operatorText.startsWith('!') || operatorText.startsWith('>') || operatorText.startsWith('<')) {
1464 // Partial operator prefix - user is typing an operator
1465 partialOperator = operatorText[0];
1466 } else if (operatorText.length > 0 && !/^\s/.test(operatorText) && !operatorText.startsWith('}')) {
1467 // There's non-whitespace after the variable name that isn't a valid operator
1468 // This is an invalid trailing character (e.g., $my$ or .var@test)
1469 // Exception: } is the closing brace, not an invalid char
1470 hasInvalidTrailingChars = true;
1471 invalidTrailingChars = operatorText.trim();
1472 }
1473
1474 // Track where the operator ends (for cursor position checks)
1475 const variableOperatorEnd = i;
1476
1477 // Check if operator requires a value
1478 const operatorDef = variableOperator ? VariableOperatorDefinitions.get(variableOperator) : null;
1479 const operatorNeedsValue = operatorDef?.needsValue ?? false;
1480
1481 // If operator requires a value, parse the value
1482 // Do this BEFORE isTypingClosingBrace detection so we can check for } in value area
1483 // let valueStartPos = i;
1484 if (operatorNeedsValue) {
1485 // Skip whitespace after operator
1486 while (i < macroText.length && /\s/.test(macroText[i])) {
1487 i++;
1488 }
1489 // valueStartPos = i;
1490 variableValue = macroText.slice(i).trimEnd();
1491 }
1492
1493 // Detect if typing first closing brace on a variable shorthand
1494 // This happens when operatorText is just "}" or when cursor is beyond content (after }})
1495 let isTypingClosingBrace = false;
1496 if (operatorText.startsWith('}') && !variableOperator) {
1497 // Typing first } on a standalone variable shorthand like {{.Lila}
1498 isTypingClosingBrace = true;
1499 } else if (cursorOffset > macroText.length && !variableOperator) {
1500 // Cursor is after }} on a standalone variable shorthand like {{.Lila}}|
1501 isTypingClosingBrace = true;
1502 } else if (cursorOffset > macroText.length && variableOperator) {
1503 // Cursor is after }} on any operator shorthand like {{.Lila++}}| or {{.Lila+=4}}|
1504 isTypingClosingBrace = true;
1505 } else if (cursorOffset >= macroText.length && variableOperator && !operatorNeedsValue) {
1506 // Cursor at end of complete operator (++ or --) like {{.Lila++ or {{.Lila++ (with trailing space)
1507 isTypingClosingBrace = true;
1508 } else if (cursorOffset >= macroText.length && !variableOperator && variableName.length > 0) {
1509 // Cursor at end of standalone variable (with or without trailing whitespace) like {{.Lila or {{ .Lila
1510 isTypingClosingBrace = true;
1511 } else if (operatorNeedsValue && variableValue.length > 0 && variableValue.endsWith('}')) {
1512 // Typing first } after a value like {{.Lila+=4}
1513 isTypingClosingBrace = true;
1514 // Strip the } from the value
1515 variableValue = variableValue.slice(0, -1);
1516 } else if (operatorNeedsValue && cursorOffset >= macroText.length && variableValue.length > 0) {
1517 // Cursor at end after typing a value (including trailing whitespace) like {{.Lila+=4
1518 // This means the shorthand is "complete" and ready to close
1519 isTypingClosingBrace = true;
1520 }
1521
1522 // Determine cursor position context for autocomplete
1523 // Note: isTypingClosingBrace takes precedence - if we're typing a closing brace,
1524 // we don't want to show operator suggestions, just the current state
1525 const prefixEnd = (macroText.indexOf(variablePrefix) ?? 0) + 1;
1526 if (cursorOffset < prefixEnd) {
1527 // Cursor is before the prefix - still in flags area conceptually
1528 isTypingVariableName = false;
1529 } else if (cursorOffset <= variableNameEnd) {
1530 // Cursor is in the variable name area (including at the end)
1531 isTypingVariableName = true;
1532 } else if (variableName.length > 0 && !variableOperator && !hasInvalidTrailingChars && !isTypingClosingBrace) {
1533 // Cursor is after variable name but no operator yet (and no invalid chars)
1534 // This includes partial operator prefixes like '+', '-', '|', '?', '>', '<'
1535 // But NOT when typing a closing brace - that takes precedence
1536 isTypingOperator = true;
1537 } else if (variableName.length > 0 && variableOperator && isShortOperatorPrefix(variableOperator) && cursorOffset <= variableOperatorEnd) {
1538 // Short operator that could be prefix of longer one (e.g., > could become >=)
1539 // But ONLY if cursor is still in the operator area, not past it into value
1540 isTypingOperator = true;
1541 } else if (operatorNeedsValue) {
1542 // Operator that requires value - cursor is in value area
1543 isTypingValue = true;
1544 }
1545 // For ++ and --, the operator is complete (no value needed)
1546 // For invalid trailing chars, none of the typing flags will be true
1547 const isOperatorComplete = (variableOperator === '++' || variableOperator === '--');
1548
1549 void onboardingExperimentalMacroEngine('variable shorthands');
1550
1551 // Return early for variable shorthand - different structure than regular macros
1552 return {
1553 fullText: macroText,
1554 cursorOffset,
1555 paddingBefore: macroText.match(/^\s+/)?.[0] ?? '',
1556 identifier: '', // No macro identifier for variable shorthand
1557 identifierStart: -1,
1558 isInFlagsArea: false,
1559 flags,
1560 currentFlag,
1561 args: [],
1562 currentArgIndex: -1,
1563 isTypingSeparator: false,
1564 isTypingClosingBrace,
1565 hasSpaceAfterIdentifier: false,
1566 hasSpaceArgContent: false,
1567 separatorCount: 0,
1568 // Variable shorthand specific properties
1569 isVariableShorthand,
1570 variablePrefix,
1571 variableName,
1572 variableNameEnd,
1573 variableOperator,
1574 variableOperatorEnd,
1575 variableValue,
1576 isTypingVariableName,
1577 isTypingOperator,
1578 isTypingValue,
1579 isOperatorComplete,
1580 hasInvalidTrailingChars,
1581 invalidTrailingChars,
1582 partialOperator,
1583 };
1584 }
1585
1586 // Regular macro parsing (not variable shorthand)
1587 // Now parse the identifier and arguments starting from position i
1588 const remainingText = macroText.slice(i);
1589 const parts = [];
1590 /** @type {{ start: number, end: number }[]} */
1591 const separatorPositions = []; // Track positions of :: separators
1592 let currentPart = '';
1593 let partStart = i;
1594 let j = 0;
1595
1596 // Track nesting depth to skip :: inside nested macros
1597 let nestedDepth = 0;
1598 // Track if we've seen a :: separator - newlines before first :: should stop parsing
1599 let hasSeenSeparator = false;
1600 // Track if we broke early (e.g., at a newline)
1601 let brokeEarly = false;
1602 while (j < remainingText.length) {
1603 // Before the first :: separator, newlines should stop parsing
1604 // This prevents text on the next line from being considered part of the identifier/space-arg
1605 if (!hasSeenSeparator && nestedDepth === 0 && (remainingText[j] === '\n' || remainingText[j] === '\r')) {
1606 // Stop parsing here - don't include the newline or anything after
1607 brokeEarly = true;
1608 break;
1609 }
1610 // Track nested macro braces
1611 if (remainingText[j] === '{' && remainingText[j + 1] === '{') {
1612 nestedDepth++;
1613 currentPart += '{{';
1614 j += 2;
1615 continue;
1616 }
1617 if (remainingText[j] === '}' && remainingText[j + 1] === '}') {
1618 nestedDepth = Math.max(0, nestedDepth - 1);
1619 currentPart += '}}';
1620 j += 2;
1621 continue;
1622 }
1623 // Only count :: as separator when not inside nested macros
1624 if (nestedDepth === 0 && remainingText[j] === ':' && remainingText[j + 1] === ':') {
1625 parts.push({ text: currentPart, start: partStart, end: i + j });
1626 separatorPositions.push({ start: i + j, end: i + j + 2 });
1627 currentPart = '';
1628 j += 2;
1629 partStart = i + j;
1630 hasSeenSeparator = true;
1631 } else {
1632 currentPart += remainingText[j];
1633 j++;
1634 }
1635 }
1636 // Push the last part - use correct end position if we broke early.
1637 // If we broke early (at a newline) AND cursor is past that point, don't push -
1638 // this filters out text on the next line from being considered part of this macro.
1639 // But if we didn't break early (cursor at end of closed macro), always push.
1640 const lastPartEnd = brokeEarly ? i + j : macroText.length;
1641 const shouldPushLastPart = !brokeEarly || cursorOffset <= lastPartEnd;
1642 if (shouldPushLastPart) {
1643 parts.push({ text: currentPart, start: partStart, end: lastPartEnd });
1644 }
1645
1646 // Determine if cursor is in the flags area (at or before identifier starts)
1647 const identifierStartPos = parts[0]?.start ?? i;
1648 const isInFlagsArea = cursorOffset <= identifierStartPos;
1649
1650 // Check if cursor is on a partial separator (single ':' that might become '::')
1651 const isTypingSeparator = remainingText.length > 0 &&
1652 cursorOffset > identifierStartPos &&
1653 macroText[cursorOffset - 1] === ':' &&
1654 macroText[cursorOffset] !== ':' &&
1655 (cursorOffset < 2 || macroText[cursorOffset - 2] !== ':');
1656
1657 // Parse identifier and space-separated argument from the first part
1658 // "getvar myvar" -> identifier="getvar", spaceArg="myvar"
1659 // "setvar " -> identifier="setvar", spaceArg="" (just whitespace, no content yet)
1660 const firstPartText = parts[0]?.text || '';
1661 const trimmedFirstPart = firstPartText.trimStart();
1662 const firstSpaceInIdentifier = trimmedFirstPart.search(/\s/);
1663
1664 let identifierOnly;
1665 let spaceArgText = '';
1666 //let spaceArgStart = -1;
1667 let hasSpaceAfterIdentifier = false;
1668
1669 if (firstSpaceInIdentifier > 0 && separatorPositions.length === 0) {
1670 // There's whitespace inside the first part - split identifier from space-arg
1671 identifierOnly = trimmedFirstPart.slice(0, firstSpaceInIdentifier);
1672 const afterIdentifier = trimmedFirstPart.slice(firstSpaceInIdentifier);
1673 // Check if there's actual content after the whitespace (not just spaces or ::)
1674 const contentAfterSpace = afterIdentifier.trimStart();
1675 hasSpaceAfterIdentifier = afterIdentifier.length > 0; // Has at least a space
1676
1677 if (contentAfterSpace.length > 0 && !contentAfterSpace.startsWith(':')) {
1678 // There's actual argument content after the space
1679 spaceArgText = contentAfterSpace;
1680 //spaceArgStart = identifierStartPos + firstSpaceInIdentifier + (afterIdentifier.length - contentAfterSpace.length);
1681 }
1682 } else {
1683 identifierOnly = trimmedFirstPart.trimEnd();
1684 }
1685
1686 // Calculate identifier end position (for space-after-identifier detection)
1687 const identifierEndPos = identifierStartPos + (firstPartText.length - firstPartText.trimStart().length) + identifierOnly.length;
1688
1689 // Determine which part the cursor is in
1690 let currentArgIndex = -1;
1691
1692 // Only consider being in an argument if we've passed a separator
1693 if (separatorPositions.length > 0) {
1694 // Find which argument we're in based on separator positions
1695 for (let sepIdx = 0; sepIdx < separatorPositions.length; sepIdx++) {
1696 const sep = separatorPositions[sepIdx];
1697 if (cursorOffset >= sep.end) {
1698 // We're past this separator, so we're in at least this argument
1699 currentArgIndex = sepIdx;
1700 }
1701 }
1702 } else if (spaceArgText.length > 0 || (hasSpaceAfterIdentifier && cursorOffset > identifierEndPos)) {
1703 // Space-separated arg: either has content, or cursor is past identifier+space
1704 currentArgIndex = 0;
1705 }
1706
1707 // If typing a separator, we're still on identifier/previous arg, not the next one
1708 if (isTypingSeparator) {
1709 currentArgIndex = -1;
1710 }
1711
1712 const leftPadding = macroText.match(/^\s+/)?.[0] ?? '';
1713
1714 if (leftPadding) {
1715 void onboardingExperimentalMacroEngine('leading whitespace');
1716 }
1717
1718 // Clean identifier: strip trailing colons (for partial :: typing)
1719 // Also strip trailing single } (for partial }} typing) - but only if no separators/args
1720 let cleanIdentifier = identifierOnly.replace(/:+$/, '');
1721 let isTypingClosingBrace = false;
1722 if (separatorPositions.length === 0 && !hasSpaceAfterIdentifier && cleanIdentifier.endsWith('}')) {
1723 // Typing first closing brace on a standalone macro like {{char}
1724 cleanIdentifier = cleanIdentifier.slice(0, -1);
1725 isTypingClosingBrace = true;
1726 }
1727
1728 // Build args array - include space-separated arg if present
1729 // Trim args like the macro engine does
1730 let args = parts.slice(1).map(p => p.text.trim());
1731 if (spaceArgText.length > 0) {
1732 args = [spaceArgText, ...args];
1733 }
1734
1735 return {
1736 fullText: macroText,
1737 cursorOffset,
1738 paddingBefore: leftPadding,
1739 identifier: cleanIdentifier,
1740 identifierStart: identifierStartPos,
1741 isInFlagsArea,
1742 flags,
1743 currentFlag,
1744 args,
1745 currentArgIndex,
1746 isTypingSeparator,
1747 isTypingClosingBrace,
1748 hasSpaceAfterIdentifier,
1749 hasSpaceArgContent: spaceArgText.length > 0,
1750 separatorCount: separatorPositions.length,
1751 // Default variable shorthand properties (not a variable shorthand)
1752 isVariableShorthand: false,
1753 variablePrefix: null,
1754 variableName: '',
1755 variableNameEnd: null,
1756 variableOperator: null,
1757 variableOperatorEnd: null,
1758 variableValue: '',
1759 isTypingVariableName: false,
1760 isTypingOperator: false,
1761 isTypingValue: false,
1762 };
1763}
1764
1765/**
1766 * A simple, generic autocomplete option for displaying basic items with name, symbol, and description.
1767 * Useful for simple options like inversion markers, prefixes, etc. without needing a full custom class.
1768 *
1769 * @extends AutoCompleteOption
1770 */
1771export class SimpleAutoCompleteOption extends AutoCompleteOption {
1772 /** @type {string} */
1773 #description;
1774
1775 /** @type {string|null} */
1776 #detailedDescription;
1777
1778 /**
1779 * @param {Object} config - Configuration for the option.
1780 * @param {string} config.name - The option name/key (used for matching).
1781 * @param {string} [config.symbol=' '] - Icon/symbol shown in the type column.
1782 * @param {string} [config.description=''] - Short description shown inline.
1783 * @param {string} [config.detailedDescription] - Longer description for details panel (supports HTML). Falls back to description if not provided.
1784 * @param {string} [config.type='simple'] - Type identifier for CSS/data attributes.
1785 */
1786 constructor({ name, symbol = ' ', description = '', detailedDescription = null, type = 'simple' }) {
1787 super(name, symbol, type);
1788 this.#description = description;
1789 this.#detailedDescription = detailedDescription;
1790 }
1791
1792 /** @returns {string} */
1793 get description() {
1794 return this.#description;
1795 }
1796
1797 /** @returns {string} */
1798 get detailedDescription() {
1799 return this.#detailedDescription ?? this.#description;
1800 }
1801
1802 /**
1803 * @returns {HTMLElement}
1804 */
1805 renderItem() {
1806 const li = document.createElement('li');
1807 li.classList.add('item');
1808 li.setAttribute('data-name', this.name);
1809 li.setAttribute('data-option-type', this.type);
1810
1811 // Type icon
1812 const typeSpan = document.createElement('span');
1813 typeSpan.classList.add('type', 'monospace');
1814 typeSpan.textContent = this.typeIcon;
1815 li.append(typeSpan);
1816
1817 // Name
1818 const specs = document.createElement('span');
1819 specs.classList.add('specs');
1820 const nameSpan = document.createElement('span');
1821 nameSpan.classList.add('name', 'monospace');
1822 this.name.split('').forEach(char => {
1823 const span = document.createElement('span');
1824 span.textContent = char;
1825 nameSpan.append(span);
1826 });
1827 specs.append(nameSpan);
1828 li.append(specs);
1829
1830 // Stopgap
1831 const stopgap = document.createElement('span');
1832 stopgap.classList.add('stopgap');
1833 li.append(stopgap);
1834
1835 // Help/description
1836 const help = document.createElement('span');
1837 help.classList.add('help');
1838 const content = document.createElement('span');
1839 content.classList.add('helpContent');
1840 content.textContent = this.#description;
1841 help.append(content);
1842 li.append(help);
1843
1844 return li;
1845 }
1846
1847 /**
1848 * @returns {DocumentFragment}
1849 */
1850 renderDetails() {
1851 const frag = document.createDocumentFragment();
1852
1853 // Header with name
1854 const specs = document.createElement('div');
1855 specs.classList.add('specs');
1856 const nameDiv = document.createElement('div');
1857 nameDiv.classList.add('name', 'monospace');
1858 nameDiv.textContent = this.name;
1859 specs.append(nameDiv);
1860 frag.append(specs);
1861
1862 // Description
1863 if (this.detailedDescription) {
1864 const helpDiv = document.createElement('div');
1865 helpDiv.classList.add('help');
1866 helpDiv.innerHTML = this.detailedDescription;
1867 frag.append(helpDiv);
1868 }
1869
1870 return frag;
1871 }
260}1872}
public/scripts/autocomplete/MacroAutoComplete.js+307 -0
@@ -0,0 +1,307 @@
1/**
2 * Macro autocomplete for free text inputs (textareas and input fields).
3 * Provides macro autocomplete when typing `{{` in marked text inputs.
4 *
5 * This module uses shared utilities from MacroAutoCompleteHelper.js to ensure
6 * consistent behavior with the slash command macro autocomplete.
7 *
8 * Usage:
9 * - Mark a textarea/input with `data-macros` or `data-macros="true"` attribute
10 * - Call `initMacroAutoComplete()` to initialize all marked elements
11 * - Dynamically added elements are automatically initialized via MutationObserver
12 */
13
14import { power_user } from '../power-user.js';
15import { AutoComplete, AUTOCOMPLETE_STATE } from './AutoComplete.js';
16import { findMacroAtCursor, findUnclosedScopes, getMacroAutoCompleteAt } from './MacroAutoCompleteHelper.js';
17
18/** Custom attribute name used to mark elements that support macro autocomplete */
19export const MACRO_AUTOCOMPLETE_ATTRIBUTE = 'data-macros';
20
21/** Attribute to control autocomplete visibility: 'always' (force show) or 'hide' (never show) */
22export const MACRO_AUTOCOMPLETE_MODE_ATTRIBUTE = 'data-macros-autocomplete';
23
24/** Generic attribute to control autocomplete popup style/size (used by AutoComplete) */
25export const MACRO_AUTOCOMPLETE_STYLE_ATTRIBUTE = 'data-macros-autocomplete-style';
26
27/**
28 * @readonly
29 * @enum {string}
30 */
31export const MACRO_AUTOCOMPLETE_MODE = Object.freeze({
32 /** Default behavior: respects global setting showInAllMacroFields */
33 DEFAULT: 'default',
34 /** Always show autocomplete in this field (expanded editors, prompt manager) */
35 ALWAYS: 'always',
36 /** Never show autocomplete in this field */
37 HIDE: 'hide',
38});
39
40/**
41 * @readonly
42 * @enum {string}
43 */
44export const MACRO_AUTOCOMPLETE_STYLE = Object.freeze({
45 /** Small popup (33vw, max 700px) for inline fields */
46 SMALL: 'small',
47 /** Expanded popup (default chat width) for expanded editors */
48 EXPANDED: 'expanded',
49});
50
51/** @type {WeakSet<HTMLElement>} Track initialized elements to avoid double-init */
52const initializedElements = new WeakSet();
53
54/** @type {WeakMap<HTMLElement, AutoComplete>} Map elements to their autocomplete instances */
55const elementAutoCompleteMap = new WeakMap();
56
57/**
58 * Checks if the cursor is positioned where macro autocomplete should activate.
59 * Activates when:
60 * - Cursor is right after typing `{{`
61 * - Cursor is inside a macro `{{...}}`
62 * - Cursor is in scoped content of an unclosed scoped macro (e.g., after `{{setvar myvar}}`)
63 *
64 * @param {string} text - The full text content.
65 * @param {number} cursorPos - The cursor position.
66 * @param {Object} [options={}] - Additional options.
67 * @param {boolean} [options.isForced=false] - Whether this is a forced activation (e.g., Ctrl+Space).
68 * @param {MACRO_AUTOCOMPLETE_MODE} [options.autocompleteMode=MACRO_AUTOCOMPLETE_MODE.DEFAULT] - The autocomplete mode.
69 * @returns {boolean}
70 */
71function shouldActivateMacroAutocomplete(text, cursorPos, { isForced = false, autocompleteMode = MACRO_AUTOCOMPLETE_MODE.DEFAULT } = {}) {
72 // If mode is 'hide', never show autocomplete
73 if (autocompleteMode === MACRO_AUTOCOMPLETE_MODE.HIDE) {
74 return false;
75 }
76
77 // Check if autocomplete is enabled at all
78 if (power_user.stscript.autocomplete.state === AUTOCOMPLETE_STATE.DISABLED) {
79 return false;
80 }
81
82 // Determine if we should show normally based on mode and settings
83 // ALWAYS mode: always show, DEFAULT mode: respect global setting
84 const alwaysShow = autocompleteMode === MACRO_AUTOCOMPLETE_MODE.ALWAYS;
85 const shouldShowNormally = isForced || alwaysShow || power_user.stscript.autocomplete.showInAllMacroFields;
86
87 // Whether setting says autocomplete should only activate after typing {{ and two characters after that
88 // Ctrl+Space (isForced) overrides this restriction
89 const onlyAfter2 = !isForced && power_user.stscript.autocomplete.state === AUTOCOMPLETE_STATE.MIN_LENGTH;
90
91 // Check if we're right after {{ (just typed the second brace)
92 if (cursorPos >= 2 && text.slice(cursorPos - 2, cursorPos) === '{{') {
93 return shouldShowNormally && !onlyAfter2;
94 }
95
96 // Check if we're inside a macro
97 const macro = findMacroAtCursor(text, cursorPos);
98 if (macro !== null) {
99 if (!shouldShowNormally) return false;
100 return !onlyAfter2 || (macro.content.trim()).length >= 2;
101 }
102
103 // Check if we're in scoped content of an unclosed scoped macro
104 const textUpToCursor = text.slice(0, cursorPos);
105 const unclosedScopes = findUnclosedScopes(textUpToCursor);
106 return shouldShowNormally && unclosedScopes.length > 0;
107}
108
109/**
110 * Sets up macro autocomplete for a text input element.
111 * The autocomplete will trigger when typing `{{` inside the element.
112 *
113 * @param {HTMLTextAreaElement|HTMLInputElement} textarea - The input element.
114 * @param {Object} [options={}] - Options for the autocomplete.
115 * @param {MACRO_AUTOCOMPLETE_MODE} [options.autocompleteMode=MACRO_AUTOCOMPLETE_MODE.DEFAULT] - The autocomplete mode.
116 * @param {MACRO_AUTOCOMPLETE_STYLE} [options.autocompleteStyle=MACRO_AUTOCOMPLETE_STYLE.SMALL] - The autocomplete style.
117 * @returns {AutoComplete} The autocomplete instance.
118 */
119export function setMacroAutoComplete(textarea, { autocompleteMode = MACRO_AUTOCOMPLETE_MODE.DEFAULT, autocompleteStyle = MACRO_AUTOCOMPLETE_STYLE.SMALL } = {}) {
120 const ac = new AutoComplete(
121 textarea,
122 () => shouldActivateMacroAutocomplete(ac.text, textarea.selectionStart, { isForced: ac.isShowForced, autocompleteMode }),
123 (text, index) => getMacroAutoCompleteAt(text, index, { isForced: ac.isShowForced }),
124 true, // isFloating - always use floating mode for free text macro autocomplete
125 );
126
127 // Set the style via data attribute for CSS targeting
128 ac.domWrap.dataset.macrosAutocompleteStyle = autocompleteStyle;
129 ac.detailsWrap.dataset.macrosAutocompleteStyle = autocompleteStyle;
130
131 elementAutoCompleteMap.set(textarea, ac);
132 return ac;
133}
134
135/**
136 * Gets the autocomplete mode from an element's data-macros-autocomplete attribute.
137 *
138 * @param {Element} element - The element to check.
139 * @returns {MACRO_AUTOCOMPLETE_MODE} The mode ('default', 'always', 'hide').
140 */
141function getAutocompleteMode(element) {
142 if (!element.hasAttribute(MACRO_AUTOCOMPLETE_MODE_ATTRIBUTE)) {
143 return MACRO_AUTOCOMPLETE_MODE.DEFAULT;
144 }
145 const value = element.getAttribute(MACRO_AUTOCOMPLETE_MODE_ATTRIBUTE);
146 if (value === MACRO_AUTOCOMPLETE_MODE.ALWAYS || value === MACRO_AUTOCOMPLETE_MODE.HIDE) {
147 return value;
148 }
149 return MACRO_AUTOCOMPLETE_MODE.DEFAULT;
150}
151
152/**
153 * Gets the autocomplete style from an element's data-autocomplete-style attribute.
154 *
155 * @param {Element} element - The element to check.
156 * @returns {MACRO_AUTOCOMPLETE_STYLE} The style ('expanded', 'small').
157 */
158function getAutocompleteStyle(element) {
159 if (!element.hasAttribute(MACRO_AUTOCOMPLETE_STYLE_ATTRIBUTE)) {
160 return MACRO_AUTOCOMPLETE_STYLE.SMALL; // Default for macro autocomplete is small
161 }
162 const value = element.getAttribute(MACRO_AUTOCOMPLETE_STYLE_ATTRIBUTE);
163 if (value === MACRO_AUTOCOMPLETE_STYLE.SMALL || value === MACRO_AUTOCOMPLETE_STYLE.EXPANDED) {
164 return value;
165 }
166 return MACRO_AUTOCOMPLETE_STYLE.EXPANDED;
167}
168
169/**
170 * Initializes macro autocomplete on a single element if not already initialized.
171 *
172 * @param {HTMLTextAreaElement|HTMLInputElement} element - The element to initialize.
173 * @returns {AutoComplete|null} The autocomplete instance, or null if already initialized.
174 */
175function initializeElement(element) {
176 if (initializedElements.has(element)) {
177 return null;
178 }
179
180 if (!(element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement)) {
181 return null;
182 }
183
184 const autocompleteMode = getAutocompleteMode(element);
185 const autocompleteStyle = getAutocompleteStyle(element);
186 initializedElements.add(element);
187 return setMacroAutoComplete(element, { autocompleteMode, autocompleteStyle });
188}
189
190/**
191 * Checks if an element has the macro autocomplete attribute enabled.
192 * Supports both `data-macros` (presence) and `data-macros="true"`.
193 *
194 * @param {Element} element - The element to check.
195 * @returns {boolean}
196 */
197function hasMacroAttribute(element) {
198 if (!element.hasAttribute(MACRO_AUTOCOMPLETE_ATTRIBUTE)) {
199 return false;
200 }
201 const value = element.getAttribute(MACRO_AUTOCOMPLETE_ATTRIBUTE);
202 // Attribute present with no value, empty string, or "true" all count as enabled
203 return value === null || value === '' || value === 'true';
204}
205
206/**
207 * Handles node changes from MutationObserver - checks for macro autocomplete attribute.
208 *
209 * @param {Node} node - The node to check.
210 */
211function handleNodeChange(node) {
212 if (node.nodeType !== Node.ELEMENT_NODE || !(node instanceof Element)) {
213 return;
214 }
215
216 // Check if this element has the macro autocomplete attribute
217 if (hasMacroAttribute(node)) {
218 if (node instanceof HTMLTextAreaElement || node instanceof HTMLInputElement) {
219 initializeElement(node);
220 }
221 }
222
223 // Check child elements - select all elements with the attribute (any value or no value)
224 const children = node.querySelectorAll(`[${MACRO_AUTOCOMPLETE_ATTRIBUTE}]`);
225 for (const child of children) {
226 if (hasMacroAttribute(child) && (child instanceof HTMLTextAreaElement || child instanceof HTMLInputElement)) {
227 initializeElement(child);
228 }
229 }
230}
231
232/**
233 * MutationObserver to watch for dynamically added elements with macro autocomplete attribute.
234 * @type {MutationObserver}
235 */
236const observer = new MutationObserver(mutations => {
237 for (const mutation of mutations) {
238 if (mutation.type === 'childList') {
239 for (const node of mutation.addedNodes) {
240 handleNodeChange(node);
241 }
242 }
243 if (mutation.type === 'attributes') {
244 const target = mutation.target;
245 const isRelevantAttr = mutation.attributeName === MACRO_AUTOCOMPLETE_ATTRIBUTE ||
246 mutation.attributeName === MACRO_AUTOCOMPLETE_MODE_ATTRIBUTE ||
247 mutation.attributeName === MACRO_AUTOCOMPLETE_STYLE_ATTRIBUTE;
248 if (isRelevantAttr && target instanceof Element) {
249 handleNodeChange(target);
250 }
251 }
252 }
253});
254
255/**
256 * Initializes macro autocomplete for all elements with the `data-macros` attribute.
257 * Also starts the MutationObserver to watch for dynamically added elements.
258 * Should be called after DOM is ready.
259 *
260 * @returns {AutoComplete[]} Array of autocomplete instances created.
261 */
262export function initMacroAutoComplete() {
263 const elements = /** @type {NodeListOf<HTMLTextAreaElement|HTMLInputElement>} */ (
264 document.querySelectorAll(`[${MACRO_AUTOCOMPLETE_ATTRIBUTE}]`)
265 );
266
267 const instances = [];
268 for (const element of elements) {
269 if (hasMacroAttribute(element)) {
270 const ac = initializeElement(element);
271 if (ac) {
272 instances.push(ac);
273 }
274 }
275 }
276
277 // Start observing for dynamically added elements
278 observer.observe(document.body, {
279 childList: true,
280 subtree: true,
281 attributes: true,
282 attributeFilter: [MACRO_AUTOCOMPLETE_ATTRIBUTE, MACRO_AUTOCOMPLETE_MODE_ATTRIBUTE, MACRO_AUTOCOMPLETE_STYLE_ATTRIBUTE],
283 });
284
285 return instances;
286}
287
288/**
289 * Enables macro autocomplete on a specific element by ID.
290 * Adds the attribute and initializes autocomplete.
291 *
292 * @param {string} elementId - The element ID (without #).
293 * @returns {AutoComplete|null} The autocomplete instance, or null if element not found.
294 */
295export function enableMacroAutoCompleteById(elementId) {
296 const element = /** @type {HTMLTextAreaElement|HTMLInputElement|null} */ (
297 document.getElementById(elementId)
298 );
299
300 if (!element || !(element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement)) {
301 console.warn(`[MacroAutoComplete] Element not found or invalid: ${elementId}`);
302 return null;
303 }
304
305 element.setAttribute(MACRO_AUTOCOMPLETE_ATTRIBUTE, 'true');
306 return initializeElement(element);
307}
public/scripts/autocomplete/MacroAutoCompleteHelper.js+1217 -0
@@ -0,0 +1,1217 @@
1/**
2 * Shared utilities for macro autocomplete functionality.
3 * Used by both SlashCommandParser (for slash command context) and MacroAutoComplete (for free text).
4 *
5 * This module extracts common macro autocomplete logic to avoid duplication and ensure
6 * consistent behavior across all contexts where macro autocomplete is used.
7 */
8
9import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
10import {
11 EnhancedMacroAutoCompleteOption,
12 MacroFlagAutoCompleteOption,
13 MacroClosingTagAutoCompleteOption,
14 VariableShorthandAutoCompleteOption,
15 VariableShorthandDefinitions,
16 VariableNameAutoCompleteOption,
17 VariableOperatorAutoCompleteOption,
18 VariableValueContextAutoCompleteOption,
19 VariableOperatorDefinitions,
20 isValidVariableShorthandName,
21 parseMacroContext,
22 SimpleAutoCompleteOption,
23} from './EnhancedMacroAutoCompleteOption.js';
24import { macros as macroSystem } from '../macros/macro-system.js';
25import { MacroFlagDefinitions, MacroFlagType } from '../macros/engine/MacroFlags.js';
26import { MacroParser } from '../macros/engine/MacroParser.js';
27import { MacroCstWalker } from '../macros/engine/MacroCstWalker.js';
28import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js';
29import { chat_metadata } from '/script.js';
30import { extension_settings } from '../extensions.js';
31
32/** @typedef {import('./EnhancedMacroAutoCompleteOption.js').MacroAutoCompleteContext} MacroAutoCompleteContext */
33/** @typedef {import('./EnhancedMacroAutoCompleteOption.js').EnhancedMacroAutoCompleteOptions} EnhancedMacroAutoCompleteOptions */
34/** @typedef {import('./AutoCompleteOption.js').AutoCompleteOption} AutoCompleteOption */
35/*** @typedef {import('../macros/macro-system.js').MacroDefinition} MacroDefinition */
36
37/**
38 * @typedef {Object} MacroInfo
39 * @property {number} start - Start position of the macro in text (at first {)
40 * @property {number} end - End position of the macro in text (after last })
41 * @property {string} content - The content between {{ and }}
42 */
43
44/**
45 * @typedef {Object} UnclosedScope
46 * @property {string} name - Macro name
47 * @property {number} startOffset - Start position in text
48 * @property {number} endOffset - End position of opening tag
49 * @property {string} paddingBefore - Whitespace before macro name
50 * @property {string} paddingAfter - Whitespace after macro content
51 */
52
53/**
54 * @typedef {Object} BuildMacroAutoCompleteOptions
55 * @property {MacroInfo|null} [macro=null] - Macro info if cursor is inside a macro
56 * @property {string|null} [textUpToCursor=null] - Pre-computed text up to cursor
57 * @property {UnclosedScope[]|null} [unclosedScopes=null] - Pre-computed unclosed scopes
58 * @property {boolean} [isForced=false] - Whether autocomplete was force-triggered (Ctrl+Space)
59 */
60
61/** @typedef {(EnhancedMacroAutoCompleteOption|MacroFlagAutoCompleteOption|MacroClosingTagAutoCompleteOption|VariableShorthandAutoCompleteOption|VariableNameAutoCompleteOption|VariableOperatorAutoCompleteOption|VariableValueContextAutoCompleteOption|SimpleAutoCompleteOption)} AnyMacroAutoCompleteOption */
62
63/**
64 * Finds unclosed scoped macros in the text up to cursor position.
65 * Uses the MacroParser and MacroCstWalker for accurate analysis.
66 *
67 * @param {string} textUpToCursor - The document text up to the cursor position.
68 * @returns {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>}
69 */
70export function findUnclosedScopes(textUpToCursor) {
71 if (!textUpToCursor) return [];
72
73 try {
74 // Parse the document to get the CST
75 const { cst } = MacroParser.parseDocument(textUpToCursor);
76 if (!cst) return [];
77
78 // Use the CST walker to find unclosed scopes
79 return MacroCstWalker.findUnclosedScopes({ text: textUpToCursor, cst });
80 } catch {
81 // If parsing fails (incomplete input), fall back to simple regex approach
82 return findUnclosedScopesRegex(textUpToCursor);
83 }
84}
85
86/**
87 * Fallback regex-based approach for finding unclosed scopes.
88 * Used when the parser fails on incomplete input.
89 *
90 * @param {string} text - The text to analyze.
91 * @returns {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>}
92 */
93export function findUnclosedScopesRegex(text) {
94 // Regex to find macro openings and closings, capturing whitespace padding
95 // Group 1: padding after {{, Group 2: optional /, Group 3: macro name
96 const macroPattern = /\{\{(\s*)(\/?)([\w-]+)/g;
97 const stack = [];
98
99 let match;
100 while ((match = macroPattern.exec(text)) !== null) {
101 const paddingBefore = match[1];
102 const isClosing = match[2] === '/';
103 const name = match[3];
104
105 if (isClosing) {
106 // Find matching opener in stack (case-insensitive)
107 // When closing an outer scope, all inner unclosed scopes are implicitly closed
108 const matchIndex = stack.findLastIndex(s => s.name.toLowerCase() === name.toLowerCase());
109 if (matchIndex !== -1) {
110 // Pop everything from matchIndex to end (inclusive) - closes the matched scope and all nested ones
111 stack.splice(matchIndex);
112 }
113 } else {
114 // Check if macro can accept scoped content
115 // List-arg macros don't support scopes - they accept arbitrary inline args instead
116 const macroDef = macroSystem.registry.getPrimaryMacro(name);
117 if (macroDef && macroDef.maxArgs > 0 && macroDef.list === null) {
118 // Try to find closing }} to extract trailing whitespace
119 let paddingAfter = '';
120 const afterMatch = text.slice(match.index + match[0].length);
121 const closingMatch = afterMatch.match(/^[^}]*?(\s*)\}\}/);
122 if (closingMatch) {
123 paddingAfter = closingMatch[1];
124 }
125
126 stack.push({
127 name,
128 startOffset: match.index,
129 endOffset: match.index + match[0].length,
130 paddingBefore,
131 paddingAfter,
132 });
133 }
134 }
135 }
136
137 return stack;
138}
139
140/**
141 * Checks if a scoped macro's scope content is optional (i.e., all required args are already filled).
142 * Used to determine whether to show the scope hint by default or only when forced.
143 *
144 * @param {UnclosedScope} scope - The unclosed scope info.
145 * @param {string} textUpToCursor - The text up to cursor to parse the macro content.
146 * @returns {boolean} - True if the scope content is optional.
147 */
148function isScopeOptional(scope, textUpToCursor) {
149 const def = macroSystem.registry.getPrimaryMacro(scope.name);
150 if (!def) {
151 // Unknown macro - treat scope as required (show hint)
152 return false;
153 }
154
155 // Find the macro's closing }} to extract its content
156 const openingEnd = textUpToCursor.indexOf('}}', scope.startOffset);
157 if (openingEnd === -1) {
158 // Macro not closed yet - can't determine
159 return false;
160 }
161
162 // Extract content between {{ and }} to count arguments
163 const macroContent = textUpToCursor.slice(scope.startOffset + 2, openingEnd);
164 const context = parseMacroContext(macroContent, macroContent.length);
165
166 // Count current arguments (including space-separated arg if present)
167 const currentArgCount = context.args.length;
168
169 // The scoped content would be the next argument (currentArgCount + 1)
170 // Scope is optional if:
171 // 1. Current args already meet minArgs requirement, AND
172 // 2. Adding one more (scope) would still be <= maxArgs
173 const wouldBeArgIndex = currentArgCount; // 0-indexed
174 const scopeIsOptional = currentArgCount >= def.minArgs && wouldBeArgIndex < def.maxArgs;
175
176 // Check if the argument at wouldBeArgIndex is marked as optional in the definition
177 if (def.unnamedArgDefs && def.unnamedArgDefs[wouldBeArgIndex]) {
178 return def.unnamedArgDefs[wouldBeArgIndex].optional === true;
179 }
180
181 // If no explicit arg definition, use the min/max args logic
182 return scopeIsOptional;
183}
184
185/**
186 * Filters unclosed scopes to exclude those with optional scope content.
187 * Used when autocomplete is not force-triggered (Ctrl+Space).
188 *
189 * @param {UnclosedScope[]} unclosedScopes - The unclosed scopes to filter.
190 * @param {string} textUpToCursor - The text up to cursor.
191 * @param {boolean} isForced - Whether autocomplete was force-triggered.
192 * @returns {UnclosedScope[]} - Filtered scopes (excludes optional scopes unless forced).
193 */
194function filterOptionalScopes(unclosedScopes, textUpToCursor, isForced) {
195 if (isForced) {
196 // When forced, show all scopes including optional ones
197 return unclosedScopes;
198 }
199
200 // Filter out scopes where the scope content is optional
201 return unclosedScopes.filter(scope => !isScopeOptional(scope, textUpToCursor));
202}
203
204/**
205 * Builds autocomplete options for variable shorthand syntax (.varName or $varName).
206 * @param {MacroAutoCompleteContext} context
207 * @param {Object} [opts] - Optional configuration.
208 * @param {boolean} [opts.forIfCondition=false] - If true, options are for {{if}} condition (closes with }}).
209 * @param {string} [opts.paddingAfter=''] - Whitespace to add before closing }}.
210 * @returns {AnyMacroAutoCompleteOption[]}
211 */
212export function buildVariableShorthandOptions(context, opts = {}) {
213 const { forIfCondition = false, paddingAfter = '' } = opts;
214 /** @type {AnyMacroAutoCompleteOption[]} */
215 const options = [];
216
217 const isLocal = context.variablePrefix === '.';
218 const scope = isLocal ? 'local' : 'global';
219
220
221 // Always show the typed variable prefix as a non-completable option (like flags do)
222 // This allows the details panel to show information about the prefix
223 const prefixDef = VariableShorthandDefinitions.get(context.variablePrefix);
224 if (prefixDef) {
225 const prefixOption = new VariableShorthandAutoCompleteOption(prefixDef);
226 prefixOption.valueProvider = () => ''; // Already typed, don't re-insert
227 prefixOption.makeSelectable = false;
228 prefixOption.sortPriority = 1; // Show at top
229 prefixOption.matchProvider = () => true; // Always show regardless of filtering
230 options.push(prefixOption);
231 }
232
233 // If typing the variable name, suggest existing variables
234 // Get existing variable names from the appropriate scope
235 // Filter to only include names that are valid for shorthand syntax
236 const existingVariables = getVariableNames(scope)
237 .filter(name => isValidVariableShorthandName(name));
238
239 // Check if the typed variable name exactly matches an existing variable
240 const variableNameMatchesExisting = context.variableName.length > 0 && existingVariables.includes(context.variableName);
241
242 if (context.isTypingVariableName) {
243 // Add existing variables that match the typed name
244 for (const varName of existingVariables) {
245 const option = new VariableNameAutoCompleteOption(varName, scope, false);
246 // Not selectable if it matches the typed name
247 if (varName === context.variableName) {
248 option.valueProvider = () => '';
249 option.makeSelectable = false;
250 }
251 // For {{if}} condition, provide full value with closing braces
252 if (forIfCondition) {
253 option.valueProvider = () => `${varName}${paddingAfter}}}`; // No variable prefix, as that has been written and committed already.
254 option.makeSelectable = true;
255 }
256 // Variables matching the typed prefix get higher priority
257 option.sortPriority = varName.startsWith(context.variableName) ? 3 : 10;
258 options.push(option);
259 }
260
261 // If typing a name that doesn't exist, offer to create a new variable
262 // But if the name is invalid for shorthand syntax, show a warning instead
263 if (context.variableName.length > 0 && !existingVariables.includes(context.variableName)) {
264 const isInvalid = !isValidVariableShorthandName(context.variableName);
265 const newVarOption = new VariableNameAutoCompleteOption(context.variableName, scope, true, isInvalid);
266 newVarOption.sortPriority = isInvalid ? 2 : 4; // Invalid names get higher priority to show warning
267 if (isInvalid) {
268 // Make it non-selectable since it can't be used
269 newVarOption.valueProvider = () => '';
270 newVarOption.makeSelectable = false;
271 } else if (forIfCondition) {
272 // For {{if}} condition, provide full value with closing braces
273 newVarOption.valueProvider = () => `${context.variablePrefix}${context.variableName}${paddingAfter}}}`;
274 newVarOption.makeSelectable = true;
275 }
276 options.push(newVarOption);
277 }
278
279 // If the typed variable name exactly matches an existing variable, also show operators
280 // This allows users to see available operators without having to type a space first
281 if (variableNameMatchesExisting) {
282 for (const [, operatorDef] of VariableOperatorDefinitions) {
283 const opOption = new VariableOperatorAutoCompleteOption(operatorDef);
284 opOption.sortPriority = 6; // Lower priority than variable suggestions
285 opOption.matchProvider = () => true; // Always show
286 // IMPORTANT: Operators should INSERT after variable name, not replace it
287 // Use replacementStartOffset to shift insertion point past the variable name
288 opOption.replacementStartOffset = context.variableName.length;
289 options.push(opOption);
290 }
291 }
292 }
293
294 // If there are invalid trailing characters after the variable name, show a warning
295 if (context.hasInvalidTrailingChars) {
296 // Show the full invalid name (variableName + invalidTrailingChars) with a warning
297 const fullInvalidName = context.variableName + (context.invalidTrailingChars || '');
298 const invalidOption = new VariableNameAutoCompleteOption(
299 fullInvalidName,
300 scope,
301 false,
302 true, // isInvalidName - triggers warning display
303 );
304 invalidOption.valueProvider = () => ''; // Don't insert anything
305 invalidOption.makeSelectable = false;
306 invalidOption.sortPriority = 2;
307 invalidOption.matchProvider = () => true; // Always show
308 options.push(invalidOption);
309 // Return early - don't show operators when syntax is invalid
310 return options;
311 }
312
313 // If ready for operator (after variable name), suggest operators
314 if (context.isTypingOperator) {
315 // Show the current variable name as context (already typed)
316 const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
317 varNameOption.valueProvider = () => ''; // Already typed, don't re-insert
318 varNameOption.makeSelectable = false;
319 varNameOption.sortPriority = 2;
320 varNameOption.matchProvider = () => true; // Always show
321 options.push(varNameOption);
322
323 // Then show available operators, filtered by partial prefix if any
324 // Also filter by current complete operator to show longer variants (e.g., > shows >=)
325 const partialOp = context.partialOperator || '';
326 const currentOp = context.variableOperator || '';
327 const filterPrefix = partialOp || currentOp;
328 for (const [, operatorDef] of VariableOperatorDefinitions) {
329 // Filter by operator prefix if user is typing one
330 // This allows typing ">" to show both ">" and ">="
331 if (filterPrefix && !operatorDef.symbol.startsWith(filterPrefix)) {
332 continue;
333 }
334 const opOption = new VariableOperatorAutoCompleteOption(operatorDef);
335 // Exact match gets higher priority
336 opOption.sortPriority = operatorDef.symbol === currentOp ? 4 : 5;
337 // Already-typed operator is non-selectable
338 if (operatorDef.symbol === currentOp) {
339 opOption.valueProvider = () => '';
340 opOption.makeSelectable = false;
341 }
342 // Always match operators when showing operator suggestions
343 opOption.matchProvider = () => true;
344 options.push(opOption);
345 }
346 }
347
348 // If typing value (after = or +=), no autocomplete needed - freeform text
349 // But we show the current context for reference (greyed out, non-selectable)
350 if (context.isTypingValue && !context.isTypingOperator && !context.isTypingClosingBrace) {
351 // Show the current variable name as context (non-selectable)
352 const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
353 varNameOption.valueProvider = () => ''; // Context only
354 varNameOption.makeSelectable = false;
355 varNameOption.sortPriority = 2;
356 varNameOption.matchProvider = () => true; // Always show
357 options.push(varNameOption);
358
359 // Show the operator that was used (non-selectable)
360 if (context.variableOperator) {
361 const opDef = VariableOperatorDefinitions.get(context.variableOperator);
362 if (opDef) {
363 const opOption = new VariableOperatorAutoCompleteOption(opDef);
364 opOption.valueProvider = () => ''; // Already typed
365 opOption.makeSelectable = false;
366 opOption.sortPriority = 3;
367 opOption.matchProvider = () => true; // Always show
368 options.push(opOption);
369
370 // Show value context info (non-selectable)
371 const valueOption = new VariableValueContextAutoCompleteOption(opDef, context.variableValue);
372 valueOption.valueProvider = () => ''; // Context only
373 valueOption.makeSelectable = false;
374 valueOption.sortPriority = 4;
375 valueOption.matchProvider = () => true; // Always show
376 options.push(valueOption);
377 }
378 }
379 }
380
381 // If operator is complete (++ or --), show context without value input (non-selectable)
382 if (context.isOperatorComplete && !context.isTypingOperator) {
383 // Show the current variable name as context (non-selectable)
384 const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
385 varNameOption.valueProvider = () => ''; // Context only
386 varNameOption.makeSelectable = false;
387 varNameOption.sortPriority = 2;
388 varNameOption.matchProvider = () => true; // Always show
389 options.push(varNameOption);
390
391 // Show the operator that was used (non-selectable)
392 if (context.variableOperator) {
393 const opDef = VariableOperatorDefinitions.get(context.variableOperator);
394 if (opDef) {
395 const opOption = new VariableOperatorAutoCompleteOption(opDef);
396 opOption.valueProvider = () => ''; // Already typed
397 opOption.makeSelectable = false;
398 opOption.sortPriority = 3;
399 opOption.matchProvider = () => true; // Always show
400 options.push(opOption);
401 }
402 }
403 }
404
405 // If typing closing brace on a variable shorthand (without operator), show the current state
406 // This handles cases like {{.Lila} or {{.Lila}}| where we want to show what was typed
407 if (context.isTypingClosingBrace && !context.isOperatorComplete && !context.isTypingOperator && !context.isTypingValue) {
408 // Show the current variable name as context (non-selectable)
409 const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
410 varNameOption.valueProvider = () => ''; // Context only
411 varNameOption.makeSelectable = false;
412 varNameOption.sortPriority = 2;
413 varNameOption.matchProvider = () => true; // Always show
414 options.push(varNameOption);
415 }
416
417 // If typing closing brace after a value operator (like {{.Lila+=4}} or {{.Lila+=4}),
418 // show the full context (variable + operator + value)
419 if (context.isTypingClosingBrace && context.variableOperator && context.isTypingValue) {
420 // Show the current variable name as context (non-selectable)
421 const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
422 varNameOption.valueProvider = () => ''; // Context only
423 varNameOption.makeSelectable = false;
424 varNameOption.sortPriority = 2;
425 varNameOption.matchProvider = () => true; // Always show
426 options.push(varNameOption);
427
428 // Show the operator that was used (non-selectable)
429 const opDef = VariableOperatorDefinitions.get(context.variableOperator);
430 if (opDef) {
431 const opOption = new VariableOperatorAutoCompleteOption(opDef);
432 opOption.valueProvider = () => ''; // Already typed
433 opOption.makeSelectable = false;
434 opOption.sortPriority = 3;
435 opOption.matchProvider = () => true; // Always show
436 options.push(opOption);
437
438 // Show value context info (non-selectable)
439 const valueOption = new VariableValueContextAutoCompleteOption(opDef, context.variableValue);
440 valueOption.valueProvider = () => ''; // Context only
441 valueOption.makeSelectable = false;
442 valueOption.sortPriority = 4;
443 valueOption.matchProvider = () => true; // Always show
444 options.push(valueOption);
445 }
446 }
447
448 return options;
449}
450
451/**
452 * Builds enhanced macro autocomplete options from the MacroRegistry.
453 * When in the flags area (before identifier), includes flag options.
454 * When typing arguments (after ::), prioritizes the exact macro match.
455 * @param {MacroAutoCompleteContext} context
456 * @param {string} [textUpToCursor] - Full document text up to cursor, for unclosed scope detection.
457 * @param {Object} [opts] - Additional options.
458 * @param {boolean} [opts.isForced=false] - Whether autocomplete was force-triggered (Ctrl+Space).
459 * @returns {AnyMacroAutoCompleteOption[]}
460 */
461export function buildEnhancedMacroOptions(context, textUpToCursor, { isForced = false } = {}) {
462 /** @type {AnyMacroAutoCompleteOption[]} */
463 const options = [];
464
465 if (context.isVariableShorthand) {
466 return buildVariableShorthandOptions(context);
467 }
468
469 // Check for unclosed scoped macros and suggest closing tags
470 // Iterate from innermost to outermost, adding optional scopes and stopping at first required scope
471 const unclosedScopes = findUnclosedScopes(textUpToCursor);
472 if (unclosedScopes.length > 0) {
473 let firstRequiredPriority = 1; // Priority for the first required (non-optional) scope
474 let optionalPriority = 3; // Lower priority for optional scopes
475 let foundRequired = false;
476 let elseOptionAdded = false;
477
478 // Iterate from innermost (last) to outermost (first)
479 for (let i = unclosedScopes.length - 1; i >= 0; i--) {
480 const scope = unclosedScopes[i];
481 const isOptional = isScopeOptional(scope, textUpToCursor);
482 const nestingLevel = unclosedScopes.length - 1 - i; // 0 = innermost
483
484 // If we've already found a required scope, stop adding more
485 if (foundRequired && !isOptional) break;
486
487 const closingOption = new MacroClosingTagAutoCompleteOption(scope.name, {
488 paddingBefore: scope.paddingBefore,
489 paddingAfter: scope.paddingAfter,
490 currentPadding: context.paddingBefore,
491 isOptional: isOptional,
492 nestingLevel: nestingLevel,
493 });
494
495 if (isOptional) {
496 closingOption.sortPriority = optionalPriority++;
497 } else {
498 // First required scope gets top priority
499 closingOption.sortPriority = firstRequiredPriority;
500 foundRequired = true;
501 }
502
503 options.push(closingOption);
504
505 // If inside a scoped {{if}}, also suggest {{else}} (only once, for innermost if)
506 if (!elseOptionAdded && scope.name === 'if') {
507 const macroDef = macroSystem.registry.getPrimaryMacro('else');
508 const elseOption = new EnhancedMacroAutoCompleteOption(macroDef);
509 elseOption.sortPriority = 2;
510 options.push(elseOption);
511 elseOptionAdded = true;
512 }
513
514 // Stop once we've added a required scope
515 if (foundRequired) break;
516 }
517 }
518
519 // If cursor is in the flags area (before identifier starts), include flag options
520 if (context.isInFlagsArea) {
521 // Build flag options with priority-based sorting
522 // Last typed flag has highest priority (1), other flags have lower priority (10)
523 // Already-typed flags (except last) are hidden from the list
524 const lastTypedFlag = context.flags.length > 0 ? context.flags[context.flags.length - 1] : null;
525
526 // Add last typed flag with high priority (so it appears at top)
527 if (lastTypedFlag) {
528 const lastFlagDef = MacroFlagDefinitions.get(lastTypedFlag);
529 if (lastFlagDef) {
530 const lastFlagOption = new MacroFlagAutoCompleteOption(lastFlagDef);
531 // Mark as already typed - valueProvider returns empty so it doesn't re-insert
532 lastFlagOption.valueProvider = () => '';
533 lastFlagOption.makeSelectable = false;
534 // High priority to appear at top (after closing tags at 1)
535 lastFlagOption.sortPriority = 2;
536 options.push(lastFlagOption);
537 }
538 }
539
540 // Add flags that haven't been typed yet (skip already-typed ones except last)
541 for (const [symbol, flagDef] of MacroFlagDefinitions) {
542 // Skip the last typed flag (already added above) and other already-typed flags
543 if (context.flags.includes(symbol)) {
544 continue;
545 }
546 const flagOption = new MacroFlagAutoCompleteOption(flagDef);
547
548 // Define whether this flag is selectable (and at the top), based on being implemented, and closing actually being relevant
549 let isSelectable = flagDef.implemented;
550 if (flagDef.type === MacroFlagType.CLOSING_BLOCK && !unclosedScopes.length) isSelectable = false;
551 if (!isSelectable) {
552 flagOption.valueProvider = () => '';
553 flagOption.makeSelectable = false;
554 }
555 // Normal flag priority
556 flagOption.sortPriority = isSelectable ? 10 : 12;
557 options.push(flagOption);
558 }
559
560 // Add variable shorthand prefix options (. for local, $ for global)
561 // These allow users to type variable shorthands instead of macro names
562 for (const [, varShorthandDef] of VariableShorthandDefinitions) {
563 const varOption = new VariableShorthandAutoCompleteOption(varShorthandDef);
564 varOption.sortPriority = 8; // Between implemented flags (10) and unimplemented (12)
565 options.push(varOption);
566 }
567 }
568
569 // Get all macros from the registry (excluding hidden aliases)
570 const allMacros = macroSystem.registry.getAllMacros({ excludeHiddenAliases: true });
571
572 // If we're typing arguments (after ::), only show the context to the matching macro
573 // Also treat typing closing brace the same way - show details for matching macro
574 const isTypingArgs = context.currentArgIndex >= 0;
575 const isTypingClosingBrace = context.isTypingClosingBrace ?? false;
576 const shouldShowMatchingMacroDetails = isTypingArgs || isTypingClosingBrace;
577
578 // Check if we're inside a scoped {{if}} for {{else}} selectability
579 const isInsideScopedIf = unclosedScopes.some(scope => scope.name === 'if');
580
581 // Track if any macro matches the identifier (for "no match" message)
582 let hasMatchingMacro = false;
583
584 for (const macro of allMacros) {
585 // Check if this macro matches the typed identifier
586 const isExactMatch = macro.name === context.identifier;
587 const isAliasMatch = macro.aliasOf === context.identifier;
588
589 if (isExactMatch || isAliasMatch) {
590 hasMatchingMacro = true;
591 }
592
593 // Only pass context to the macro that matches the identifier being typed
594 // This ensures argument hints only show for the relevant macro
595 /** @type {MacroAutoCompleteContext|EnhancedMacroAutoCompleteOptions|null} */
596 let macroContext = (isExactMatch || isAliasMatch) ? context : null;
597
598 // If no context, we pass some options for additional details though
599 if (!macroContext) {
600 macroContext = /** @type {EnhancedMacroAutoCompleteOptions} */ ({
601 paddingAfter: context.paddingBefore, // Match whitespace before the macro - will only be used if the macro gets auto-closed
602 flags: context.flags,
603 currentFlag: context.currentFlag,
604 fullText: context.fullText,
605 });
606 }
607
608 const option = new EnhancedMacroAutoCompleteOption(macro, macroContext);
609
610 // {{else}} is only selectable inside a scoped {{if}} block
611 // Outside of {{if}}, it should appear in the list but not be tab-completable
612 if (macro.name === 'else' && !isInsideScopedIf) {
613 option.valueProvider = () => '';
614 option.makeSelectable = false;
615 }
616
617 // When typing arguments or closing brace, prioritize exact matches by putting them first
618 if (shouldShowMatchingMacroDetails && (isExactMatch || isAliasMatch)) {
619 options.unshift(option);
620 } else {
621 options.push(option);
622 }
623 }
624
625 // If typing args/closing brace but no macro matches, check for closing macro context
626 if (shouldShowMatchingMacroDetails && !hasMatchingMacro && context.identifier.length > 0) {
627 // Check if this is a closing macro (starts with /) - show original macro's details
628 // Note: We look up the macro directly, not from unclosedScopes, because the closing tag
629 // itself may have already closed the scope by this point in the text
630 const isClosingMacro = context.identifier.startsWith('/');
631 const closingMacroName = isClosingMacro ? context.identifier.slice(1) : null;
632 const macroDef = closingMacroName ? macroSystem.registry.getPrimaryMacro(closingMacroName) : null;
633
634 if (macroDef) {
635 // Show the original macro's details for the closing tag
636 // Create a context that shows we're closing the scope (no argument highlight)
637 const closingContext = /** @type {MacroAutoCompleteContext} */ ({
638 ...context,
639 identifier: macroDef.name,
640 currentArgIndex: -1, // No argument highlight
641 isClosingTag: true,
642 });
643 const closingOption = new EnhancedMacroAutoCompleteOption(macroDef, closingContext);
644 closingOption.valueProvider = () => '';
645 closingOption.makeSelectable = false;
646 closingOption.matchProvider = () => true;
647 closingOption.sortPriority = 0;
648 options.unshift(closingOption);
649 hasMatchingMacro = true; // Prevent "no match" message
650 }
651
652 // Only show "no match" if we didn't find a matching closing scope
653 if (!hasMatchingMacro) {
654 const noMatchOption = new SimpleAutoCompleteOption({
655 name: context.identifier,
656 symbol: '❌',
657 description: `No macro found: "${context.identifier}"`,
658 detailedDescription: `The macro name <code>${context.identifier}</code> does not exist.<br><br>Check spelling or use a different macro name.`,
659 type: 'error',
660 });
661 noMatchOption.valueProvider = () => '';
662 noMatchOption.makeSelectable = false;
663 noMatchOption.matchProvider = () => true; // Always show
664 noMatchOption.sortPriority = 0; // Top priority
665 options.unshift(noMatchOption);
666 }
667 }
668
669 return options;
670}
671
672/**
673 * Builds autocomplete options for {{if}} condition - shows zero-arg macros as shorthand.
674 * @param {MacroAutoCompleteContext} context
675 * @param {MacroDefinition[]} allMacros
676 * @param {string} macroInnerText - The text inside the macro braces (e.g., " if pers" from "{{ if pers").
677 * @returns {AutoCompleteOption[]}
678 */
679export function buildIfConditionOptions(context, allMacros, macroInnerText) {
680 /** @type {AutoCompleteOption[]} */
681 const options = [];
682
683 // Calculate padding from the original macro text for matching whitespace on completion
684 // e.g., " if pers" -> leading padding = " " (whitespace before 'if', used before '}}')
685 const leadingMatch = macroInnerText.match(/^(\s*)/);
686 const paddingAfter = leadingMatch ? leadingMatch[1] : '';
687
688 // Get the condition text being typed (trimmed for detection)
689 const conditionText = (context.args[0] || '').trim();
690
691 // Check for inversion prefix (!) - also trim whitespace after !
692 const hasInversionPrefix = conditionText.startsWith('!');
693 const conditionAfterInversion = hasInversionPrefix ? conditionText.slice(1).trimStart() : conditionText;
694
695 const inversionOption = new SimpleAutoCompleteOption({
696 name: '!',
697 symbol: '🔁',
698 description: 'Invert condition (NOT)',
699 detailedDescription: 'Inverts the condition result. If the condition is truthy, it becomes falsy, and vice versa.<br><br>Example: <code>{{if !myVar}}</code> executes when <code>myVar</code> is empty or zero.',
700 type: 'inverse',
701 });
702
703 // Check if condition starts with a variable shorthand prefix (with or without !)
704 const isTypingVariableShorthand = conditionAfterInversion.startsWith('.') || conditionAfterInversion.startsWith('$');
705
706 if (isTypingVariableShorthand) {
707 // User is typing a variable shorthand - reuse #buildVariableShorthandOptions
708 const prefix = /** @type {'.'|'$'} */ (conditionAfterInversion[0]);
709 const varNameTyped = conditionAfterInversion.slice(1); // Variable name after the prefix
710
711 // If inverted, show the ! as non-selectable context
712 if (hasInversionPrefix) {
713 inversionOption.valueProvider = () => ''; // Already typed
714 inversionOption.makeSelectable = false;
715 inversionOption.sortPriority = 0;
716 options.push(inversionOption);
717 }
718
719 // Create a synthetic context for #buildVariableShorthandOptions
720 /** @type {MacroAutoCompleteContext} */
721 const varContext = {
722 ...context,
723 isVariableShorthand: true,
724 variablePrefix: prefix,
725 variableName: varNameTyped,
726 isTypingVariableName: true,
727 isTypingOperator: false,
728 isTypingValue: false,
729 isOperatorComplete: false,
730 hasInvalidTrailingChars: false,
731 variableOperator: null,
732 variableValue: '',
733 };
734
735 const varOptions = buildVariableShorthandOptions(varContext, { forIfCondition: true, paddingAfter });
736 options.push(...varOptions);
737 return options;
738 }
739
740 // Not typing a variable shorthand - show macro options, variable shorthand prefixes, and inversion
741
742 // Show ! inversion option at the top when nothing typed, or keep it visible (non-selectable) if already typed
743 if (conditionText.length === 0) {
744 // Nothing typed - offer ! as selectable option
745 inversionOption.valueProvider = () => '!';
746 inversionOption.makeSelectable = true;
747 inversionOption.sortPriority = -1; // Show at very top
748 options.push(inversionOption);
749 } else if (hasInversionPrefix && conditionAfterInversion.length === 0) {
750 // Just ! typed - show it as non-selectable context, then show macro names and variable prefixes
751 inversionOption.valueProvider = () => ''; // Already typed
752 inversionOption.makeSelectable = false;
753 inversionOption.sortPriority = -1;
754 options.push(inversionOption);
755 }
756
757 // Add variable shorthand prefix options when no content typed yet (or just ! typed)
758 if (conditionAfterInversion.length === 0) {
759 for (const [, prefixDef] of VariableShorthandDefinitions) {
760 const prefixOption = new VariableShorthandAutoCompleteOption(prefixDef);
761 // Complete with just the prefix symbol
762 prefixOption.valueProvider = () => prefixDef.type;
763 prefixOption.makeSelectable = true;
764 prefixOption.sortPriority = 0; // Show at top
765 options.push(prefixOption);
766 }
767 }
768
769 // Add zero-arg macros as condition shorthand options
770 for (const macro of allMacros) {
771 // Only include macros that require zero arguments (can be auto-resolved)
772 if (macro.minArgs !== 0) continue;
773
774 // Skip internal/utility macros that don't make sense as conditions
775 if (['else', 'noop', 'trim', '//'].includes(macro.name)) continue;
776
777 const option = new EnhancedMacroAutoCompleteOption(macro, {
778 noBraces: true,
779 paddingAfter,
780 closeWithBraces: true,
781 });
782 options.push(option);
783 }
784
785 return options;
786}
787
788/**
789 * Finds macro boundaries at a given cursor position in any text.
790 * Works independently of slash command parsing.
791 *
792 * @param {string} text - The full text content.
793 * @param {number} cursorPos - The cursor position in the text.
794 * @returns {{ start: number, end: number, content: string } | null}
795 */
796export function findMacroAtCursor(text, cursorPos) {
797 // Search backwards for opening {{ while tracking nesting depth for nested macros
798 let openPos = -1;
799 let depth = 0;
800
801 // If cursor is right after }}, those are the closing braces of the macro we're looking for,
802 // not nested braces. Skip them by starting the search before them.
803 let searchStart = cursorPos - 1;
804 let cursorAfterClosingBraces = false;
805 if (cursorPos >= 2 && text[cursorPos - 1] === '}' && text[cursorPos - 2] === '}') {
806 searchStart = cursorPos - 3; // Start before the }}
807 cursorAfterClosingBraces = true;
808 }
809
810 for (let i = searchStart; i >= 0; i--) {
811 if (text[i] === '}' && i > 0 && text[i - 1] === '}') {
812 // Found }}, going backwards means we're entering a nested macro
813 depth++;
814 i--; // Skip the other brace
815 continue;
816 }
817 if (text[i] === '{' && i > 0 && text[i - 1] === '{') {
818 if (depth > 0) {
819 // This {{ closes a nested macro we entered going backwards
820 depth--;
821 i--; // Skip the other brace
822 continue;
823 }
824 // Found our opening {{ at depth 0
825 openPos = i - 1;
826 break;
827 }
828 }
829
830 if (openPos === -1) return null;
831
832 // Search forwards for closing }} while tracking nesting depth
833 let closePos = -1;
834
835 // If cursor is right after }}, we already know where the closing braces are
836 if (cursorAfterClosingBraces) {
837 closePos = cursorPos;
838 } else {
839 depth = 0;
840 for (let i = cursorPos; i < text.length - 1; i++) {
841 if (text[i] === '{' && text[i + 1] === '{') {
842 // Found {{, entering a nested macro
843 depth++;
844 i++; // Skip the other brace
845 continue;
846 }
847 if (text[i] === '}' && text[i + 1] === '}') {
848 if (depth > 0) {
849 // This }} closes a nested macro
850 depth--;
851 i++; // Skip the other brace
852 continue;
853 }
854 // Found our closing }} at depth 0
855 closePos = i + 2;
856 break;
857 }
858 }
859
860 if (closePos === -1) {
861 closePos = text.length;
862 }
863 }
864
865 const hasClosingBraces = closePos <= text.length && text.slice(closePos - 2, closePos) === '}}';
866 const content = text.slice(openPos + 2, hasClosingBraces ? closePos - 2 : closePos);
867
868 return {
869 start: openPos,
870 end: closePos,
871 content,
872 };
873}
874
875/**
876 * Gets variable names from the specified scope.
877 *
878 * @param {'local'|'global'} scope - The variable scope.
879 * @returns {string[]} Array of variable names.
880 */
881export function getVariableNames(scope) {
882 try {
883 // Import chat_metadata and extension_settings dynamically to avoid circular deps
884 // These are the same sources used by commonEnumProviders.variables
885 if (scope === 'local') {
886 // Local variables are in chat_metadata.variables
887 return Object.keys(chat_metadata?.variables ?? {});
888 } else {
889 // Global variables are in extension_settings.variables.global
890 return Object.keys(extension_settings?.variables?.global ?? {});
891 }
892 } catch {
893 return [];
894 }
895}
896
897/**
898 * Core function to build macro autocomplete results.
899 * Used by both SlashCommandParser (slash command context) and MacroAutoComplete (free text).
900 *
901 * This is the shared implementation that handles:
902 * - Scoped content detection and context display
903 * - {{if}} condition special handling
904 * - Variable shorthand syntax (.var, $var)
905 * - Flag handling
906 * - Regular macro options
907 *
908 * @param {string} text - The full text content.
909 * @param {number} cursorPos - The cursor position.
910 * @param {BuildMacroAutoCompleteOptions} [options={}] - Optional pre-computed values.
911 * @returns {Promise<AutoCompleteNameResult|null>}
912 */
913export async function buildMacroAutoCompleteResult(text, cursorPos, {
914 macro = null,
915 textUpToCursor = null,
916 unclosedScopes = null,
917 isForced = false,
918} = {}) {
919 // Compute textUpToCursor if not provided
920 if (textUpToCursor === null) {
921 textUpToCursor = text.slice(0, cursorPos);
922 }
923
924 // Compute unclosedScopes if not provided
925 if (unclosedScopes === null) {
926 unclosedScopes = findUnclosedScopes(textUpToCursor);
927 }
928
929 // Filter out optional scopes unless forced (Ctrl+Space)
930 // This prevents intrusive hints for macros like {{trim}} where scope is optional
931 const filteredScopes = filterOptionalScopes(unclosedScopes, textUpToCursor, isForced);
932
933 // If cursor is NOT inside a macro, check if we're in scoped content
934 if (!macro) {
935 if (filteredScopes.length > 0) {
936 const scopedMacro = filteredScopes[filteredScopes.length - 1];
937
938 // Find where the opening macro ends
939 const openingEnd = text.indexOf('}}', scopedMacro.startOffset);
940 if (openingEnd !== -1 && cursorPos >= openingEnd + 2) {
941 // We're in scoped content - show parent macro's details
942 const macroContent = text.slice(scopedMacro.startOffset + 2, openingEnd);
943 const baseContext = parseMacroContext(macroContent, macroContent.length);
944
945 // Check if this scope is optional (for display purposes)
946 const scopeIsOptional = isScopeOptional(scopedMacro, textUpToCursor);
947
948 const scopedContext = {
949 ...baseContext,
950 currentArgIndex: baseContext.args.length,
951 isInScopedContent: true,
952 isScopedContentOptional: scopeIsOptional,
953 scopedMacroName: scopedMacro.name,
954 };
955
956 await onboardingExperimentalMacroEngine('scoped macros');
957
958 const macroDef = macroSystem.registry.getPrimaryMacro(scopedMacro.name);
959 if (macroDef) {
960 const scopedOption = new EnhancedMacroAutoCompleteOption(macroDef, scopedContext);
961 scopedOption.valueProvider = () => '';
962 scopedOption.makeSelectable = false;
963
964 return new AutoCompleteNameResult(
965 scopedMacro.name,
966 scopedMacro.startOffset + 2,
967 [scopedOption],
968 false,
969 );
970 }
971 }
972 }
973 return null;
974 }
975
976 // Cursor is inside a macro - parse context
977 const cursorInMacro = cursorPos - macro.start - 2;
978 const context = parseMacroContext(macro.content, cursorInMacro);
979
980 // Check if cursor is at/after closing }}
981 const macroEndsBrackets = text.slice(macro.end - 2, macro.end) === '}}';
982 const isCursorAtClosing = macroEndsBrackets && cursorPos >= macro.end - 1;
983
984 if (isCursorAtClosing) {
985 // Cursor is at the closing }} - check if this is an unclosed scoped macro
986 if (filteredScopes.length > 0) {
987 const scopedMacro = filteredScopes[filteredScopes.length - 1];
988 // Check if the current macro IS the unclosed scoped macro
989 if (scopedMacro.startOffset === macro.start) {
990 // Show scoped context - cursor is right at the end of the opening tag
991 // Check if this scope is optional (for display purposes)
992 const scopeIsOptional = isScopeOptional(scopedMacro, textUpToCursor);
993
994 const scopedContext = {
995 ...context,
996 currentArgIndex: context.args.length,
997 isInScopedContent: true,
998 isScopedContentOptional: scopeIsOptional,
999 scopedMacroName: scopedMacro.name,
1000 };
1001
1002 const macroDef = macroSystem.registry.getPrimaryMacro(scopedMacro.name);
1003 if (macroDef) {
1004 const scopedOption = new EnhancedMacroAutoCompleteOption(macroDef, scopedContext);
1005 scopedOption.valueProvider = () => '';
1006 scopedOption.makeSelectable = false;
1007
1008 return new AutoCompleteNameResult(
1009 scopedMacro.name,
1010 macro.start + 2,
1011 [scopedOption],
1012 false,
1013 );
1014 }
1015 }
1016 }
1017
1018 // Check if this is a closing tag ({{/macroName}}) - show original macro's details
1019 // Note: We look up the macro directly, not from unclosedScopes, because the closing tag
1020 // itself has already closed the scope by this point in the text
1021 if (context.identifier.startsWith('/')) {
1022 const closingMacroName = context.identifier.slice(1);
1023 const macroDef = macroSystem.registry.getPrimaryMacro(closingMacroName);
1024 if (macroDef) {
1025 const closingContext = /** @type {MacroAutoCompleteContext} */ ({
1026 ...context,
1027 identifier: macroDef.name,
1028 currentArgIndex: -1, // No argument highlight
1029 isClosingTag: true,
1030 });
1031 const closingOption = new EnhancedMacroAutoCompleteOption(macroDef, closingContext);
1032 closingOption.valueProvider = () => '';
1033 closingOption.makeSelectable = false;
1034
1035 return new AutoCompleteNameResult(
1036 macroDef.name,
1037 macro.start + 2,
1038 [closingOption],
1039 false,
1040 );
1041 }
1042 }
1043
1044 // Not a scoped macro, just clear arg highlighting
1045 context.currentArgIndex = -1;
1046 }
1047
1048 // Use the identifier from context (handles whitespace and flags)
1049 // Start position must be where the identifier actually begins (after whitespace/flags)
1050 // so that the autocomplete range calculation works correctly
1051 const identifier = context.identifier;
1052 const identifierStartInText = macro.start + 2 + context.identifierStart;
1053
1054 // Special case for {{if}} condition: use the condition text for matching/replacement
1055 const isTypingIfCondition = context.identifier === 'if' && context.currentArgIndex === 0;
1056 if (isTypingIfCondition) {
1057 // Get the typed condition text and calculate its start position
1058 const conditionText = context.args[0] || '';
1059 // Find where the condition argument starts in the macro text
1060 const separatorMatch = macro.content.match(/^.*?if\s*(?:::?)\s*/);
1061 const spaceMatch = macro.content.match(/^.*?if\s+/);
1062 let conditionStartOffset;
1063 if (separatorMatch) {
1064 conditionStartOffset = separatorMatch[0].length;
1065 } else if (spaceMatch) {
1066 conditionStartOffset = spaceMatch[0].length;
1067 } else {
1068 conditionStartOffset = context.identifierStart + identifier.length;
1069 }
1070 const conditionStartInText = macro.start + 2 + conditionStartOffset;
1071
1072 // Build if-condition options using macroContent for padding calculation
1073 const allMacros = macroSystem.registry.getAllMacros({ excludeHiddenAliases: true });
1074 const options = buildIfConditionOptions(context, allMacros, macro.content);
1075
1076 // For variable shorthand in {{if}} condition, adjust identifier and start position
1077 // Same fix as for regular variable shorthands - identifier must be just the var name
1078 // Also handle ! inversion prefix: !.var or !$var or !macroName
1079 const trimmedCondition = conditionText.trim();
1080 const hasInversion = trimmedCondition.startsWith('!');
1081 // Trim whitespace after ! to handle "! $myvar" syntax
1082 const conditionAfterInversion = hasInversion ? trimmedCondition.slice(1).trimStart() : trimmedCondition;
1083 const isTypingVarShorthand = conditionAfterInversion.startsWith('.') || conditionAfterInversion.startsWith('$');
1084 let resultIdentifier = conditionText;
1085 let resultStart = conditionStartInText;
1086
1087 if (isTypingVarShorthand) {
1088 // Identifier = just the variable name part (without prefix and without !)
1089 resultIdentifier = conditionAfterInversion.slice(1);
1090 // Start = after the ! (if any) and the prefix
1091 const prefixChar = conditionAfterInversion[0];
1092 const prefixPosInCondition = conditionText.indexOf(prefixChar, hasInversion ? 1 : 0);
1093 resultStart = conditionStartInText + prefixPosInCondition + 1;
1094 } else if (hasInversion && conditionAfterInversion.length === 0) {
1095 // Just ! (possibly with whitespace) typed - identifier should be empty so other options can match
1096 resultIdentifier = '';
1097 // Start at end of actual condition text (including any whitespace after !)
1098 // This ensures cursor is within the name range for filtering
1099 resultStart = conditionStartInText + conditionText.length;
1100 } else if (hasInversion && conditionAfterInversion.length > 0) {
1101 // Typing a macro name after ! (e.g., !descr) - identifier should be just the macro name
1102 resultIdentifier = conditionAfterInversion;
1103 // Start = after the ! and any whitespace, at the beginning of the macro name
1104 const macroNameStart = trimmedCondition.indexOf(conditionAfterInversion);
1105 resultStart = conditionStartInText + macroNameStart;
1106 }
1107
1108 await onboardingExperimentalMacroEngine('{{if}} macro');
1109
1110 return new AutoCompleteNameResult(
1111 resultIdentifier,
1112 resultStart,
1113 options,
1114 false,
1115 () => isTypingVarShorthand
1116 ? 'Enter a variable name for the condition'
1117 : 'Use {{macro}} syntax for dynamic conditions',
1118 () => isTypingVarShorthand
1119 ? 'Enter a variable name or select from the list'
1120 : 'Enter a macro name or {{macro}} for the condition',
1121 );
1122 }
1123
1124 // Build regular macro options
1125 /** @type {()=>string|undefined} */
1126 let makeNoMatchText = undefined;
1127 /** @type {()=>string|undefined} */
1128 let makeNoOptionsText = undefined;
1129
1130 const options = buildEnhancedMacroOptions(context, textUpToCursor);
1131
1132 // For variable shorthands, calculate the correct identifier and start position
1133 // based on what the user is currently typing (variable name, operator, or value)
1134 let resultIdentifier = identifier;
1135 let resultStart = identifierStartInText;
1136 if (context.isVariableShorthand && context.variablePrefix) {
1137 // Find where the prefix is in the macro content
1138 const prefixIndex = macro.content.indexOf(context.variablePrefix);
1139
1140 if (context.isTypingVariableName) {
1141 // Typing variable name: identifier = variableName, start = after prefix
1142 resultIdentifier = context.variableName;
1143 if (prefixIndex >= 0) {
1144 resultStart = macro.start + 2 + prefixIndex + 1; // +1 to skip the prefix
1145 }
1146 } else if (context.isTypingOperator) {
1147 // Typing operator: identifier = partial operator or current operator, start = after variable name
1148 resultIdentifier = context.partialOperator || context.variableOperator || '';
1149 // Use actual variableNameEnd position from parsing (accounts for whitespace)
1150 resultStart = macro.start + 2 + context.variableNameEnd;
1151 // Skip whitespace between variable name and operator
1152 while (resultStart < cursorPos && /\s/.test(text[resultStart])) {
1153 resultStart++;
1154 }
1155 } else if (context.isOperatorComplete) {
1156 // Operator complete (++ or --) - show context but no value input needed
1157 resultIdentifier = '';
1158 resultStart = cursorPos; // Cursor at end
1159 } else if (context.hasInvalidTrailingChars) {
1160 // Invalid chars after variable name: show the invalid chars for warning
1161 resultIdentifier = context.invalidTrailingChars || '';
1162 // Use actual variableNameEnd position from parsing
1163 resultStart = macro.start + 2 + context.variableNameEnd;
1164 } else if (context.isTypingValue && !context.isTypingClosingBrace) {
1165 // Typing value: identifier = value being typed, start = after operator
1166 resultIdentifier = context.variableValue;
1167 // Use actual operatorEnd position from parsing (accounts for whitespace)
1168 resultStart = macro.start + 2 + context.variableOperatorEnd;
1169 // Skip any whitespace between operator and value
1170 while (resultStart < cursorPos && /\s/.test(text[resultStart])) {
1171 resultStart++;
1172 }
1173
1174 makeNoMatchText = () => `Type any value you want to ${context.variableOperator == '+=' ? `add to the variable '${context.variableName}'` : `set the variable '${context.variableName}' to`}.`;
1175 makeNoOptionsText = () => 'Enter a variable value';
1176 } else if (context.isTypingClosingBrace) {
1177 // Typing closing brace on variable shorthand - show context, no replacement needed
1178 resultIdentifier = '';
1179 resultStart = cursorPos;
1180 } else {
1181 // Fallback: use variable name
1182 resultIdentifier = context.variableName;
1183 if (prefixIndex >= 0) {
1184 resultStart = macro.start + 2 + prefixIndex + 1;
1185 }
1186 }
1187
1188 if (!makeNoMatchText && !makeNoOptionsText) {
1189 makeNoMatchText = () => 'Invalid syntax or variable name (must be alphanumeric, not ending in hyphen or underscore). Use a valid macro name or syntax.';
1190 makeNoOptionsText = () => 'Enter a variable name to create or use a new variable';
1191 }
1192 }
1193
1194 return new AutoCompleteNameResult(
1195 resultIdentifier,
1196 resultStart,
1197 options,
1198 false,
1199 makeNoMatchText,
1200 makeNoOptionsText,
1201 );
1202}
1203
1204/**
1205 * Entry point for macro autocomplete in free text contexts.
1206 * Finds the macro at cursor position and delegates to the shared builder.
1207 *
1208 * @param {string} text - The full text content.
1209 * @param {number} cursorPos - The cursor position.
1210 * @param {Object} [options={}] - Additional options.
1211 * @param {boolean} [options.isForced=false] - Whether autocomplete was force-triggered (Ctrl+Space).
1212 * @returns {Promise<AutoCompleteNameResult|null>}
1213 */
1214export async function getMacroAutoCompleteAt(text, cursorPos, { isForced = false } = {}) {
1215 const macro = findMacroAtCursor(text, cursorPos);
1216 return buildMacroAutoCompleteResult(text, cursorPos, { macro, isForced });
1217}
public/scripts/backgrounds.js+129 -3
@@ -3,7 +3,7 @@ import { characters, chat_metadata, eventSource, event_types, generateQuietPromp
3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
4import { SlashCommand } from './slash-commands/SlashCommand.js';4import { SlashCommand } from './slash-commands/SlashCommand.js';
5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension } from './utils.js';6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension, sortIgnoreCaseAndAccents } from './utils.js';
7import { debounce_timeout } from './constants.js';7import { debounce_timeout } from './constants.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { Popup } from './popup.js';9import { Popup } from './popup.js';
@@ -42,6 +42,12 @@ const THUMBNAIL_CONFIG = {
42};42};
4343
44/**44/**
45 * Cache for image metadata.
46 * @type {Map<string, import('../../src/endpoints/image-metadata.js').ImageMetadata>}
47 */
48const METADATA_CACHE = new Map();
49
50/**
45 * Background source types.51 * Background source types.
46 * @readonly52 * @readonly
47 * @enum {number}53 * @enum {number}
@@ -52,6 +58,18 @@ const BG_SOURCES = {
52};58};
5359
54/**60/**
61 * Background sorting options.
62 * @readonly
63 * @enum {string}
64 */
65const BG_SORT_OPTIONS = {
66 AZ: 'az',
67 ZA: 'za',
68 NEWEST: 'newest',
69 OLDEST: 'oldest',
70};
71
72/**
55 * Mapping of background sources to their corresponding tab IDs.73 * Mapping of background sources to their corresponding tab IDs.
56 * @readonly74 * @readonly
57 * @type {Record<string, string>}75 * @type {Record<string, string>}
@@ -67,14 +85,56 @@ const BG_TABS = Object.freeze({
67 */85 */
68let lazyLoadObserver = null;86let lazyLoadObserver = null;
6987
88/**
89 * Cache for the current list of system background filenames.
90 * Used to re-sort backgrounds without refetching from the server.
91 * @type {string[]}
92 */
93let cachedSystemBackgrounds = [];
94
70export let background_settings = {95export let background_settings = {
71 name: '__transparent.png',96 name: '__transparent.png',
72 url: generateUrlParameter('__transparent.png', false),97 url: generateUrlParameter('__transparent.png', false),
73 fitting: 'classic',98 fitting: 'classic',
74 animation: false,99 animation: false,
100 sortOrder: BG_SORT_OPTIONS.AZ,
75};101};
76102
77/**103/**
104 * Sorts an array of background filenames based on the current sort order.
105 * @param {string[]} backgrounds - Array of background filenames
106 * @param {boolean} isCustom - Whether these are custom (chat) backgrounds
107 * @returns {string[]} Sorted array of background filenames
108 */
109function sortBackgrounds(backgrounds, isCustom = false) {
110 const sortOrder = background_settings.sortOrder || BG_SORT_OPTIONS.AZ;
111
112 return [...backgrounds].sort((a, b) => {
113 switch (sortOrder) {
114 case BG_SORT_OPTIONS.AZ:
115 return sortIgnoreCaseAndAccents(a, b);
116 case BG_SORT_OPTIONS.ZA:
117 return sortIgnoreCaseAndAccents(b, a);
118 case BG_SORT_OPTIONS.NEWEST:
119 case BG_SORT_OPTIONS.OLDEST: {
120 const keyA = isCustom ? a : `backgrounds/${a}`;
121 const keyB = isCustom ? b : `backgrounds/${b}`;
122 const metaA = METADATA_CACHE.get(keyA);
123 const metaB = METADATA_CACHE.get(keyB);
124 const timestampA = metaA?.addedTimestamp ?? 0;
125 const timestampB = metaB?.addedTimestamp ?? 0;
126 // Newest first (descending) or oldest first (ascending)
127 return sortOrder === BG_SORT_OPTIONS.NEWEST
128 ? timestampB - timestampA
129 : timestampA - timestampB;
130 }
131 default:
132 return 0;
133 }
134 });
135}
136
137/**
78 * Creates a single thumbnail DOM element. The CSS now handles all sizing.138 * Creates a single thumbnail DOM element. The CSS now handles all sizing.
79 * @param {object} imageData - Data for the image (filename, isCustom).139 * @param {object} imageData - Data for the image (filename, isCustom).
80 * @returns {HTMLElement} The created thumbnail element.140 * @returns {HTMLElement} The created thumbnail element.
@@ -89,6 +149,18 @@ function createThumbnailElement(imageData) {
89 clipper.className = 'thumbnail-clipper lazy-load-background';149 clipper.className = 'thumbnail-clipper lazy-load-background';
90 clipper.style.backgroundImage = PLACEHOLDER_IMAGE;150 clipper.style.backgroundImage = PLACEHOLDER_IMAGE;
91151
152 // Apply dominant color and aspect ratio as placeholder if available
153 const metadataKey = isCustom ? bg : `backgrounds/${bg}`;
154 const metadata = METADATA_CACHE.get(metadataKey);
155 if (metadata) {
156 if (metadata.dominantColor) {
157 clipper.style.backgroundColor = metadata.dominantColor;
158 }
159 if (metadata.aspectRatio) {
160 thumbnail.css('aspect-ratio', metadata.aspectRatio);
161 }
162 }
163
92 const titleElement = thumbnail.find('.BGSampleTitle');164 const titleElement = thumbnail.find('.BGSampleTitle');
93 clipper.appendChild(titleElement.get(0));165 clipper.appendChild(titleElement.get(0));
94 thumbnail.append(clipper);166 thumbnail.append(clipper);
@@ -132,6 +204,9 @@ export function loadBackgroundSettings(settings) {
132 if (!Object.hasOwn(backgroundSettings, 'animation')) {204 if (!Object.hasOwn(backgroundSettings, 'animation')) {
133 backgroundSettings.animation = false;205 backgroundSettings.animation = false;
134 }206 }
207 if (!backgroundSettings.sortOrder) {
208 backgroundSettings.sortOrder = BG_SORT_OPTIONS.AZ;
209 }
135210
136 // If a value is already saved, use it. Otherwise, determine default based on screen size.211 // If a value is already saved, use it. Otherwise, determine default based on screen size.
137 let columns = backgroundSettings.thumbnailColumns;212 let columns = backgroundSettings.thumbnailColumns;
@@ -140,12 +215,14 @@ export function loadBackgroundSettings(settings) {
140 columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP;215 columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP;
141 }216 }
142 background_settings.thumbnailColumns = columns;217 background_settings.thumbnailColumns = columns;
218 background_settings.sortOrder = backgroundSettings.sortOrder;
143 applyThumbnailColumns(background_settings.thumbnailColumns);219 applyThumbnailColumns(background_settings.thumbnailColumns);
144220
145 setBackground(backgroundSettings.name, backgroundSettings.url);221 setBackground(backgroundSettings.name, backgroundSettings.url);
146 setFittingClass(backgroundSettings.fitting);222 setFittingClass(backgroundSettings.fitting);
147 $('#background_fitting').val(backgroundSettings.fitting);223 $('#background_fitting').val(backgroundSettings.fitting);
148 $('#background_thumbnails_animation').prop('checked', background_settings.animation);224 $('#background_thumbnails_animation').prop('checked', background_settings.animation);
225 $('#bg-sort').val(background_settings.sortOrder);
149 highlightSelectedBackground();226 highlightSelectedBackground();
150}227}
151228
@@ -429,6 +506,11 @@ async function onDeleteBackgroundClick(e) {
429 // If it's not custom, it's a built-in background. Delete it from the server506 // If it's not custom, it's a built-in background. Delete it from the server
430 if (!isCustom) {507 if (!isCustom) {
431 await delBackground(bg);508 await delBackground(bg);
509 // Remove from cache to prevent reappearing on sort change
510 const cacheIndex = cachedSystemBackgrounds.indexOf(bg);
511 if (cacheIndex !== -1) {
512 cachedSystemBackgrounds.splice(cacheIndex, 1);
513 }
432 } else {514 } else {
433 const list = chat_metadata[LIST_METADATA_KEY] || [];515 const list = chat_metadata[LIST_METADATA_KEY] || [];
434 const index = list.indexOf(bg);516 const index = list.indexOf(bg);
@@ -517,7 +599,8 @@ function renderSystemBackgrounds(backgrounds) {
517599
518 if (sourceList.length === 0) return;600 if (sourceList.length === 0) return;
519601
520 sourceList.forEach(bg => {602 const sortedList = sortBackgrounds(sourceList, false);
603 sortedList.forEach(bg => {
521 const imageData = { filename: bg, isCustom: false };604 const imageData = { filename: bg, isCustom: false };
522 const thumbnail = createThumbnailElement(imageData);605 const thumbnail = createThumbnailElement(imageData);
523 container.append(thumbnail);606 container.append(thumbnail);
@@ -538,7 +621,8 @@ function renderChatBackgrounds(backgrounds) {
538621
539 if (sourceList.length === 0) return;622 if (sourceList.length === 0) return;
540623
541 sourceList.forEach(bg => {624 const sortedList = sortBackgrounds(sourceList, true);
625 sortedList.forEach(bg => {
542 const imageData = { filename: bg, isCustom: true };626 const imageData = { filename: bg, isCustom: true };
543 const thumbnail = createThumbnailElement(imageData);627 const thumbnail = createThumbnailElement(imageData);
544 container.append(thumbnail);628 container.append(thumbnail);
@@ -548,6 +632,8 @@ function renderChatBackgrounds(backgrounds) {
548}632}
549633
550export async function getBackgrounds() {634export async function getBackgrounds() {
635 const metadataPromise = preloadImageMetadata();
636
551 const response = await fetch('/api/backgrounds/all', {637 const response = await fetch('/api/backgrounds/all', {
552 method: 'POST',638 method: 'POST',
553 headers: getRequestHeaders(),639 headers: getRequestHeaders(),
@@ -557,11 +643,40 @@ export async function getBackgrounds() {
557 const { images, config } = await response.json();643 const { images, config } = await response.json();
558 Object.assign(THUMBNAIL_CONFIG, config);644 Object.assign(THUMBNAIL_CONFIG, config);
559645
646 cachedSystemBackgrounds = images;
647
648 await metadataPromise;
649
560 renderSystemBackgrounds(images);650 renderSystemBackgrounds(images);
561 highlightSelectedBackground();651 highlightSelectedBackground();
562 }652 }
563}653}
564654
655/**
656 * Preloads all image metadata to use dominant colors as placeholders.
657 * @return {Promise<void>}
658 */
659async function preloadImageMetadata() {
660 try {
661 const response = await fetch('/api/image-metadata/all', {
662 method: 'POST',
663 headers: getRequestHeaders(),
664 body: JSON.stringify({ prefix: 'backgrounds/' }),
665 });
666 if (response.ok) {
667 const data = await response.json();
668 if (data?.images) {
669 METADATA_CACHE.clear();
670 for (const [path, metadata] of Object.entries(data.images)) {
671 METADATA_CACHE.set(path, metadata);
672 }
673 }
674 }
675 } catch (error) {
676 console.error('[ImageMetadata] Failed to preload metadata:', error);
677 }
678}
679
565function activateLazyLoader() {680function activateLazyLoader() {
566 // Disconnect previous observer to prevent memory leaks681 // Disconnect previous observer to prevent memory leaks
567 if (lazyLoadObserver) {682 if (lazyLoadObserver) {
@@ -921,6 +1036,17 @@ export function initBackgrounds() {
921 $('#auto_background').on('click', autoBackgroundCommand);1036 $('#auto_background').on('click', autoBackgroundCommand);
922 $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent));1037 $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent));
923 $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput());1038 $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput());
1039 $('#bg-sort').on('change', function () {
1040 background_settings.sortOrder = String($(this).val());
1041 saveSettingsDebounced();
1042 // Re-render both galleries with new sort order
1043 renderSystemBackgrounds(cachedSystemBackgrounds);
1044 renderChatBackgrounds();
1045 highlightSelectedBackground();
1046 highlightLockedBackground();
1047 // Re-apply any active search filter
1048 onBackgroundFilterInput();
1049 });
924 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1050 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
925 name: 'lockbg',1051 name: 'lockbg',
926 callback: () => {1052 callback: () => {
public/scripts/bookmarks.js+43 -23
@@ -12,6 +12,7 @@ import {
12 saveChatConditional,12 saveChatConditional,
13 saveItemizedPrompts,13 saveItemizedPrompts,
14 setActiveGroup,14 setActiveGroup,
15 getCurrentChatDetails,
15} from '../script.js';16} from '../script.js';
16import { humanizedDateTime } from './RossAscends-mods.js';17import { humanizedDateTime } from './RossAscends-mods.js';
17import {18import {
@@ -81,30 +82,35 @@ async function getExistingChatNames() {
81}82}
8283
83async function getBookmarkName({ isReplace = false, forceName = null } = {}) {84async function getBookmarkName({ isReplace = false, forceName = null } = {}) {
84 const chatNames = await getExistingChatNames();85 const mainChatName = (getCurrentChatDetails()).sessionName;
8586
86 const body = await renderTemplateAsync('createCheckpoint', { isReplace: isReplace });87 function buildCheckpointName(name, i) {
87 let name = forceName ?? await Popup.show.input('Create Checkpoint', body);88 // Strip off existing suffixes, then build new name
89 let cleanName = name.replace(new RegExp(` - ${bookmarkNameToken}\\d+$`), '');
90 // Strip off legacy old name prefix too
91 cleanName = cleanName.replace(new RegExp(`^${bookmarkNameToken}\\d+ - `), '');
92 return `${cleanName} - ${bookmarkNameToken}${i}`;
93 }
94 const existingChats = await getExistingChatNames();
95 const suggestedName = getUniqueName(mainChatName, (x) => existingChats.includes(x), { nameBuilder: buildCheckpointName });
96
97 const body = await renderTemplateAsync('createCheckpoint', { isReplace: isReplace, suggestedName: suggestedName });
98 let name = forceName ?? await Popup.show.input('Create Checkpoint', body, suggestedName);
88 // Special handling for confirmed empty input (=> auto-generate name)99 // Special handling for confirmed empty input (=> auto-generate name)
89 if (name === '') {100 if (name === '') {
90 for (let i = chatNames.length; i < 1000; i++) {101 name = suggestedName;
91 name = bookmarkNameToken + i;
92 if (!chatNames.includes(name)) {
93 break;
94 }
95 }
96 }102 }
97 if (!name) {103 if (!name) {
98 return null;104 return null;
99 }105 }
100106
101 return `${name} - ${humanizedDateTime()}`;107 return name;
102}108}
103109
104function getMainChatName() {110function getMainChatName() {
105 if (chat_metadata) {111 if (chat_metadata) {
106 if (chat_metadata['main_chat']) {112 if (chat_metadata.main_chat) {
107 return chat_metadata['main_chat'];113 return chat_metadata.main_chat;
108 }114 }
109 // groups didn't support bookmarks before chat metadata was introduced115 // groups didn't support bookmarks before chat metadata was introduced
110 else if (selected_group) {116 else if (selected_group) {
@@ -112,8 +118,8 @@ function getMainChatName() {
112 }118 }
113 else if (characters[this_chid].chat && characters[this_chid].chat.includes(bookmarkNameToken)) {119 else if (characters[this_chid].chat && characters[this_chid].chat.includes(bookmarkNameToken)) {
114 const tokenIndex = characters[this_chid].chat.lastIndexOf(bookmarkNameToken);120 const tokenIndex = characters[this_chid].chat.lastIndexOf(bookmarkNameToken);
115 chat_metadata['main_chat'] = characters[this_chid].chat.substring(0, tokenIndex).trim();121 chat_metadata.main_chat = characters[this_chid].chat.substring(0, tokenIndex).trim();
116 return chat_metadata['main_chat'];122 return chat_metadata.main_chat;
117 }123 }
118 }124 }
119 return null;125 return null;
@@ -127,7 +133,7 @@ export function showBookmarksButtons() {
127 $('#option_convert_to_group').show();133 $('#option_convert_to_group').show();
128 }134 }
129135
130 if (chat_metadata['main_chat']) {136 if (chat_metadata.main_chat) {
131 // In bookmark chat137 // In bookmark chat
132 $('#option_back_to_main').show();138 $('#option_back_to_main').show();
133 $('#option_new_bookmark').show();139 $('#option_new_bookmark').show();
@@ -170,9 +176,23 @@ export async function createBranch(mesId) {
170 }176 }
171177
172 const lastMes = chat[mesId];178 const lastMes = chat[mesId];
173 const mainChat = selected_group ? groups?.find(x => x.id == selected_group)?.chat_id : characters[this_chid].chat;179 const mainChatName = (getCurrentChatDetails()).sessionName;
174 const newMetadata = { main_chat: mainChat };180 const newMetadata = { main_chat: mainChatName };
175 let name = `Branch #${mesId} - ${humanizedDateTime()}`;181
182 function buildBranchName(name, i) {
183 // Strip off existing suffixes, then build new name
184 let cleanName = name.replace(/ - Branch #\d+$/, '');
185 // Strip off legacy old name prefix too
186 cleanName = cleanName.replace(/^Branch #\d+ - /, '');
187 return `${cleanName} - Branch #${i}`;
188 }
189 const existingChats = await getExistingChatNames();
190 const name = getUniqueName(mainChatName, (x) => existingChats.includes(x), { nameBuilder: buildBranchName });
191 if (!name) {
192 console.error('Could not generate a unique branch name.');
193 toastr.error('Could not generate a unique branch name.', 'Branch creation failed');
194 return;
195 }
176196
177 if (selected_group) {197 if (selected_group) {
178 await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);198 await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);
@@ -184,10 +204,10 @@ export async function createBranch(mesId) {
184 if (typeof lastMes.extra !== 'object') {204 if (typeof lastMes.extra !== 'object') {
185 lastMes.extra = {};205 lastMes.extra = {};
186 }206 }
187 if (typeof lastMes.extra['branches'] !== 'object') {207 if (typeof lastMes.extra.branches !== 'object') {
188 lastMes.extra['branches'] = [];208 lastMes.extra.branches = [];
189 }209 }
190 lastMes.extra['branches'].push(name);210 lastMes.extra.branches.push(name);
191 return name;211 return name;
192}212}
193213
@@ -236,7 +256,7 @@ export async function createNewBookmark(mesId, { forceName = null } = {}) {
236 await saveChat({ chatName: name, withMetadata: newMetadata, mesId });256 await saveChat({ chatName: name, withMetadata: newMetadata, mesId });
237 }257 }
238258
239 lastMes.extra['bookmark_link'] = name;259 lastMes.extra.bookmark_link = name;
240260
241 const mes = $(`.mes[mesid="${mesId}"]`);261 const mes = $(`.mes[mesid="${mesId}"]`);
242 updateBookmarkDisplay(mes, name);262 updateBookmarkDisplay(mes, name);
@@ -636,7 +656,7 @@ export function initBookmarks() {
636656
637 const fileName = $(this).hasClass('mes_bookmark')657 const fileName = $(this).hasClass('mes_bookmark')
638 ? $(this).closest('.mes').attr('bookmark_link')658 ? $(this).closest('.mes').attr('bookmark_link')
639 : $(this).attr('file_name').replace('.jsonl', '');659 : $(this).attr('file_name');
640660
641 if (!fileName) {661 if (!fileName) {
642 return;662 return;
public/scripts/cfg-scale.js+14 -14
@@ -42,13 +42,13 @@ function setCharCfg(tempValue, setting) {
4242
43 switch (setting) {43 switch (setting) {
44 case settingType.guidance_scale:44 case settingType.guidance_scale:
45 tempCharaCfg['guidance_scale'] = Number(tempValue);45 tempCharaCfg.guidance_scale = Number(tempValue);
46 break;46 break;
47 case settingType.negative_prompt:47 case settingType.negative_prompt:
48 tempCharaCfg['negative_prompt'] = tempValue;48 tempCharaCfg.negative_prompt = tempValue;
49 break;49 break;
50 case settingType.positive_prompt:50 case settingType.positive_prompt:
51 tempCharaCfg['positive_prompt'] = tempValue;51 tempCharaCfg.positive_prompt = tempValue;
52 break;52 break;
53 default:53 default:
54 return false;54 return false;
@@ -239,31 +239,31 @@ function migrateSettings() {
239239
240 if (power_user.guidance_scale) {240 if (power_user.guidance_scale) {
241 extension_settings.cfg.global.guidance_scale = power_user.guidance_scale;241 extension_settings.cfg.global.guidance_scale = power_user.guidance_scale;
242 delete power_user['guidance_scale'];242 delete power_user.guidance_scale;
243 performSettingsSave = true;243 performSettingsSave = true;
244 }244 }
245245
246 if (power_user.negative_prompt) {246 if (power_user.negative_prompt) {
247 extension_settings.cfg.global.negative_prompt = power_user.negative_prompt;247 extension_settings.cfg.global.negative_prompt = power_user.negative_prompt;
248 delete power_user['negative_prompt'];248 delete power_user.negative_prompt;
249 performSettingsSave = true;249 performSettingsSave = true;
250 }250 }
251251
252 if (chat_metadata['cfg_negative_combine']) {252 if (chat_metadata.cfg_negative_combine) {
253 chat_metadata[metadataKeys.prompt_combine] = chat_metadata['cfg_negative_combine'];253 chat_metadata[metadataKeys.prompt_combine] = chat_metadata.cfg_negative_combine;
254 chat_metadata['cfg_negative_combine'] = undefined;254 chat_metadata.cfg_negative_combine = undefined;
255 performMetaSave = true;255 performMetaSave = true;
256 }256 }
257257
258 if (chat_metadata['cfg_negative_insertion_depth']) {258 if (chat_metadata.cfg_negative_insertion_depth) {
259 chat_metadata[metadataKeys.prompt_insertion_depth] = chat_metadata['cfg_negative_insertion_depth'];259 chat_metadata[metadataKeys.prompt_insertion_depth] = chat_metadata.cfg_negative_insertion_depth;
260 chat_metadata['cfg_negative_insertion_depth'] = undefined;260 chat_metadata.cfg_negative_insertion_depth = undefined;
261 performMetaSave = true;261 performMetaSave = true;
262 }262 }
263263
264 if (chat_metadata['cfg_negative_separator']) {264 if (chat_metadata.cfg_negative_separator) {
265 chat_metadata[metadataKeys.prompt_separator] = chat_metadata['cfg_negative_separator'];265 chat_metadata[metadataKeys.prompt_separator] = chat_metadata.cfg_negative_separator;
266 chat_metadata['cfg_negative_separator'] = undefined;266 chat_metadata.cfg_negative_separator = undefined;
267 performMetaSave = true;267 performMetaSave = true;
268 }268 }
269269
public/scripts/chat-templates.js+6 -6
@@ -148,8 +148,8 @@ export async function bindModelTemplates(power_user, online_status) {
148 ?? power_user.model_templates_mappings[chatTemplateHash]148 ?? power_user.model_templates_mappings[chatTemplateHash]
149 ?? {};149 ?? {};
150 const bindingsMatch = bindModelTemplates150 const bindingsMatch = bindModelTemplates
151 && power_user.context.preset == bindModelTemplates['context']151 && power_user.context.preset == bindModelTemplates.context
152 && (!power_user.instruct.enabled || power_user.instruct.preset === bindModelTemplates['instruct']);152 && (!power_user.instruct.enabled || power_user.instruct.preset === bindModelTemplates.instruct);
153153
154 const bound = [];154 const bound = [];
155155
@@ -160,21 +160,21 @@ export async function bindModelTemplates(power_user, online_status) {
160 toastr.info(t`Context preset for ${online_status} will use defaults when loaded the next time.`);160 toastr.info(t`Context preset for ${online_status} will use defaults when loaded the next time.`);
161 } else {161 } else {
162 if (power_user.context_derived) {162 if (power_user.context_derived) {
163 if (power_user.context.preset !== bindModelTemplates['context']) {163 if (power_user.context.preset !== bindModelTemplates.context) {
164 bound.push(`${power_user.context.preset} context preset`);164 bound.push(`${power_user.context.preset} context preset`);
165 // toastr.info(`Bound ${power_user.context.preset} preset to currently loaded model and all models that share its chat template.`);165 // toastr.info(`Bound ${power_user.context.preset} preset to currently loaded model and all models that share its chat template.`);
166166
167 // map current preset to current chat template hash167 // map current preset to current chat template hash
168 bindModelTemplates['context'] = power_user.context.preset;168 bindModelTemplates.context = power_user.context.preset;
169 }169 }
170 } else {170 } else {
171 toastr.warning(t`Note: Context derivation is disabled. Not including context preset.`);171 toastr.warning(t`Note: Context derivation is disabled. Not including context preset.`);
172 }172 }
173 if (power_user.instruct.enabled) {173 if (power_user.instruct.enabled) {
174 if (power_user.instruct_derived) {174 if (power_user.instruct_derived) {
175 if (power_user.instruct.preset !== bindModelTemplates['instruct']) {175 if (power_user.instruct.preset !== bindModelTemplates.instruct) {
176 bound.push(`${power_user.instruct.preset} instruct preset`);176 bound.push(`${power_user.instruct.preset} instruct preset`);
177 bindModelTemplates['instruct'] = power_user.instruct.preset;177 bindModelTemplates.instruct = power_user.instruct.preset;
178 }178 }
179 } else {179 } else {
180 toastr.warning(t`Note: Instruct derivation is disabled. Not including instruct preset.`);180 toastr.warning(t`Note: Instruct derivation is disabled. Not including instruct preset.`);
public/scripts/chats.js+9 -4
@@ -685,7 +685,7 @@ export function formatCreatorNotes(text, avatarId) {
685 const preference = new StylesPreference(avatarId);685 const preference = new StylesPreference(avatarId);
686 const sanitizeStyles = !preference.get();686 const sanitizeStyles = !preference.get();
687 const decodeStyleParam = { prefix: sanitizeStyles ? '#creator_notes_spoiler ' : '' };687 const decodeStyleParam = { prefix: sanitizeStyles ? '#creator_notes_spoiler ' : '' };
688 /** @type {import('dompurify').Config & { MESSAGE_SANITIZE: boolean }} */688 /** @type {DOMPurify.Config} */
689 const config = {689 const config = {
690 RETURN_DOM: false,690 RETURN_DOM: false,
691 RETURN_DOM_FRAGMENT: false,691 RETURN_DOM_FRAGMENT: false,
@@ -1911,13 +1911,13 @@ export function addDOMPurifyHooks() {
1911 });1911 });
19121912
1913 DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {1913 DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {
1914 if (!config['MESSAGE_SANITIZE']) {1914 if (!config.MESSAGE_SANITIZE) {
1915 return;1915 return;
1916 }1916 }
19171917
1918 /* Retain the classes on UI elements of messages that interact with the main UI */1918 /* Retain the classes on UI elements of messages that interact with the main UI */
1919 const permittedNodeTypes = ['BUTTON', 'DIV'];1919 const permittedNodeTypes = ['BUTTON', 'DIV'];
1920 if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) {1920 if (config.MESSAGE_ALLOW_SYSTEM_UI && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) {
1921 return;1921 return;
1922 }1922 }
19231923
@@ -1938,7 +1938,7 @@ export function addDOMPurifyHooks() {
1938 });1938 });
19391939
1940 DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {1940 DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
1941 if (!config['MESSAGE_SANITIZE']) {1941 if (!config.MESSAGE_SANITIZE) {
1942 return;1942 return;
1943 }1943 }
19441944
@@ -2239,6 +2239,11 @@ export function initChatUtilities() {
2239 wrapper.classList.add('flexFlowColumn', 'justifyCenter', 'alignitemscenter');2239 wrapper.classList.add('flexFlowColumn', 'justifyCenter', 'alignitemscenter');
2240 const textarea = document.createElement('textarea');2240 const textarea = document.createElement('textarea');
2241 textarea.dataset.for = broId;2241 textarea.dataset.for = broId;
2242 if (bro[0].dataset.macros !== undefined) {
2243 textarea.dataset.macros = bro[0].dataset.macros;
2244 textarea.dataset.macrosAutocomplete = 'always'; // Always show autocomplete in expanded editor
2245 textarea.dataset.macrosAutocompleteStyle = 'expanded'; // Use expanded autocomplete style
2246 }
2242 textarea.value = String(contentEditable ? bro[0].innerText : bro.val());2247 textarea.value = String(contentEditable ? bro[0].innerText : bro.val());
2243 textarea.classList.add('height100p', 'wide100p', 'maximized_textarea');2248 textarea.classList.add('height100p', 'wide100p', 'maximized_textarea');
2244 bro.hasClass('monospace') && textarea.classList.add('monospace');2249 bro.hasClass('monospace') && textarea.classList.add('monospace');
public/scripts/events.js+4 -1
@@ -1,6 +1,7 @@
1import { EventEmitter } from '../lib/eventemitter.js';1import { EventEmitter } from '../lib/eventemitter.js';
22
3export const event_types = {3export const event_types = {
4 APP_INITIALIZED: 'app_initialized',
4 APP_READY: 'app_ready',5 APP_READY: 'app_ready',
5 EXTRAS_CONNECTED: 'extras_connected',6 EXTRAS_CONNECTED: 'extras_connected',
6 MESSAGE_SWIPED: 'message_swiped',7 MESSAGE_SWIPED: 'message_swiped',
@@ -16,6 +17,8 @@ export const event_types = {
16 MORE_MESSAGES_LOADED: 'more_messages_loaded',17 MORE_MESSAGES_LOADED: 'more_messages_loaded',
17 IMPERSONATE_READY: 'impersonate_ready',18 IMPERSONATE_READY: 'impersonate_ready',
18 CHAT_CHANGED: 'chat_id_changed',19 CHAT_CHANGED: 'chat_id_changed',
20 // TODO: Naming convention is inconsistent with other events
21 CHAT_LOADED: 'chatLoaded',
19 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',22 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
20 GENERATION_STARTED: 'generation_started',23 GENERATION_STARTED: 'generation_started',
21 GENERATION_STOPPED: 'generation_stopped',24 GENERATION_STOPPED: 'generation_stopped',
@@ -95,4 +98,4 @@ export const event_types = {
95 MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',98 MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',
96};99};
97100
98export const eventSource = new EventEmitter([event_types.APP_READY]);101export const eventSource = new EventEmitter([event_types.APP_READY, event_types.APP_INITIALIZED]);
public/scripts/extensions-slashcommands.js+22 -37
@@ -1,11 +1,11 @@
1import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js';1import { disableExtension, enableExtension, extensionNames, findExtension } from './extensions.js';
2import { SlashCommand } from './slash-commands/SlashCommand.js';2import { SlashCommand } from './slash-commands/SlashCommand.js';
3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
4import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';4import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
5import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';5import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
6import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';6import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
7import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';7import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
8import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';8import { isFalseBoolean, isTrueBoolean } from './utils.js';
99
10/**10/**
11 * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension11 * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
@@ -22,30 +22,28 @@ function getExtensionActionCallback(action) {
22 }22 }
2323
24 const reload = !isFalseBoolean(args?.reload?.toString());24 const reload = !isFalseBoolean(args?.reload?.toString());
25 const internalExtensionName = findExtension(extensionName);25 const extension = findExtension(extensionName);
26 if (!internalExtensionName) {26 if (!extension) {
27 toastr.warning(`Extension ${extensionName} does not exist.`);27 toastr.warning(`Extension ${extensionName} does not exist.`);
28 return '';28 return '';
29 }29 }
3030
31 const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);31 if (action === 'enable' && extension.enabled) {
3232 toastr.info(`Extension ${extension.name} is already enabled.`);
33 if (action === 'enable' && isEnabled) {33 return extension.name;
34 toastr.info(`Extension ${extensionName} is already enabled.`);
35 return internalExtensionName;
36 }34 }
3735
38 if (action === 'disable' && !isEnabled) {36 if (action === 'disable' && !extension.enabled) {
39 toastr.info(`Extension ${extensionName} is already disabled.`);37 toastr.info(`Extension ${extension.name} is already disabled.`);
40 return internalExtensionName;38 return extension.name;
41 }39 }
4240
43 if (action === 'toggle') {41 if (action === 'toggle') {
44 action = isEnabled ? 'disable' : 'enable';42 action = extension.enabled ? 'disable' : 'enable';
45 }43 }
4644
47 if (reload) {45 if (reload) {
48 toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);46 toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extension.name} and reloading...`);
4947
50 // Clear input, so it doesn't stay because the command didn't "finish",48 // Clear input, so it doesn't stay because the command didn't "finish",
51 // and wait for a bit to both show the toast and let the clear bubble through.49 // and wait for a bit to both show the toast and let the clear bubble through.
@@ -54,36 +52,24 @@ function getExtensionActionCallback(action) {
54 }52 }
5553
56 if (action === 'enable') {54 if (action === 'enable') {
57 await enableExtension(internalExtensionName, reload);55 await enableExtension(extension.name, reload);
58 } else {56 } else {
59 await disableExtension(internalExtensionName, reload);57 await disableExtension(extension.name, reload);
60 }58 }
6159
62 toastr.success(`Extension ${extensionName} ${action}d.`);60 toastr.success(`Extension ${extension.name} ${action}d.`);
6361
6462
65 console.info(`Extension ${action}ed: ${extensionName}`);63 console.info(`Extension ${action}ed: ${extension.name}`);
66 if (!reload) {64 if (!reload) {
67 console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');65 console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
68 }66 }
6967
70 return internalExtensionName;68 return extension.name;
71 };69 };
72}70}
7371
74/**72/**
75 * Finds an extension by name, allowing omission of the "third-party/" prefix.
76 *
77 * @param {string} name - The name of the extension to find
78 * @returns {string?} - The matched extension name or undefined if not found
79 */
80function findExtension(name) {
81 return extensionNames.find(extName => {
82 return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
83 });
84}
85
86/**
87 * Provides an array of SlashCommandEnumValue objects based on the extension names.73 * Provides an array of SlashCommandEnumValue objects based on the extension names.
88 * Each object contains the name of the extension and a description indicating if it is a third-party extension.74 * Each object contains the name of the extension and a description indicating if it is a third-party extension.
89 *75 *
@@ -244,14 +230,13 @@ export function registerExtensionSlashCommands() {
244 name: 'extension-state',230 name: 'extension-state',
245 callback: async (_, extensionName) => {231 callback: async (_, extensionName) => {
246 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');232 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
247 const internalExtensionName = findExtension(extensionName);233 const extension = findExtension(extensionName);
248 if (!internalExtensionName) {234 if (!extension) {
249 toastr.warning(`Extension ${extensionName} does not exist.`);235 toastr.warning(`Extension ${extensionName} does not exist.`);
250 return '';236 return '';
251 }237 }
252238
253 const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);239 return String(extension.enabled);
254 return String(isEnabled);
255 },240 },
256 returns: 'The state of the extension, whether it is enabled.',241 returns: 'The state of the extension, whether it is enabled.',
257 unnamedArgumentList: [242 unnamedArgumentList: [
@@ -282,8 +267,8 @@ export function registerExtensionSlashCommands() {
282 aliases: ['extension-installed'],267 aliases: ['extension-installed'],
283 callback: async (_, extensionName) => {268 callback: async (_, extensionName) => {
284 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');269 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
285 const exists = findExtension(extensionName) !== undefined;270 const extension = findExtension(extensionName);
286 return exists ? 'true' : 'false';271 return extension !== null ? 'true' : 'false';
287 },272 },
288 returns: 'Whether the extension exists and is installed.',273 returns: 'Whether the extension exists and is installed.',
289 unnamedArgumentList: [274 unnamedArgumentList: [
public/scripts/extensions.js+152 -3
@@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
4import { showLoader } from './loader.js';4import { showLoader } from './loader.js';
5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
6import { renderTemplate, renderTemplateAsync } from './templates.js';6import { renderTemplate, renderTemplateAsync } from './templates.js';
7import { delay, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';7import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
8import { getContext } from './st-context.js';8import { getContext } from './st-context.js';
9import { isAdmin } from './user.js';9import { isAdmin } from './user.js';
10import { addLocaleData, getCurrentLocale, t } from './i18n.js';10import { addLocaleData, getCurrentLocale, t } from './i18n.js';
@@ -300,6 +300,64 @@ function onEnableExtensionClick() {
300}300}
301301
302/**302/**
303 * Handles toggling all extensions on or off.
304 * @param {Object[]} extensionsToToggle
305 * @param {JQuery<HTMLElement>} toggleContainer
306 * @returns {Object[]} Updated extensionsToToggle array
307 */
308function onToggleAllExtensions(extensionsToToggle, toggleContainer) {
309 const extensionNames = Object.keys(manifests);
310 const thirdPartyExtensions = extensionNames.filter(name => ['local', 'global'].includes(getExtensionType(name)));
311
312 const checkIfDisabled = (name) => {
313 const toggle = extensionsToToggle.find(ext => ext.name === name);
314 return toggle
315 ? !toggle.enable
316 : extension_settings.disabledExtensions.includes(name);
317 };
318
319 if (thirdPartyExtensions.length === 0) return [];
320
321 let enable = true;
322
323 for (const name of thirdPartyExtensions) {
324 const isEnabled = !checkIfDisabled(name);
325
326 if (isEnabled) {
327 enable = false;
328 break;
329 }
330 }
331
332 const toggleHandler = enable ? enableExtension : disableExtension;
333
334 for (const name of thirdPartyExtensions) {
335 const isDisabled = checkIfDisabled(name);
336 const doToggleExtension = enable ? isDisabled : !isDisabled;
337
338 if (doToggleExtension) {
339 const toggle = extensionsToToggle.find(ext => ext.name === name);
340
341 if (toggle) {
342 toggle.toggleHandler = toggleHandler;
343 toggle.enable = enable;
344 } else {
345 extensionsToToggle.push({ name, toggleHandler, enable });
346 }
347
348 toggleContainer
349 .find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
350 .prop('checked', enable)
351 .toggleClass('toggle_enable', !enable)
352 .toggleClass('toggle_disable', enable)
353 .toggleClass('checkbox_disabled', !enable);
354 }
355 }
356
357 return extensionsToToggle;
358}
359
360/**
303 * Enables an extension by name.361 * Enables an extension by name.
304 * @param {string} name Extension name362 * @param {string} name Extension name
305 * @param {boolean} [reload=true] If true, reload the page after enabling the extension363 * @param {boolean} [reload=true] If true, reload the page after enabling the extension
@@ -332,6 +390,21 @@ export async function disableExtension(name, reload = true) {
332}390}
333391
334/**392/**
393 * Finds an extension by name, allowing omission of the "third-party/" prefix.
394 *
395 * @param {string} name - The name of the extension to find
396 * @returns {{name: string, enabled: boolean}|null} Object with name and enabled properties, or null if not found
397 */
398export function findExtension(name) {
399 const internalExtensionName = extensionNames.find(extName => {
400 return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
401 });
402 if (!internalExtensionName) return null;
403 const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
404 return { name: internalExtensionName, enabled: isEnabled };
405}
406
407/**
335 * Loads manifest.json files for extensions.408 * Loads manifest.json files for extensions.
336 * @param {string[]} names Array of extension names409 * @param {string[]} names Array of extension names
337 * @returns {Promise<Record<string, object>>} Object with extension names as keys and their manifests as values410 * @returns {Promise<Record<string, object>>} Object with extension names as keys and their manifests as values
@@ -839,8 +912,15 @@ async function showExtensionsDetails() {
839 await oldPopup.completeCancelled();912 await oldPopup.completeCancelled();
840 }913 }
841 const htmlErrors = getExtensionLoadErrorsHtml();914 const htmlErrors = getExtensionLoadErrorsHtml();
842 const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');915 const htmlDefault = $('<div class="marginBot10"><h3>' + t`Built-in Extensions:` + '</h3></div>');
843 const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Installed Extensions:` + '</h3></div>');916
917 const htmlExternal = $(`<div class="marginBot10">
918 <div class="flex-container alignitemscenter spaceBetween flexnowrap marginBot10">
919 <h3 class="margin0">${t`Installed Extensions:`}</h3>
920 <div class="flex-container third_party_toolbar"></div>
921 </div>
922 </div>`);
923
844 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">924 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
845 <i class="fa-solid fa-spinner fa-spin"></i>925 <i class="fa-solid fa-spinner fa-spin"></i>
846 <span>` + t`Loading third-party extensions... Please wait...` + `</span>926 <span>` + t`Loading third-party extensions... Please wait...` + `</span>
@@ -852,6 +932,7 @@ async function showExtensionsDetails() {
852 const sortByName = accountStorage.getItem(sortOrderKey) === 'true';932 const sortByName = accountStorage.getItem(sortOrderKey) === 'true';
853 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;933 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
854 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);934 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
935 let extensionsToToggle = [];
855936
856 extensions.forEach(value => {937 extensions.forEach(value => {
857 const { isExternal, extensionHtml } = value;938 const { isExternal, extensionHtml } = value;
@@ -886,6 +967,54 @@ async function showExtensionsDetails() {
886 updateEnabledOnlyButton.textContent = t`Update enabled`;967 updateEnabledOnlyButton.textContent = t`Update enabled`;
887 updateEnabledOnlyButton.addEventListener('click', () => updateAction(false));968 updateEnabledOnlyButton.addEventListener('click', () => updateAction(false));
888969
970 const toggleAllExtensionsButton = document.createElement('div');
971 toggleAllExtensionsButton.classList.add('menu_button', 'menu_button_icon');
972 toggleAllExtensionsButton.title = t`Bulk toggle third-party extensions.`;
973 toggleAllExtensionsButton.innerHTML = `
974 <span>${t`Toggle extensions`}</span>
975 <div class="fa-solid fa-circle-info opacity50p"></div>
976 `;
977
978 const restoreBulkToggledExtensionsButton = document.createElement('div');
979 restoreBulkToggledExtensionsButton.classList.add('menu_button', 'menu_button_icon', 'fa-solid', 'fa-arrow-right-rotate', 'displayNone');
980 restoreBulkToggledExtensionsButton.title = t`Restore toggled extensions.\n\nIt does not restore extensions toggled individually.`;
981
982 toggleAllExtensionsButton.addEventListener('click', () => {
983 extensionsToToggle = onToggleAllExtensions(extensionsToToggle, htmlExternal);
984
985 for (const extension of extensionsToToggle) {
986 const { name } = extension;
987
988 htmlExternal
989 .find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
990 .off('click')
991 .one('click', () => {
992 extensionsToToggle = extensionsToToggle.filter(ext => ext.name !== name);
993 });
994 }
995
996 const restoreButtonHandler = extensionsToToggle.length > 0 ? 'remove' : 'add';
997
998 restoreBulkToggledExtensionsButton.classList[restoreButtonHandler]('displayNone');
999 });
1000
1001 restoreBulkToggledExtensionsButton.addEventListener('click', () => {
1002 for (const extension of extensionsToToggle) {
1003 const { name } = extension;
1004 const isDisabled = extension_settings.disabledExtensions.includes(name);
1005
1006 htmlExternal
1007 .find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
1008 .prop('checked', !isDisabled)
1009 .toggleClass('toggle_enable', isDisabled)
1010 .toggleClass('toggle_disable', !isDisabled)
1011 .toggleClass('checkbox_disabled', isDisabled);
1012 }
1013
1014 extensionsToToggle = [];
1015 restoreBulkToggledExtensionsButton.classList.add('displayNone');
1016 });
1017
889 const flexExpander = document.createElement('div');1018 const flexExpander = document.createElement('div');
890 flexExpander.classList.add('expander');1019 flexExpander.classList.add('expander');
8911020
@@ -899,6 +1028,7 @@ async function showExtensionsDetails() {
899 });1028 });
9001029
901 toolbar.append(updateAllButton, updateEnabledOnlyButton, flexExpander, sortOrderButton);1030 toolbar.append(updateAllButton, updateEnabledOnlyButton, flexExpander, sortOrderButton);
1031 htmlExternal.find('.third_party_toolbar').append(restoreBulkToggledExtensionsButton, toggleAllExtensionsButton);
902 html.prepend(toolbar);1032 html.prepend(toolbar);
903 }1033 }
9041034
@@ -914,6 +1044,24 @@ async function showExtensionsDetails() {
914 if (waitingForSave) {1044 if (waitingForSave) {
915 return false;1045 return false;
916 }1046 }
1047
1048 for (const extension of extensionsToToggle) {
1049 const { name, toggleHandler, enable } = extension;
1050 const isDisabled = extension_settings.disabledExtensions.includes(name);
1051
1052 try {
1053 if (isDisabled && !enable) continue;
1054 if (!isDisabled && enable) continue;
1055
1056 requiresReload = true;
1057
1058 await toggleHandler(name, false);
1059 } catch (error) {
1060 console.error(`Could not toggle extension ${name}:`, error);
1061 toastr.error(t`Could not toggle extension ${name}. See console for details.`);
1062 }
1063 }
1064
917 if (stateChanged) {1065 if (stateChanged) {
918 waitingForSave = true;1066 waitingForSave = true;
919 const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`);1067 const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`);
@@ -922,6 +1070,7 @@ async function showExtensionsDetails() {
922 waitingForSave = false;1070 waitingForSave = false;
923 requiresReload = true;1071 requiresReload = true;
924 }1072 }
1073
925 return true;1074 return true;
926 },1075 },
927 });1076 });
public/scripts/extensions/assets/index.js+15 -15
@@ -103,10 +103,10 @@ async function downloadAssetsList(url) {
103103
104 for (const i of json) {104 for (const i of json) {
105 //console.log(DEBUG_PREFIX,i)105 //console.log(DEBUG_PREFIX,i)
106 if (availableAssets[i['type']] === undefined)106 if (availableAssets[i.type] === undefined)
107 availableAssets[i['type']] = [];107 availableAssets[i.type] = [];
108108
109 availableAssets[i['type']].push(i);109 availableAssets[i.type].push(i);
110 }110 }
111111
112 console.debug(DEBUG_PREFIX, 'Updated available assets to', availableAssets);112 console.debug(DEBUG_PREFIX, 'Updated available assets to', availableAssets);
@@ -139,7 +139,7 @@ async function downloadAssetsList(url) {
139 assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));139 assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
140 }140 }
141141
142 for (const asset of availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {142 for (const asset of availableAssets[assetType].sort((a, b) => a?.name && b?.name && a.name.localeCompare(b.name))) {
143 const i = availableAssets[assetType].indexOf(asset);143 const i = availableAssets[assetType].indexOf(asset);
144 const elemId = `assets_install_${assetType}_${i}`;144 const elemId = `assets_install_${assetType}_${i}`;
145 let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });145 let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
@@ -149,13 +149,13 @@ async function downloadAssetsList(url) {
149 //if (DEBUG_TONY_SAMA_FORK_MODE)149 //if (DEBUG_TONY_SAMA_FORK_MODE)
150 // asset["url"] = asset["url"].replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG150 // asset["url"] = asset["url"].replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG
151151
152 console.debug(DEBUG_PREFIX, 'Checking asset', asset['id'], asset['url']);152 console.debug(DEBUG_PREFIX, 'Checking asset', asset.id, asset.url);
153153
154 const assetInstall = async function () {154 const assetInstall = async function () {
155 element.off('click');155 element.off('click');
156 label.removeClass('fa-download');156 label.removeClass('fa-download');
157 this.classList.add('asset-download-button-loading');157 this.classList.add('asset-download-button-loading');
158 await installAsset(asset['url'], assetType, asset['id']);158 await installAsset(asset.url, assetType, asset.id);
159 label.addClass('fa-check');159 label.addClass('fa-check');
160 this.classList.remove('asset-download-button-loading');160 this.classList.remove('asset-download-button-loading');
161 element.on('click', assetDelete);161 element.on('click', assetDelete);
@@ -173,11 +173,11 @@ async function downloadAssetsList(url) {
173 const assetDelete = async function () {173 const assetDelete = async function () {
174 if (assetType === 'character') {174 if (assetType === 'character') {
175 toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');175 toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');
176 await executeSlashCommandsWithOptions(`/go ${asset['id']}`);176 await executeSlashCommandsWithOptions(`/go ${asset.id}`);
177 return;177 return;
178 }178 }
179 element.off('click');179 element.off('click');
180 await deleteAsset(assetType, asset['id']);180 await deleteAsset(assetType, asset.id);
181 label.removeClass('fa-check');181 label.removeClass('fa-check');
182 label.removeClass('redOverlayGlow');182 label.removeClass('redOverlayGlow');
183 label.removeClass('fa-trash');183 label.removeClass('fa-trash');
@@ -186,7 +186,7 @@ async function downloadAssetsList(url) {
186 element.on('click', assetInstall);186 element.on('click', assetInstall);
187 };187 };
188188
189 if (isAssetInstalled(assetType, asset['id'])) {189 if (isAssetInstalled(assetType, asset.id)) {
190 console.debug(DEBUG_PREFIX, 'installed, checked');190 console.debug(DEBUG_PREFIX, 'installed, checked');
191 label.toggleClass('fa-download');191 label.toggleClass('fa-download');
192 label.toggleClass('fa-check');192 label.toggleClass('fa-check');
@@ -207,14 +207,14 @@ async function downloadAssetsList(url) {
207 element.on('click', assetInstall);207 element.on('click', assetInstall);
208 }208 }
209209
210 console.debug(DEBUG_PREFIX, 'Created element for ', asset['id']);210 console.debug(DEBUG_PREFIX, 'Created element for ', asset.id);
211211
212 const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);212 const displayName = DOMPurify.sanitize(asset.name || asset.id);
213 const description = DOMPurify.sanitize(asset['description'] || '');213 const description = DOMPurify.sanitize(asset.description || '');
214 const url = isValidUrl(asset['url']) ? asset['url'] : '';214 const url = isValidUrl(asset.url) ? asset.url : '';
215 const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;215 const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
216 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';216 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
217 const toolTag = assetType === 'extension' && asset['tool'];217 const toolTag = assetType === 'extension' && asset.tool;
218 const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR;218 const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR;
219219
220 const assetBlock = $('<i></i>')220 const assetBlock = $('<i></i>')
@@ -246,7 +246,7 @@ async function downloadAssetsList(url) {
246 if (asset.highlight) {246 if (asset.highlight) {
247 assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');247 assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
248 }248 }
249 assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset['url']}" alt="${displayName}"></div>`);249 assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset.url}" alt="${displayName}"></div>`);
250 }250 }
251251
252 assetBlock.addClass('asset-block');252 assetBlock.addClass('asset-block');
public/scripts/extensions/caption/index.js+11 -5
@@ -204,7 +204,7 @@ async function sendCaptionedMessage(caption, image, mimeType) {
204 inline_image: !!extension_settings.caption.show_in_chat,204 inline_image: !!extension_settings.caption.show_in_chat,
205 },205 },
206 };206 };
207 chat_metadata['tainted'] = true;207 chat_metadata.tainted = true;
208 context.chat.push(message);208 context.chat.push(message);
209 const messageId = context.chat.length - 1;209 const messageId = context.chat.length - 1;
210 await eventSource.emit(event_types.MESSAGE_SENT, messageId);210 await eventSource.emit(event_types.MESSAGE_SENT, messageId);
@@ -489,6 +489,8 @@ jQuery(async function () {
489 'vertexai': SECRET_KEYS.VERTEXAI,489 'vertexai': SECRET_KEYS.VERTEXAI,
490 'anthropic': SECRET_KEYS.CLAUDE,490 'anthropic': SECRET_KEYS.CLAUDE,
491 'xai': SECRET_KEYS.XAI,491 'xai': SECRET_KEYS.XAI,
492 'zai': SECRET_KEYS.ZAI,
493 'moonshot': SECRET_KEYS.MOONSHOT,
492 };494 };
493495
494 if (reverseProxyApis[api]) {496 if (reverseProxyApis[api]) {
@@ -502,11 +504,10 @@ jQuery(async function () {
502 'groq': SECRET_KEYS.GROQ,504 'groq': SECRET_KEYS.GROQ,
503 'cohere': SECRET_KEYS.COHERE,505 'cohere': SECRET_KEYS.COHERE,
504 'aimlapi': SECRET_KEYS.AIMLAPI,506 'aimlapi': SECRET_KEYS.AIMLAPI,
505 'moonshot': SECRET_KEYS.MOONSHOT,
506 'nanogpt': SECRET_KEYS.NANOGPT,507 'nanogpt': SECRET_KEYS.NANOGPT,
507 'chutes': SECRET_KEYS.CHUTES,508 'chutes': SECRET_KEYS.CHUTES,
508 'electronhub': SECRET_KEYS.ELECTRONHUB,509 'electronhub': SECRET_KEYS.ELECTRONHUB,
509 'zai': SECRET_KEYS.ZAI,510 'pollinations': SECRET_KEYS.POLLINATIONS,
510 };511 };
511512
512 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {513 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -530,7 +531,7 @@ jQuery(async function () {
530 }531 }
531532
532 // Custom API doesn't need additional checks533 // Custom API doesn't need additional checks
533 if (api === 'custom' || api === 'pollinations') {534 if (api === 'custom') {
534 return true;535 return true;
535 }536 }
536 }537 }
@@ -602,7 +603,7 @@ jQuery(async function () {
602 const modelIds = await response.json();603 const modelIds = await response.json();
603 if (Array.isArray(modelIds) && modelIds.length > 0) {604 if (Array.isArray(modelIds) && modelIds.length > 0) {
604 modelIds.sort().forEach((modelId) => {605 modelIds.sort().forEach((modelId) => {
605 if (!modelId || typeof modelId !== 'string' || options.some(o => o.value === modelId)) {606 if (!modelId || typeof modelId !== 'string' || options.some(o => o.value === modelId && o.dataset.type === api)) {
606 return;607 return;
607 }608 }
608 const option = document.createElement('option');609 const option = document.createElement('option');
@@ -622,6 +623,7 @@ jQuery(async function () {
622 await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');623 await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');
623 await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');624 await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');
624 await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');625 await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');
626 await processEndpoint('moonshot', '/api/backends/chat-completions/multimodal-models/moonshot');
625 }627 }
626628
627 await addSettings();629 await addSettings();
@@ -699,6 +701,10 @@ jQuery(async function () {
699 extension_settings.caption.ollama_custom_model = String($('#caption_ollama_custom_model').val()).trim();701 extension_settings.caption.ollama_custom_model = String($('#caption_ollama_custom_model').val()).trim();
700 saveSettingsDebounced();702 saveSettingsDebounced();
701 });703 });
704 $('#caption_custom_model').val(extension_settings.caption.custom_model || '').on('input', () => {
705 extension_settings.caption.custom_model = String($('#caption_custom_model').val()).trim();
706 saveSettingsDebounced();
707 });
702 $('#caption_refresh_models').on('click', async () => {708 $('#caption_refresh_models').on('click', async () => {
703 extension_settings.caption.multimodal_model = '';709 extension_settings.caption.multimodal_model = '';
704 await switchMultimodalBlocks();710 await switchMultimodalBlocks();
public/scripts/extensions/caption/settings.html+11 -5
@@ -49,13 +49,10 @@
49 </div>49 </div>
50 </label>50 </label>
51 <select id="caption_multimodal_model" class="flex1 text_pole">51 <select id="caption_multimodal_model" class="flex1 text_pole">
52 <!-- AI/ML API, OpenRouter, Pollinations, NanoGPT, Mistral, xAI are added externally by JavaScript -->52 <!-- AI/ML API, OpenRouter, Pollinations, NanoGPT, Mistral, xAI, Moonshot are added externally by JavaScript -->
53 <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option>53 <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option>
54 <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option>54 <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option>
55 <option data-type="cohere" value="command-a-vision-07-2025">command-a-vision-07-2025</option>55 <option data-type="cohere" value="command-a-vision-07-2025">command-a-vision-07-2025</option>
56 <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option>
57 <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option>
58 <option data-type="moonshot" value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option>
59 <option data-type="openai" value="gpt-5.2">gpt-5.2</option>56 <option data-type="openai" value="gpt-5.2">gpt-5.2</option>
60 <option data-type="openai" value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>57 <option data-type="openai" value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>
61 <option data-type="openai" value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>58 <option data-type="openai" value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>
@@ -89,6 +86,7 @@
89 <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>86 <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>
90 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>87 <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>
91 <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>88 <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
89 <option data-type="anthropic" value="claude-opus-4-6">claude-opus-4-6</option>
92 <option data-type="anthropic" value="claude-opus-4-5">claude-opus-4-5</option>90 <option data-type="anthropic" value="claude-opus-4-5">claude-opus-4-5</option>
93 <option data-type="anthropic" value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>91 <option data-type="anthropic" value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>
94 <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option>92 <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option>
@@ -177,8 +175,16 @@
177 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>175 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
178 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>176 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
179 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>177 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
178 <option data-type="custom" value="custom_custom" data-i18n="[Custom model]">[Custom model]</option>
180 </select>179 </select>
181 </div>180 </div>
181 <div data-type="custom">
182 <label for="caption_custom_model">
183 <span data-i18n="Model Id">Model Id</span>
184 <small data-i18n="(for [Custom model] option)">(for [Custom model] option)</small>
185 </label>
186 <input id="caption_custom_model" class="text_pole" type="text" placeholder="e.g. gpt-4o" />
187 </div>
182 <div data-type="ollama">188 <div data-type="ollama">
183 <div>189 <div>
184 The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>.190 The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>.
@@ -191,7 +197,7 @@
191 <input id="caption_ollama_custom_model" class="text_pole" type="text" placeholder="e.g. gemma3:latest" />197 <input id="caption_ollama_custom_model" class="text_pole" type="text" placeholder="e.g. gemma3:latest" />
192 </div>198 </div>
193 </div>199 </div>
194 <label data-type="openai,anthropic,google,vertexai,mistral,xai" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">200 <label data-type="openai,anthropic,google,vertexai,mistral,xai,zai,moonshot" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
195 <input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">201 <input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
196 <span data-i18n="Allow reverse proxy">Allow reverse proxy</span>202 <span data-i18n="Allow reverse proxy">Allow reverse proxy</span>
197 </label>203 </label>
public/scripts/extensions/memory/index.js+10 -6
@@ -437,9 +437,13 @@ async function onChatEvent() {
437437
438 const context = getContext();438 const context = getContext();
439 const chat = context.chat;439 const chat = context.chat;
440 // Chat can't be empty.
441 if (chat.length === 0) return;
442
443 const lastMessage = chat[chat.length - 1];
440444
441 // No new messages - do nothing445 // No new messages - do nothing
442 if (chat.length === 0 || (lastMessageId === chat.length && getStringHash(chat[chat.length - 1].mes) === lastMessageHash)) {446 if ((lastMessageId === chat.length && getStringHash(lastMessage.mes) === lastMessageHash)) {
443 return;447 return;
444 }448 }
445449
@@ -451,18 +455,18 @@ async function onChatEvent() {
451455
452 // Message has been edited / regenerated - delete the saved memory456 // Message has been edited / regenerated - delete the saved memory
453 if (chat.length457 if (chat.length
454 && chat[chat.length - 1].extra458 && lastMessage.extra
455 && chat[chat.length - 1].extra.memory459 && lastMessage.extra.memory
456 && lastMessageId === chat.length460 && lastMessageId === chat.length
457 && getStringHash(chat[chat.length - 1].mes) !== lastMessageHash) {461 && getStringHash(lastMessage.mes) !== lastMessageHash) {
458 delete chat[chat.length - 1].extra.memory;462 delete lastMessage.extra.memory;
459 }463 }
460464
461 summarizeChat(context)465 summarizeChat(context)
462 .catch(console.error)466 .catch(console.error)
463 .finally(() => {467 .finally(() => {
464 lastMessageId = context.chat?.length ?? null;468 lastMessageId = context.chat?.length ?? null;
465 lastMessageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1]['mes']) ?? '');469 lastMessageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? '');
466 });470 });
467}471}
468472
public/scripts/extensions/quick-reply/index.js+1 -1
@@ -185,7 +185,7 @@ const init = async () => {
185 buttons.show();185 buttons.show();
186 settings.onSave = ()=>buttons.refresh();186 settings.onSave = ()=>buttons.refresh();
187187
188 window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {188 globalThis.executeQuickReplyByName = async(name, args = {}, options = {}) => {
189 let qr = [189 let qr = [
190 ...settings.config.setList,190 ...settings.config.setList,
191 ...(settings.chatConfig?.setList ?? []),191 ...(settings.chatConfig?.setList ?? []),
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+1 -1
@@ -77,7 +77,7 @@ export class SlashCommandHandler {
77 },77 },
78 };78 };
7979
80 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;80 globalThis.qrEnumProviderExecutables = localEnumProviders.qrExecutables;
8181
82 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr',82 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr',
83 callback: (_, value) => this.executeQuickReplyByIndex(Number(value)),83 callback: (_, value) => this.executeQuickReplyByIndex(Number(value)),
public/scripts/extensions/shared.js+17 -2
@@ -15,7 +15,7 @@ import { createThumbnail, isValidUrl } from '../utils.js';
15 */15 */
16export async function getMultimodalCaption(base64Img, prompt) {16export async function getMultimodalCaption(base64Img, prompt) {
17 const useReverseProxy =17 const useReverseProxy =
18 (['openai', 'anthropic', 'google', 'mistral', 'vertexai', 'xai'].includes(extension_settings.caption.multimodal_api))18 (['openai', 'anthropic', 'google', 'mistral', 'vertexai', 'xai', 'zai', 'moonshot'].includes(extension_settings.caption.multimodal_api))
19 && extension_settings.caption.allow_reverse_proxy19 && extension_settings.caption.allow_reverse_proxy
20 && oai_settings.reverse_proxy20 && oai_settings.reverse_proxy
21 && isValidUrl(oai_settings.reverse_proxy);21 && isValidUrl(oai_settings.reverse_proxy);
@@ -108,8 +108,15 @@ export async function getMultimodalCaption(base64Img, prompt) {
108 }108 }
109109
110 if (isCustom) {110 if (isCustom) {
111 if (extension_settings.caption.multimodal_model === 'custom_current') {
112 requestBody.model = oai_settings.custom_model || '';
113 }
114
115 if (extension_settings.caption.multimodal_model === 'custom_custom') {
116 requestBody.model = extension_settings.caption.custom_model || '';
117 }
118
111 requestBody.server_url = oai_settings.custom_url;119 requestBody.server_url = oai_settings.custom_url;
112 requestBody.model = oai_settings.custom_model || 'gpt-4-turbo';
113 requestBody.custom_include_headers = oai_settings.custom_include_headers;120 requestBody.custom_include_headers = oai_settings.custom_include_headers;
114 requestBody.custom_include_body = oai_settings.custom_include_body;121 requestBody.custom_include_body = oai_settings.custom_include_body;
115 requestBody.custom_exclude_body = oai_settings.custom_exclude_body;122 requestBody.custom_exclude_body = oai_settings.custom_exclude_body;
@@ -245,6 +252,10 @@ function throwIfInvalidModel(useReverseProxy) {
245 throw new Error('Custom API URL is not set.');252 throw new Error('Custom API URL is not set.');
246 }253 }
247254
255 if (multimodalApi === 'custom' && multimodalModel === 'custom_custom' && !extension_settings.caption.custom_model) {
256 throw new Error('Custom OpenAI-compatible Model ID is not set.');
257 }
258
248 if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) {259 if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) {
249 throw new Error('AI/ML API key is not set.');260 throw new Error('AI/ML API key is not set.');
250 }261 }
@@ -268,6 +279,10 @@ function throwIfInvalidModel(useReverseProxy) {
268 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {279 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {
269 throw new Error('Z.AI API key is not set.');280 throw new Error('Z.AI API key is not set.');
270 }281 }
282
283 if (multimodalApi === 'pollinations' && !secret_state[SECRET_KEYS.POLLINATIONS]) {
284 throw new Error('Pollinations API key is not set.');
285 }
271}286}
272287
273/**288/**
public/scripts/extensions/stable-diffusion/index.js+474 -36
@@ -69,10 +69,17 @@ const MODULE_NAME = 'sd';
69// This is a 1x1 transparent PNG69// This is a 1x1 transparent PNG
70const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';70const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
71const CUSTOM_STOP_EVENT = 'sd_stop_generation';71const CUSTOM_STOP_EVENT = 'sd_stop_generation';
72
73// Generation tracking for status indicator
74let activeGenerations = 0;
75/** @type {JQuery<HTMLElement>|null} */
76let generationToast = null;
77
72const sources = {78const sources = {
73 extras: 'extras',79 extras: 'extras',
74 horde: 'horde',80 horde: 'horde',
75 auto: 'auto',81 auto: 'auto',
82 sdcpp: 'sdcpp',
76 novel: 'novel',83 novel: 'novel',
77 vlad: 'vlad',84 vlad: 'vlad',
78 openai: 'openai',85 openai: 'openai',
@@ -277,6 +284,7 @@ const defaultSettings = {
277 snap: false,284 snap: false,
278 free_extend: false,285 free_extend: false,
279 function_tool: false,286 function_tool: false,
287 minimal_prompt_processing: false,
280288
281 prompts: promptTemplates,289 prompts: promptTemplates,
282290
@@ -284,6 +292,9 @@ const defaultSettings = {
284 auto_url: 'http://localhost:7860',292 auto_url: 'http://localhost:7860',
285 auto_auth: '',293 auto_auth: '',
286294
295 // stable-diffusion.cpp settings
296 sdcpp_url: 'http://127.0.0.1:1234',
297
287 vlad_url: 'http://localhost:7860',298 vlad_url: 'http://localhost:7860',
288 vlad_auth: '',299 vlad_auth: '',
289300
@@ -320,6 +331,7 @@ const defaultSettings = {
320 // OpenAI settings331 // OpenAI settings
321 openai_style: 'vivid',332 openai_style: 'vivid',
322 openai_quality: 'standard',333 openai_quality: 'standard',
334 openai_quality_gpt: 'auto',
323 openai_duration: '8',335 openai_duration: '8',
324336
325 style: 'Default',337 style: 'Default',
@@ -425,7 +437,7 @@ function processTriggers(chat, _, abort, type) {
425 }437 }
426}438}
427439
428window['SD_ProcessTriggers'] = processTriggers;440globalThis.SD_ProcessTriggers = processTriggers;
429441
430function getSdRequestBody() {442function getSdRequestBody() {
431 switch (extension_settings.sd.source) {443 switch (extension_settings.sd.source) {
@@ -521,6 +533,7 @@ async function loadSettings() {
521 $('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning);533 $('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning);
522 $('#sd_auto_url').val(extension_settings.sd.auto_url);534 $('#sd_auto_url').val(extension_settings.sd.auto_url);
523 $('#sd_auto_auth').val(extension_settings.sd.auto_auth);535 $('#sd_auto_auth').val(extension_settings.sd.auto_auth);
536 $('#sd_sdcpp_url').val(extension_settings.sd.sdcpp_url);
524 $('#sd_vlad_url').val(extension_settings.sd.vlad_url);537 $('#sd_vlad_url').val(extension_settings.sd.vlad_url);
525 $('#sd_vlad_auth').val(extension_settings.sd.vlad_auth);538 $('#sd_vlad_auth').val(extension_settings.sd.vlad_auth);
526 $('#sd_drawthings_url').val(extension_settings.sd.drawthings_url);539 $('#sd_drawthings_url').val(extension_settings.sd.drawthings_url);
@@ -528,12 +541,14 @@ async function loadSettings() {
528 $('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode);541 $('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode);
529 $('#sd_openai_style').val(extension_settings.sd.openai_style);542 $('#sd_openai_style').val(extension_settings.sd.openai_style);
530 $('#sd_openai_quality').val(extension_settings.sd.openai_quality);543 $('#sd_openai_quality').val(extension_settings.sd.openai_quality);
544 $('#sd_openai_quality_gpt').val(extension_settings.sd.openai_quality_gpt);
531 $('#sd_openai_duration').val(extension_settings.sd.openai_duration);545 $('#sd_openai_duration').val(extension_settings.sd.openai_duration);
532 $('#sd_comfy_type').val(extension_settings.sd.comfy_type);546 $('#sd_comfy_type').val(extension_settings.sd.comfy_type);
533 $('#sd_comfy_url').val(extension_settings.sd.comfy_url);547 $('#sd_comfy_url').val(extension_settings.sd.comfy_url);
534 $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);548 $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
535 $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url);549 $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url);
536 $('#sd_snap').prop('checked', extension_settings.sd.snap);550 $('#sd_snap').prop('checked', extension_settings.sd.snap);
551 $('#sd_minimal_prompt_processing').prop('checked', extension_settings.sd.minimal_prompt_processing);
537 $('#sd_clip_skip').val(extension_settings.sd.clip_skip);552 $('#sd_clip_skip').val(extension_settings.sd.clip_skip);
538 $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);553 $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);
539 $('#sd_seed').val(extension_settings.sd.seed);554 $('#sd_seed').val(extension_settings.sd.seed);
@@ -656,6 +671,11 @@ function onSnapInput() {
656 saveSettingsDebounced();671 saveSettingsDebounced();
657}672}
658673
674function onMinimalPromptProcessing() {
675 extension_settings.sd.minimal_prompt_processing = !!$(this).prop('checked');
676 saveSettingsDebounced();
677}
678
659function onStyleSelect() {679function onStyleSelect() {
660 const selectedStyle = String($('#sd_style').find(':selected').val());680 const selectedStyle = String($('#sd_style').find(':selected').val());
661 const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);681 const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
@@ -708,7 +728,8 @@ async function onDeleteStyleClick() {
708}728}
709729
710async function onSaveStyleClick() {730async function onSaveStyleClick() {
711 const userInput = await callGenericPopup('Enter style name:', POPUP_TYPE.INPUT);731 const selectedStyle = extension_settings.sd.style || '';
732 const userInput = await callGenericPopup(t`Enter style name:`, POPUP_TYPE.INPUT, selectedStyle);
712733
713 if (!userInput) {734 if (!userInput) {
714 return;735 return;
@@ -744,6 +765,48 @@ async function onSaveStyleClick() {
744 saveSettingsDebounced();765 saveSettingsDebounced();
745}766}
746767
768async function onRenameStyleClick() {
769 const selectedStyle = extension_settings.sd.style;
770 const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
771
772 if (!styleObject) {
773 return;
774 }
775
776 const newName = await callGenericPopup(t`Enter new style name:`, POPUP_TYPE.INPUT, selectedStyle);
777
778 if (!newName) {
779 return;
780 }
781
782 const name = String(newName).trim();
783
784 if (name === selectedStyle) {
785 return;
786 }
787
788 const existingStyle = extension_settings.sd.styles.find(x => x.name === name);
789
790 if (existingStyle) {
791 toastr.error(t`A style with that name already exists`);
792 return;
793 }
794
795 styleObject.name = name;
796 extension_settings.sd.style = name;
797
798 $('#sd_style').empty();
799 for (const style of extension_settings.sd.styles) {
800 const option = document.createElement('option');
801 option.value = style.name;
802 option.text = style.name;
803 option.selected = style.name === extension_settings.sd.style;
804 $('#sd_style').append(option);
805 }
806
807 saveSettingsDebounced();
808}
809
747/**810/**
748 * Modifies prompt based on user inputs.811 * Modifies prompt based on user inputs.
749 * @param {string} prompt Prompt to refine812 * @param {string} prompt Prompt to refine
@@ -977,6 +1040,13 @@ const resolutionOptions = {
977 sd_res_1024x1536: { width: 1024, height: 1536, name: '1024x1536 (2:3, ChatGPT)' },1040 sd_res_1024x1536: { width: 1024, height: 1536, name: '1024x1536 (2:3, ChatGPT)' },
978 sd_res_1024x1792: { width: 1024, height: 1792, name: '1024x1792 (4:7, DALL-E)' },1041 sd_res_1024x1792: { width: 1024, height: 1792, name: '1024x1792 (4:7, DALL-E)' },
979 sd_res_1792x1024: { width: 1792, height: 1024, name: '1792x1024 (7:4, DALL-E)' },1042 sd_res_1792x1024: { width: 1792, height: 1024, name: '1792x1024 (7:4, DALL-E)' },
1043 sd_res_1280x1280: { width: 1280, height: 1280, name: '1280x1280 (1:1, Z.AI)' },
1044 sd_res_1568x1056: { width: 1568, height: 1056, name: '1568x1056 (3:2, Z.AI)' },
1045 sd_res_1056x1568: { width: 1056, height: 1568, name: '1056x1568 (2:3, Z.AI)' },
1046 sd_res_1472x1088: { width: 1472, height: 1088, name: '1472x1088 (4:3, Z.AI)' },
1047 sd_res_1088x1472: { width: 1088, height: 1472, name: '1088x1472 (3:4, Z.AI)' },
1048 sd_res_1728x960: { width: 1728, height: 960, name: '1728x960 (16:9, Z.AI)' },
1049 sd_res_960x1728: { width: 960, height: 1728, name: '960x1728 (9:16, Z.AI)' },
980};1050};
9811051
982function onResolutionChange() {1052function onResolutionChange() {
@@ -1141,6 +1211,11 @@ function onAutoAuthInput() {
1141 saveSettingsDebounced();1211 saveSettingsDebounced();
1142}1212}
11431213
1214function onSdcppUrlInput() {
1215 extension_settings.sd.sdcpp_url = $('#sd_sdcpp_url').val();
1216 saveSettingsDebounced();
1217}
1218
1144function onVladUrlInput() {1219function onVladUrlInput() {
1145 extension_settings.sd.vlad_url = $('#sd_vlad_url').val();1220 extension_settings.sd.vlad_url = $('#sd_vlad_url').val();
1146 saveSettingsDebounced();1221 saveSettingsDebounced();
@@ -1249,6 +1324,29 @@ async function validateAutoUrl() {
1249 }1324 }
1250}1325}
12511326
1327async function validateSdcppUrl() {
1328 try {
1329 if (!extension_settings.sd.sdcpp_url) {
1330 throw new Error('URL is not set.');
1331 }
1332
1333 const result = await fetch('/api/sd/sdcpp/ping', {
1334 method: 'POST',
1335 headers: getRequestHeaders(),
1336 body: JSON.stringify({ url: extension_settings.sd.sdcpp_url }),
1337 });
1338
1339 if (!result.ok) {
1340 throw new Error('stable-diffusion.cpp server returned an error.');
1341 }
1342
1343 await loadSettingOptions();
1344 toastr.success('stable-diffusion.cpp server connected.');
1345 } catch (error) {
1346 toastr.error(`Could not validate stable-diffusion.cpp server: ${error.message}`);
1347 }
1348}
1349
1252async function validateDrawthingsUrl() {1350async function validateDrawthingsUrl() {
1253 try {1351 try {
1254 if (!extension_settings.sd.drawthings_url) {1352 if (!extension_settings.sd.drawthings_url) {
@@ -1542,6 +1640,9 @@ async function loadSamplers() {
1542 case sources.auto:1640 case sources.auto:
1543 samplers = await loadAutoSamplers();1641 samplers = await loadAutoSamplers();
1544 break;1642 break;
1643 case sources.sdcpp:
1644 samplers = await loadSdcppSamplers();
1645 break;
1545 case sources.drawthings:1646 case sources.drawthings:
1546 samplers = await loadDrawthingsSamplers();1647 samplers = await loadDrawthingsSamplers();
1547 break;1648 break;
@@ -1667,6 +1768,11 @@ async function loadAutoSamplers() {
1667 }1768 }
1668}1769}
16691770
1771async function loadSdcppSamplers() {
1772 // The sdcpp server does not provide an API for samplers, so we return the known list.
1773 return ['euler', 'euler_a', 'heun', 'dpm2', 'dpm++2s_a', 'dpm++2m', 'dpm++2mv2', 'ipndm', 'ipndm_v', 'lcm', 'ddim_trailing', 'tcd'];
1774}
1775
1670async function loadDrawthingsSamplers() {1776async function loadDrawthingsSamplers() {
1671 // The app developer doesn't provide an API to get these yet1777 // The app developer doesn't provide an API to get these yet
1672 return [1778 return [
@@ -1756,6 +1862,9 @@ async function loadModels() {
1756 case sources.auto:1862 case sources.auto:
1757 models = await loadAutoModels();1863 models = await loadAutoModels();
1758 break;1864 break;
1865 case sources.sdcpp:
1866 models = [{ value: '', text: 'N/A' }];
1867 break;
1759 case sources.drawthings:1868 case sources.drawthings:
1760 models = await loadDrawthingsModels();1869 models = await loadDrawthingsModels();
1761 break;1870 break;
@@ -1850,7 +1959,7 @@ function switchModelSpecificControls(modelId) {
18501959
1851 modelControls.each(function () {1960 modelControls.each(function () {
1852 const models = String($(this).attr('data-sd-model') || '').split(',').map(m => m.trim());1961 const models = String($(this).attr('data-sd-model') || '').split(',').map(m => m.trim());
1853 $(this).toggle(models.includes(modelId));1962 $(this).toggle(models.some(m => modelId.includes(m)));
1854 });1963 });
1855}1964}
18561965
@@ -1940,6 +2049,8 @@ async function loadXAIModels() {
1940}2049}
19412050
1942async function loadPollinationsModels() {2051async function loadPollinationsModels() {
2052 $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]);
2053
1943 const result = await fetch('/api/sd/pollinations/models', {2054 const result = await fetch('/api/sd/pollinations/models', {
1944 method: 'POST',2055 method: 'POST',
1945 headers: getRequestHeaders({ omitContentType: true }),2056 headers: getRequestHeaders({ omitContentType: true }),
@@ -2169,6 +2280,7 @@ async function loadOpenAiModels() {
2169 { value: 'gpt-image-1.5', text: 'gpt-image-1.5' },2280 { value: 'gpt-image-1.5', text: 'gpt-image-1.5' },
2170 { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' },2281 { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' },
2171 { value: 'gpt-image-1', text: 'gpt-image-1' },2282 { value: 'gpt-image-1', text: 'gpt-image-1' },
2283 { value: 'chatgpt-image-latest', text: 'chatgpt-image-latest' },
2172 { value: 'dall-e-3', text: 'dall-e-3' },2284 { value: 'dall-e-3', text: 'dall-e-3' },
2173 { value: 'dall-e-2', text: 'dall-e-2' },2285 { value: 'dall-e-2', text: 'dall-e-2' },
2174 { value: 'sora-2', text: 'sora-2' },2286 { value: 'sora-2', text: 'sora-2' },
@@ -2294,7 +2406,12 @@ async function loadGoogleModels() {
2294}2406}
22952407
2296async function loadZaiModels() {2408async function loadZaiModels() {
2297 return ['cogview-4-250304'].map(name => ({ value: name, text: name }));2409 return [
2410 { value: 'glm-image', text: 'GLM-Image' },
2411 { value: 'cogview-4-250304', text: 'CogView-4' },
2412 { value: 'cogvideox-3', text: 'CogVideoX-3' },
2413 { value: 'viduq1-text', text: 'Viduq1-Text' },
2414 ];
2298}2415}
22992416
2300async function loadOpenRouterModels() {2417async function loadOpenRouterModels() {
@@ -2356,6 +2473,9 @@ async function loadSchedulers() {
2356 case sources.auto:2473 case sources.auto:
2357 schedulers = await getAutoRemoteSchedulers();2474 schedulers = await getAutoRemoteSchedulers();
2358 break;2475 break;
2476 case sources.sdcpp:
2477 schedulers = await loadSdcppSchedulers();
2478 break;
2359 case sources.novel:2479 case sources.novel:
2360 schedulers = loadNovelSchedulers();2480 schedulers = loadNovelSchedulers();
2361 break;2481 break;
@@ -2454,6 +2574,11 @@ async function loadComfySchedulers() {
2454 }2574 }
2455}2575}
24562576
2577async function loadSdcppSchedulers() {
2578 // The sdcpp server does not provide an API for schedulers, so we return the known list.
2579 return ['discrete', 'karras', 'exponential', 'ays', 'gits', 'smoothstep', 'sgm_uniform', 'simple', 'kl_optimal', 'lcm'];
2580}
2581
2457async function loadVaes() {2582async function loadVaes() {
2458 $('#sd_vae').empty();2583 $('#sd_vae').empty();
2459 let vaes = [];2584 let vaes = [];
@@ -2468,6 +2593,9 @@ async function loadVaes() {
2468 case sources.auto:2593 case sources.auto:
2469 vaes = await loadAutoVaes();2594 vaes = await loadAutoVaes();
2470 break;2595 break;
2596 case sources.sdcpp:
2597 vaes = ['N/A'];
2598 break;
2471 case sources.novel:2599 case sources.novel:
2472 vaes = ['N/A'];2600 vaes = ['N/A'];
2473 break;2601 break;
@@ -2657,6 +2785,15 @@ function processReply(str) {
2657 return '';2785 return '';
2658 }2786 }
26592787
2788 if (extension_settings.sd.minimal_prompt_processing) {
2789 // Minimal prompt processing
2790 // JSON and similar should be preserved
2791 str = str.normalize('NFD');
2792 str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one
2793 str = str.trim();
2794 return str;
2795 }
2796
2660 str = str.replaceAll('"', '');2797 str = str.replaceAll('"', '');
2661 str = str.replaceAll('“', '');2798 str = str.replaceAll('“', '');
2662 str = str.replaceAll('\n', ', ');2799 str = str.replaceAll('\n', ', ');
@@ -2728,6 +2865,62 @@ function ensureSelectionExists(setting, selector) {
2728}2865}
27292866
2730/**2867/**
2868 * Updates the generation status indicator based on active generation count.
2869 * Shows/hides various UI indicators to inform user of background image generation.
2870 */
2871function updateGenerationIndicator() {
2872 if (activeGenerations > 0) {
2873 const countText = activeGenerations > 1 ? ` (${activeGenerations})` : '';
2874 const toastText = `<i class="fa-solid fa-spinner fa-spin"></i> ${t`Generating image`}${countText}...`;
2875
2876 // Show persistent toast if not already showing
2877 if (!generationToast) {
2878 generationToast = toastr.info(
2879 toastText,
2880 'Image Generation',
2881 {
2882 timeOut: 0,
2883 extendedTimeOut: 0,
2884 tapToDismiss: true,
2885 escapeHtml: false,
2886 onHidden: () => {
2887 generationToast = null;
2888 },
2889 },
2890 );
2891 } else if (activeGenerations > 1) {
2892 // Update count in existing toast
2893 const toastMessage = $(generationToast).find('.toast-message');
2894 if (toastMessage.length) {
2895 toastMessage.html(toastText);
2896 }
2897 }
2898 } else {
2899 // Hide toast when done
2900 if (generationToast) {
2901 toastr.clear(generationToast);
2902 generationToast = null;
2903 }
2904 }
2905}
2906
2907/**
2908 * Increments the active generation counter and updates indicators.
2909 */
2910function startGenerationTracking() {
2911 activeGenerations++;
2912 updateGenerationIndicator();
2913}
2914
2915/**
2916 * Decrements the active generation counter and updates indicators.
2917 */
2918function endGenerationTracking() {
2919 activeGenerations = Math.max(0, activeGenerations - 1);
2920 updateGenerationIndicator();
2921}
2922
2923/**
2731 * Generates an image based on the given trigger word.2924 * Generates an image based on the given trigger word.
2732 * @param {string} initiator The initiator of the image generation2925 * @param {string} initiator The initiator of the image generation
2733 * @param {Record<string, object>} args Command arguments2926 * @param {Record<string, object>} args Command arguments
@@ -2801,6 +2994,9 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2801 await eventSource.emit(event_types.SD_PROMPT_PROCESSING, eventData);2994 await eventSource.emit(event_types.SD_PROMPT_PROCESSING, eventData);
2802 prompt = eventData.prompt; // Allow extensions to modify the prompt2995 prompt = eventData.prompt; // Allow extensions to modify the prompt
28032996
2997 // Track this generation for status indicator
2998 startGenerationTracking();
2999 // Show stop button after prompt is ready (prompt generation uses separate abort mechanism)
2804 $(stopButton).show();3000 $(stopButton).show();
2805 eventSource.once(CUSTOM_STOP_EVENT, stopListener);3001 eventSource.once(CUSTOM_STOP_EVENT, stopListener);
28063002
@@ -2811,6 +3007,13 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2811 // generate the image3007 // generate the image
2812 imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal);3008 imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal);
2813 } catch (err) {3009 } catch (err) {
3010 // Check if this was an intentional abort by user
3011 if (abortController.signal.aborted) {
3012 console.log('SD: Image generation aborted by user');
3013 toastr.info('Image generation stopped.', 'Image Generation');
3014 return;
3015 }
3016
2814 console.trace(err);3017 console.trace(err);
2815 // errors here are most likely due to text generation failure3018 // errors here are most likely due to text generation failure
2816 // sendGenerationRequest mostly deals with its own errors3019 // sendGenerationRequest mostly deals with its own errors
@@ -2823,6 +3026,7 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2823 $(stopButton).hide();3026 $(stopButton).hide();
2824 restoreOriginalDimensions(dimensions);3027 restoreOriginalDimensions(dimensions);
2825 eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);3028 eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
3029 endGenerationTracking();
2826 }3030 }
28273031
2828 return imagePath;3032 return imagePath;
@@ -3014,8 +3218,10 @@ function getUserAvatarUrl() {
3014 * @returns {Promise<string>} - A promise that resolves when the prompt generation completes.3218 * @returns {Promise<string>} - A promise that resolves when the prompt generation completes.
3015 */3219 */
3016async function generatePrompt(quietPrompt) {3220async function generatePrompt(quietPrompt) {
3221 const toast = toastr.info('Generating image prompt with an LLM...', 'Image Generation');
3017 const reply = await generateQuietPrompt({ quietPrompt });3222 const reply = await generateQuietPrompt({ quietPrompt });
3018 const processedReply = processReply(reply);3223 const processedReply = processReply(reply);
3224 toastr.clear(toast);
30193225
3020 if (!processedReply) {3226 if (!processedReply) {
3021 toastr.error('Prompt generation produced no text. Make sure you\'re using a valid instruct template and try again', 'Image Generation');3227 toastr.error('Prompt generation produced no text. Make sure you\'re using a valid instruct template and try again', 'Image Generation');
@@ -3074,6 +3280,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
3074 case sources.auto:3280 case sources.auto:
3075 result = await generateAutoImage(prefixedPrompt, negativePrompt, signal);3281 result = await generateAutoImage(prefixedPrompt, negativePrompt, signal);
3076 break;3282 break;
3283 case sources.sdcpp:
3284 result = await generateSdcppImage(prefixedPrompt, negativePrompt, signal);
3285 break;
3077 case sources.novel:3286 case sources.novel:
3078 result = await generateNovelImage(prefixedPrompt, negativePrompt, signal);3287 result = await generateNovelImage(prefixedPrompt, negativePrompt, signal);
3079 break;3288 break;
@@ -3140,6 +3349,13 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
3140 throw new Error('Endpoint did not return image data.');3349 throw new Error('Endpoint did not return image data.');
3141 }3350 }
3142 } catch (err) {3351 } catch (err) {
3352 // Check if this was an intentional abort by user
3353 if (signal?.aborted) {
3354 console.log('SD: Image generation aborted by user');
3355 toastr.info('Image generation stopped.', 'Image Generation');
3356 return;
3357 }
3358
3143 console.error('Image generation request error: ', err);3359 console.error('Image generation request error: ', err);
3144 toastr.error('Image generation failed. Please try again.' + '\n\n' + String(err), 'Image Generation');3360 toastr.error('Image generation failed. Please try again.' + '\n\n' + String(err), 'Image Generation');
3145 return;3361 return;
@@ -3215,7 +3431,7 @@ async function generatePollinationsImage(prompt, negativePrompt, signal) {
32153431
3216 if (result.ok) {3432 if (result.ok) {
3217 const data = await result.json();3433 const data = await result.json();
3218 return { format: 'jpg', data: data?.image };3434 return { format: data?.format, data: data?.image };
3219 } else {3435 } else {
3220 const text = await result.text();3436 const text = await result.text();
3221 throw new Error(text);3437 throw new Error(text);
@@ -3271,7 +3487,7 @@ async function generateExtrasImage(prompt, negativePrompt, signal) {
3271 * Gets an aspect ratio for Stability that is the closest to the given width and height.3487 * Gets an aspect ratio for Stability that is the closest to the given width and height.
3272 * @param {number} width Target width3488 * @param {number} width Target width
3273 * @param {number} height Target height3489 * @param {number} height Target height
3274 * @param {'google'|'stability'} source Source of the request, used to determine aspect ratio3490 * @param {'google'|'stability'|'zai'} source Source of the request, used to determine aspect ratio
3275 * @returns {string} Closest aspect ratio as a string3491 * @returns {string} Closest aspect ratio as a string
3276 */3492 */
3277function getClosestAspectRatio(width, height, source) {3493function getClosestAspectRatio(width, height, source) {
@@ -3297,6 +3513,12 @@ function getClosestAspectRatio(width, height, source) {
3297 '4:3': 4 / 3,3513 '4:3': 4 / 3,
3298 '3:4': 3 / 4,3514 '3:4': 3 / 4,
3299 };3515 };
3516 case 'zai':
3517 return {
3518 '1:1': 1,
3519 '16:9': 16 / 9,
3520 '9:16': 9 / 16,
3521 };
3300 default:3522 default:
3301 console.warn(`Unknown source "${source}" for aspect ratio calculation.`);3523 console.warn(`Unknown source "${source}" for aspect ratio calculation.`);
3302 return null;3524 return null;
@@ -3325,9 +3547,15 @@ function getClosestAspectRatio(width, height, source) {
3325 * Get closest size for Electron Hub3547 * Get closest size for Electron Hub
3326 * @param {number} width - The width of the image3548 * @param {number} width - The width of the image
3327 * @param {number} height - The height of the image3549 * @param {number} height - The height of the image
3550 * @param {string[]} sizes - Available sizes
3328 * @returns {Promise<string>} - The closest size3551 * @returns {Promise<string>} - The closest size
3329 */3552 */
3330async function getClosestSize(width, height) {3553async function getClosestSize(width, height, sizes = []) {
3554 const sizesData = [];
3555
3556 if (Array.isArray(sizes) && sizes.length > 0) {
3557 sizesData.push(...sizes);
3558 } else if (extension_settings.sd.source === sources.electronhub) {
3331 const response = await fetch('/api/sd/electronhub/sizes', {3559 const response = await fetch('/api/sd/electronhub/sizes', {
3332 method: 'POST',3560 method: 'POST',
3333 headers: getRequestHeaders(),3561 headers: getRequestHeaders(),
@@ -3340,7 +3568,20 @@ async function getClosestSize(width, height) {
3340 throw new Error(text);3568 throw new Error(text);
3341 }3569 }
3342 const result = await response.json();3570 const result = await response.json();
3343 const sizesData = result.sizes;3571 sizesData.push(...result.sizes);
3572 } else {
3573 return null;
3574 }
3575
3576 const targetWidth = Number(width);
3577 const targetHeight = Number(height);
3578
3579 if (isNaN(targetWidth) || isNaN(targetHeight)) {
3580 return null;
3581 }
3582
3583 const targetAspect = targetWidth / targetHeight;
3584 const targetResolution = targetWidth * targetHeight;
33443585
3345 const closestSize = sizesData.reduce((closest, size) => {3586 const closestSize = sizesData.reduce((closest, size) => {
3346 if (!size || typeof size !== 'string') {3587 if (!size || typeof size !== 'string') {
@@ -3353,16 +3594,14 @@ async function getClosestSize(width, height) {
33533594
3354 const sizeWidth = Number(sizeParts[0]);3595 const sizeWidth = Number(sizeParts[0]);
3355 const sizeHeight = Number(sizeParts[1]);3596 const sizeHeight = Number(sizeParts[1]);
3356 const targetWidth = Number(width);
3357 const targetHeight = Number(height);
33583597
3359 if (isNaN(sizeWidth) || isNaN(sizeHeight) || isNaN(targetWidth) || isNaN(targetHeight)) {3598 if (isNaN(sizeWidth) || isNaN(sizeHeight)) {
3360 return closest;3599 return closest;
3361 }3600 }
33623601
3363 const sizeArea = sizeWidth * sizeHeight;3602 const aspectDiff = Math.abs((sizeWidth / sizeHeight) - targetAspect) / targetAspect;
3364 const targetArea = targetWidth * targetHeight;3603 const resolutionDiff = Math.abs(sizeWidth * sizeHeight - targetResolution) / targetResolution;
3365 const diff = Math.abs(sizeArea - targetArea);3604 const diff = aspectDiff + resolutionDiff;
33663605
3367 return diff < closest.diff ? { size, diff } : closest;3606 return diff < closest.diff ? { size, diff } : closest;
3368 }, { size: null, diff: Infinity });3607 }, { size: null, diff: Infinity });
@@ -3532,6 +3771,55 @@ async function generateAutoImage(prompt, negativePrompt, signal) {
3532}3771}
35333772
3534/**3773/**
3774 * Generates an image using stable-diffusion.cpp server API.
3775 *
3776 * @param {string} prompt - The main instruction used to guide the image generation.
3777 * @param {string} negativePrompt - The instruction used to restrict the image generation.
3778 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3779 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3780 */
3781async function generateSdcppImage(prompt, negativePrompt, signal) {
3782 const payload = {
3783 url: extension_settings.sd.sdcpp_url,
3784 prompt: prompt,
3785 negative_prompt: negativePrompt,
3786 steps: extension_settings.sd.steps,
3787 cfg_scale: extension_settings.sd.scale,
3788 width: extension_settings.sd.width,
3789 height: extension_settings.sd.height,
3790 batch_size: 1,
3791 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
3792 };
3793
3794 if (extension_settings.sd.sampler && extension_settings.sd.sampler !== 'N/A') {
3795 payload.sampler_name = extension_settings.sd.sampler;
3796 }
3797
3798 if (extension_settings.sd.scheduler && extension_settings.sd.scheduler !== 'N/A') {
3799 payload.scheduler = extension_settings.sd.scheduler;
3800 }
3801
3802 if (Number.isFinite(extension_settings.sd.clip_skip)) {
3803 payload.clip_skip = extension_settings.sd.clip_skip;
3804 }
3805
3806 const result = await fetch('/api/sd/sdcpp/generate', {
3807 method: 'POST',
3808 headers: getRequestHeaders(),
3809 signal: signal,
3810 body: JSON.stringify(payload),
3811 });
3812
3813 if (result.ok) {
3814 const data = await result.json();
3815 return { format: 'png', data: data.images?.[0] };
3816 } else {
3817 const text = await result.text();
3818 throw new Error(text);
3819 }
3820}
3821
3822/**
3535 * Generates an image in Drawthings API using the provided prompt and configuration settings.3823 * Generates an image in Drawthings API using the provided prompt and configuration settings.
3536 *3824 *
3537 * @param {string} prompt - The main instruction used to guide the image generation.3825 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3697,7 +3985,7 @@ async function generateOpenAiImage(prompt, signal) {
36973985
3698 const isDalle2 = /dall-e-2/.test(extension_settings.sd.model);3986 const isDalle2 = /dall-e-2/.test(extension_settings.sd.model);
3699 const isDalle3 = /dall-e-3/.test(extension_settings.sd.model);3987 const isDalle3 = /dall-e-3/.test(extension_settings.sd.model);
3700 const isGptImg = /gpt-image-1/.test(extension_settings.sd.model);3988 const isGptImg = /gpt-image-(1|latest)/.test(extension_settings.sd.model);
3701 const isSora2 = /sora-2/.test(extension_settings.sd.model);3989 const isSora2 = /sora-2/.test(extension_settings.sd.model);
37023990
3703 if (isDalle2 && prompt.length > dalle2PromptLimit) {3991 if (isDalle2 && prompt.length > dalle2PromptLimit) {
@@ -3770,7 +4058,7 @@ async function generateOpenAiImage(prompt, signal) {
3770 model: extension_settings.sd.model,4058 model: extension_settings.sd.model,
3771 size: `${width}x${height}`,4059 size: `${width}x${height}`,
3772 n: 1,4060 n: 1,
3773 quality: isDalle3 ? extension_settings.sd.openai_quality : undefined,4061 quality: isDalle3 ? extension_settings.sd.openai_quality : (isGptImg ? extension_settings.sd.openai_quality_gpt : undefined),
3774 style: isDalle3 ? extension_settings.sd.openai_style : undefined,4062 style: isDalle3 ? extension_settings.sd.openai_style : undefined,
3775 response_format: isDalle2 || isDalle3 ? 'b64_json' : undefined,4063 response_format: isDalle2 || isDalle3 ? 'b64_json' : undefined,
3776 moderation: isGptImg ? 'low' : undefined,4064 moderation: isGptImg ? 'low' : undefined,
@@ -4242,16 +4530,54 @@ async function generateGoogleImage(prompt, negativePrompt, signal) {
4242 * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.4530 * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
4243 */4531 */
4244async function generateZaiImage(prompt, signal) {4532async function generateZaiImage(prompt, signal) {
4245 // Round width and height to nearest multiple of 16, and clamp to 512-2048 range4533 // Video generation models (CogVideoX, Viduq1)
4246 let width = clamp(Math.round(extension_settings.sd.width / 16) * 16, 512, 2048);4534 if (/(cogvideox|vidu)/.test(extension_settings.sd.model)) {
4247 let height = clamp(Math.round(extension_settings.sd.height / 16) * 16, 512, 2048);4535 const videoParams = {};
4536 if (/cogvideox/.test(extension_settings.sd.model)) {
4537 const cogVideoSizes = ['1280x720', '720x1280', '1024x1024', '1080x1920', '2048x1080', '3840x2160'];
4538 videoParams.quality = extension_settings.sd.openai_quality === 'hd' ? 'quality' : 'speed';
4539 videoParams.size = await getClosestSize(extension_settings.sd.width, extension_settings.sd.height, cogVideoSizes);
4540 }
4541 if (/vidu/.test(extension_settings.sd.model)) {
4542 videoParams.aspect_ratio = getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'zai');
4543 }
4544
4545 const videoResult = await fetch('/api/sd/zai/generate-video', {
4546 method: 'POST',
4547 headers: getRequestHeaders(),
4548 signal: signal,
4549 body: JSON.stringify({
4550 prompt: prompt,
4551 model: extension_settings.sd.model,
4552 ...videoParams,
4553 }),
4554 });
4555
4556 if (videoResult.ok) {
4557 const data = await videoResult.json();
4558 return { format: data.format, data: data.video };
4559 }
42484560
4249 // Make sure the pixel count does not exceed 2^21px4561 const text = await videoResult.text();
4562 throw new Error(text);
4563 } else {
4564 // Image generation models (GLM-Image, CogView)
4565 // GLM-Image requires multiples of 32, CogView requires multiples of 16
4566 const isGlmImage = /glm-image/.test(extension_settings.sd.model);
4567 const multiple = isGlmImage ? 32 : 16;
4568
4569 // Round width and height to nearest multiple and clamp to 512-2048 range
4570 let width = clamp(Math.round(extension_settings.sd.width / multiple) * multiple, 512, 2048);
4571 let height = clamp(Math.round(extension_settings.sd.height / multiple) * multiple, 512, 2048);
4572
4573 // CogView has a 2^21px pixel count limit, GLM-Image does not
4574 if (!isGlmImage) {
4250 while ((width * height) > Math.pow(2, 21)) {4575 while ((width * height) > Math.pow(2, 21)) {
4251 if (width >= height) {4576 if (width >= height) {
4252 width -= 16;4577 width -= multiple;
4253 } else {4578 } else {
4254 height -= 16;4579 height -= multiple;
4580 }
4255 }4581 }
4256 }4582 }
42574583
@@ -4275,6 +4601,7 @@ async function generateZaiImage(prompt, signal) {
4275 const text = await result.text();4601 const text = await result.text();
4276 throw new Error(text);4602 throw new Error(text);
4277 }4603 }
4604}
42784605
4279/**4606/**
4280 * Generates an image using the OpenRouter API.4607 * Generates an image using the OpenRouter API.
@@ -4443,6 +4770,58 @@ async function onComfyDeleteWorkflowClick() {
4443 onComfyWorkflowChange();4770 onComfyWorkflowChange();
4444}4771}
44454772
4773async function onComfyRenameWorkflowClick() {
4774 const oldName = extension_settings.sd.comfy_workflow;
4775
4776 if (!oldName) {
4777 return;
4778 }
4779
4780 let newName = await callGenericPopup(t`Enter new workflow name:`, POPUP_TYPE.INPUT, oldName);
4781
4782 if (!newName) {
4783 return;
4784 }
4785
4786 newName = String(newName).trim();
4787
4788 if (!newName.toLowerCase().endsWith('.json')) {
4789 newName += '.json';
4790 }
4791
4792 if (newName === oldName) {
4793 return;
4794 }
4795
4796 const existingWorkflow = Array
4797 .from(document.querySelectorAll('#sd_comfy_workflow option'))
4798 .find(opt => opt instanceof HTMLOptionElement && opt.value === newName);
4799
4800 if (existingWorkflow) {
4801 toastr.warning(t`A workflow with that name already exists`);
4802 return;
4803 }
4804
4805 const response = await fetch('/api/sd/comfy/rename-workflow', {
4806 method: 'POST',
4807 headers: getRequestHeaders(),
4808 body: JSON.stringify({
4809 old_name: oldName,
4810 new_name: newName,
4811 }),
4812 });
4813
4814 if (!response.ok) {
4815 const text = await response.text();
4816 toastr.error(t`Failed to rename workflow.\n\n${text}`);
4817 return;
4818 }
4819
4820 extension_settings.sd.comfy_workflow = newName;
4821 saveSettingsDebounced();
4822 await loadComfyWorkflows();
4823}
4824
4446/**4825/**
4447 * Sends a chat message with the generated image.4826 * Sends a chat message with the generated image.
4448 * @param {string} prompt Prompt used for the image generation4827 * @param {string} prompt Prompt used for the image generation
@@ -4575,6 +4954,8 @@ function isValidState() {
4575 return true;4954 return true;
4576 case sources.auto:4955 case sources.auto:
4577 return !!extension_settings.sd.auto_url;4956 return !!extension_settings.sd.auto_url;
4957 case sources.sdcpp:
4958 return !!extension_settings.sd.sdcpp_url;
4578 case sources.drawthings:4959 case sources.drawthings:
4579 return !!extension_settings.sd.drawthings_url;4960 return !!extension_settings.sd.drawthings_url;
4580 case sources.vlad:4961 case sources.vlad:
@@ -4598,7 +4979,7 @@ function isValidState() {
4598 case sources.togetherai:4979 case sources.togetherai:
4599 return secret_state[SECRET_KEYS.TOGETHERAI];4980 return secret_state[SECRET_KEYS.TOGETHERAI];
4600 case sources.pollinations:4981 case sources.pollinations:
4601 return true;4982 return secret_state[SECRET_KEYS.POLLINATIONS];
4602 case sources.stability:4983 case sources.stability:
4603 return secret_state[SECRET_KEYS.STABILITY];4984 return secret_state[SECRET_KEYS.STABILITY];
4604 case sources.huggingface:4985 case sources.huggingface:
@@ -4626,7 +5007,8 @@ function isValidState() {
4626 }5007 }
4627}5008}
46285009
4629let buttonAbortController = null;5010/** @type {WeakMap<HTMLElement, AbortController>} */
5011const buttonAbortControllers = new WeakMap();
46305012
4631/**5013/**
4632 * "Paintbrush" button handler to generate a new image for a message.5014 * "Paintbrush" button handler to generate a new image for a message.
@@ -4644,16 +5026,30 @@ async function sdMessageButton($icon, { animate } = {}) {
4644 $icon.toggleClass(classes.idle, !isBusy);5026 $icon.toggleClass(classes.idle, !isBusy);
4645 $icon.toggleClass(classes.busy, isBusy);5027 $icon.toggleClass(classes.busy, isBusy);
4646 $media.toggleClass(classes.animation, isBusy);5028 $media.toggleClass(classes.animation, isBusy);
5029
5030 // Update generation counter toast
5031 const trackingFunction = isBusy ? startGenerationTracking : endGenerationTracking;
5032 trackingFunction();
4647 }5033 }
46485034
4649 let $media = jQuery();5035 let $media = jQuery();
46505036
4651 const classes = { busy: 'fa-hourglass', idle: 'fa-paintbrush', animation: 'fa-fade' };5037 const classes = { busy: 'fa-hourglass', idle: 'fa-paintbrush', animation: 'fa-fade' };
4652 const context = getContext();5038 const context = getContext();
5039 const abortController = (() => {
5040 const nativeElement = $icon.get(0);
5041 if (buttonAbortControllers.has(nativeElement)) {
5042 return buttonAbortControllers.get(nativeElement);
5043 } else {
5044 const controller = new AbortController();
5045 buttonAbortControllers.set(nativeElement, controller);
5046 return controller;
5047 }
5048 })();
46535049
4654 if ($icon.hasClass(classes.busy)) {5050 if ($icon.hasClass(classes.busy)) {
4655 buttonAbortController?.abort('Aborted by user');5051 abortController.abort('Aborted by user');
4656 console.log('Previous image is still being generated...');5052 console.log('SD: Image generation aborted by user');
4657 return;5053 return;
4658 }5054 }
46595055
@@ -4690,13 +5086,12 @@ async function sdMessageButton($icon, { animate } = {}) {
4690 $media = messageElement.find(`.mes_media_container[data-index="${index}"]`).find('.mes_img, .mes_video');5086 $media = messageElement.find(`.mes_media_container[data-index="${index}"]`).find('.mes_img, .mes_video');
4691 }5087 }
46925088
4693 buttonAbortController = new AbortController();
4694 const newMediaAttachment = await generateMediaSwipe(5089 const newMediaAttachment = await generateMediaSwipe(
4695 selectedMedia,5090 selectedMedia,
4696 message,5091 message,
4697 () => setBusyIcon(true),5092 () => setBusyIcon(true),
4698 () => setBusyIcon(false),5093 () => setBusyIcon(false),
4699 buttonAbortController,5094 abortController,
4700 );5095 );
47015096
4702 if (!newMediaAttachment) {5097 if (!newMediaAttachment) {
@@ -4869,6 +5264,17 @@ function applyCommandArguments(args) {
4869 'denoise': 'denoising_strength',5264 'denoise': 'denoising_strength',
4870 '2ndpass': 'hr_second_pass_steps',5265 '2ndpass': 'hr_second_pass_steps',
4871 'faces': 'restore_faces',5266 'faces': 'restore_faces',
5267 'processing': 'minimal_prompt_processing',
5268 };
5269 const enumHandlers = {
5270 'processing': (value) => {
5271 if (/standard/gi.test(String(value))) {
5272 return false;
5273 }
5274 if (/minimal/gi.test(String(value))) {
5275 return true;
5276 }
5277 },
4872 };5278 };
48735279
4874 for (const [param, setting] of Object.entries(settingMap)) {5280 for (const [param, setting] of Object.entries(settingMap)) {
@@ -4877,6 +5283,14 @@ function applyCommandArguments(args) {
4877 }5283 }
4878 currentSettings[setting] = extension_settings.sd[setting];5284 currentSettings[setting] = extension_settings.sd[setting];
4879 const value = String(args[param]);5285 const value = String(args[param]);
5286 const enumHandler = enumHandlers[param];
5287 if (typeof enumHandler === 'function') {
5288 const enumValue = enumHandler(value);
5289 if (enumValue !== undefined) {
5290 overrideSettings[setting] = enumValue;
5291 }
5292 continue;
5293 }
4880 const type = typeof defaultSettings[setting];5294 const type = typeof defaultSettings[setting];
4881 switch (type) {5295 switch (type) {
4882 case 'boolean':5296 case 'boolean':
@@ -4999,6 +5413,17 @@ jQuery(async () => {
4999 acceptsMultiple: false,5413 acceptsMultiple: false,
5000 }),5414 }),
5001 SlashCommandNamedArgument.fromProps({5415 SlashCommandNamedArgument.fromProps({
5416 name: 'processing',
5417 description: 'level of response prompt processing returned by the LLM',
5418 typeList: [ARGUMENT_TYPE.STRING],
5419 enumList: [
5420 new SlashCommandEnumValue('standard', 'Standard prompt processing'),
5421 new SlashCommandEnumValue('minimal', 'Minimal prompt processing'),
5422 ],
5423 isRequired: false,
5424 acceptsMultiple: false,
5425 }),
5426 SlashCommandNamedArgument.fromProps({
5002 name: 'seed',5427 name: 'seed',
5003 description: 'random seed',5428 description: 'random seed',
5004 isRequired: false,5429 isRequired: false,
@@ -5243,6 +5668,8 @@ jQuery(async () => {
5243 $('#sd_auto_validate').on('click', validateAutoUrl);5668 $('#sd_auto_validate').on('click', validateAutoUrl);
5244 $('#sd_auto_url').on('input', onAutoUrlInput);5669 $('#sd_auto_url').on('input', onAutoUrlInput);
5245 $('#sd_auto_auth').on('input', onAutoAuthInput);5670 $('#sd_auto_auth').on('input', onAutoAuthInput);
5671 $('#sd_sdcpp_validate').on('click', validateSdcppUrl);
5672 $('#sd_sdcpp_url').on('input', onSdcppUrlInput);
5246 $('#sd_drawthings_validate').on('click', validateDrawthingsUrl);5673 $('#sd_drawthings_validate').on('click', validateDrawthingsUrl);
5247 $('#sd_drawthings_url').on('input', onDrawthingsUrlInput);5674 $('#sd_drawthings_url').on('input', onDrawthingsUrlInput);
5248 $('#sd_drawthings_auth').on('input', onDrawthingsAuthInput);5675 $('#sd_drawthings_auth').on('input', onDrawthingsAuthInput);
@@ -5268,9 +5695,11 @@ jQuery(async () => {
5268 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);5695 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);
5269 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);5696 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
5270 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);5697 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
5698 $('#sd_comfy_rename_workflow').on('click', onComfyRenameWorkflowClick);
5271 $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick);5699 $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick);
5272 $('#sd_style').on('change', onStyleSelect);5700 $('#sd_style').on('change', onStyleSelect);
5273 $('#sd_save_style').on('click', onSaveStyleClick);5701 $('#sd_save_style').on('click', onSaveStyleClick);
5702 $('#sd_rename_style').on('click', onRenameStyleClick);
5274 $('#sd_delete_style').on('click', onDeleteStyleClick);5703 $('#sd_delete_style').on('click', onDeleteStyleClick);
5275 $('#sd_character_prompt_block').hide();5704 $('#sd_character_prompt_block').hide();
5276 $('#sd_interactive_mode').on('input', onInteractiveModeInput);5705 $('#sd_interactive_mode').on('input', onInteractiveModeInput);
@@ -5279,6 +5708,7 @@ jQuery(async () => {
5279 $('#sd_openai_duration').on('input', onOpenAiDurationSelect);5708 $('#sd_openai_duration').on('input', onOpenAiDurationSelect);
5280 $('#sd_multimodal_captioning').on('input', onMultimodalCaptioningInput);5709 $('#sd_multimodal_captioning').on('input', onMultimodalCaptioningInput);
5281 $('#sd_snap').on('input', onSnapInput);5710 $('#sd_snap').on('input', onSnapInput);
5711 $('#sd_minimal_prompt_processing').on('input', onMinimalPromptProcessing);
5282 $('#sd_clip_skip').on('input', onClipSkipInput);5712 $('#sd_clip_skip').on('input', onClipSkipInput);
5283 $('#sd_seed').on('input', onSeedInput);5713 $('#sd_seed').on('input', onSeedInput);
5284 $('#sd_character_prompt_share').on('input', onCharacterPromptShareInput);5714 $('#sd_character_prompt_share').on('input', onCharacterPromptShareInput);
@@ -5309,6 +5739,10 @@ jQuery(async () => {
5309 extension_settings.sd.electronhub_quality = String($(this).val());5739 extension_settings.sd.electronhub_quality = String($(this).val());
5310 saveSettingsDebounced();5740 saveSettingsDebounced();
5311 });5741 });
5742 $('#sd_openai_quality_gpt').on('input', function () {
5743 extension_settings.sd.openai_quality_gpt = String($(this).val());
5744 saveSettingsDebounced();
5745 });
53125746
5313 if (!CSS.supports('field-sizing', 'content')) {5747 if (!CSS.supports('field-sizing', 'content')) {
5314 $('.sd_settings .inline-drawer-toggle').on('click', function () {5748 $('.sd_settings .inline-drawer-toggle').on('click', function () {
@@ -5337,15 +5771,19 @@ jQuery(async () => {
53375771
5338 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {5772 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
5339 eventSource.on(event, async (/** @type {string} */ key) => {5773 eventSource.on(event, async (/** @type {string} */ key) => {
5340 switch (key) {5774 const keySourceMap = {
5341 case SECRET_KEYS.BFL:5775 [sources.bfl]: SECRET_KEYS.BFL,
5342 case SECRET_KEYS.FALAI:5776 [sources.falai]: SECRET_KEYS.FALAI,
5343 case SECRET_KEYS.STABILITY:5777 [sources.stability]: SECRET_KEYS.STABILITY,
5344 case SECRET_KEYS.AIMLAPI:5778 [sources.aimlapi]: SECRET_KEYS.AIMLAPI,
5345 case SECRET_KEYS.COMFY_RUNPOD:5779 [sources.comfy]: SECRET_KEYS.COMFY_RUNPOD,
5346 await loadSettingOptions();5780 [sources.pollinations]: SECRET_KEYS.POLLINATIONS,
5347 break;5781 };
5782 const shouldReloadOptions = Object.entries(keySourceMap).some(([k, v]) => k === extension_settings.sd.source && v === key);
5783 if (!shouldReloadOptions) {
5784 return;
5348 }5785 }
5786 await loadSettingOptions();
5349 });5787 });
5350 });5788 });
53515789
public/scripts/extensions/stable-diffusion/settings.html+47 -9
@@ -35,6 +35,10 @@
35 <input id="sd_snap" type="checkbox" />35 <input id="sd_snap" type="checkbox" />
36 <span data-i18n="sd_snap_txt">Snap auto-adjusted resolutions</span>36 <span data-i18n="sd_snap_txt">Snap auto-adjusted resolutions</span>
37 </label>37 </label>
38 <label for="sd_minimal_prompt_processing" class="checkbox_label" data-i18n="[title]sd_minimal_prompt_processing" title="Reduce post-processing on a prompt generated by the LLM to preserve JSON and other structured output.">
39 <input id="sd_minimal_prompt_processing" type="checkbox" />
40 <span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span>
41 </label>
38 <label for="sd_source" data-i18n="Source">Source</label>42 <label for="sd_source" data-i18n="Source">Source</label>
39 <select id="sd_source">43 <select id="sd_source">
40 <option value="aimlapi">AI/ML API</option>44 <option value="aimlapi">AI/ML API</option>
@@ -55,10 +59,11 @@
55 <option value="vlad">SD.Next (vladmandic)</option>59 <option value="vlad">SD.Next (vladmandic)</option>
56 <option value="stability">Stability AI</option>60 <option value="stability">Stability AI</option>
57 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>61 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
62 <option value="sdcpp">stable-diffusion.cpp server</option>
58 <option value="horde">Stable Horde</option>63 <option value="horde">Stable Horde</option>
59 <option value="togetherai">TogetherAI</option>64 <option value="togetherai">TogetherAI</option>
60 <option value="xai">xAI (Grok)</option>65 <option value="xai">xAI (Grok)</option>
61 <option value="zai">Z.AI (CogView)</option>66 <option value="zai">Z.AI</option>
62 </select>67 </select>
63 <div data-sd-source="auto">68 <div data-sd-source="auto">
64 <label for="sd_auto_url">SD Web UI URL</label>69 <label for="sd_auto_url">SD Web UI URL</label>
@@ -76,6 +81,19 @@
76 <!-- (Original Text)<b>Important:</b> run SD Web UI with the <tt>--api</tt> flag! The server must be accessible from the SillyTavern host machine. -->81 <!-- (Original Text)<b>Important:</b> run SD Web UI with the <tt>--api</tt> flag! The server must be accessible from the SillyTavern host machine. -->
77 <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_auto_auth_warning_1"> run SD Web UI with the </i><i><tt>--api</tt></i><i data-i18n="sd_auto_auth_warning_2"> flag! The server must be accessible from the SillyTavern host machine.</i>82 <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_auto_auth_warning_1"> run SD Web UI with the </i><i><tt>--api</tt></i><i data-i18n="sd_auto_auth_warning_2"> flag! The server must be accessible from the SillyTavern host machine.</i>
78 </div>83 </div>
84 <div data-sd-source="sdcpp">
85 <label for="sd_sdcpp_url">stable-diffusion.cpp URL</label>
86 <div class="flex-container flexnowrap">
87 <input id="sd_sdcpp_url" type="text" class="text_pole" placeholder="Example: {{sdcpp_url}}" value="{{sdcpp_url}}" />
88 <div id="sd_sdcpp_validate" class="menu_button menu_button_icon">
89 <i class="fa-solid fa-check"></i>
90 <span data-i18n="Connect">
91 Connect
92 </span>
93 </div>
94 </div>
95 <i data-i18n="The server must be accessible from the SillyTavern host machine.">The server must be accessible from the SillyTavern host machine.</i>
96 </div>
79 <div data-sd-source="drawthings">97 <div data-sd-source="drawthings">
80 <label for="sd_drawthings_url">DrawThings API URL</label>98 <label for="sd_drawthings_url">DrawThings API URL</label>
81 <div class="flex-container flexnowrap">99 <div class="flex-container flexnowrap">
@@ -178,7 +196,16 @@
178 <option value="natural">Natural</option>196 <option value="natural">Natural</option>
179 </select>197 </select>
180 </div>198 </div>
181 <div data-sd-model="dall-e-3,cogview-4-250304" class="flex1">199 <div data-sd-model="gpt-image" class="flex1">
200 <label for="sd_openai_quality_gpt" data-i18n="Image Quality">Image Quality</label>
201 <select id="sd_openai_quality_gpt">
202 <option value="auto" data-i18n="Auto">Auto</option>
203 <option value="low" data-i18n="Low">Low</option>
204 <option value="medium" data-i18n="Medium">Medium</option>
205 <option value="high" data-i18n="High">High</option>
206 </select>
207 </div>
208 <div data-sd-model="dall-e-3,cogview-4,glm-image,cogvideox" class="flex1">
182 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>209 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
183 <select id="sd_openai_quality">210 <select id="sd_openai_quality">
184 <option value="standard" data-i18n="Standard">Standard</option>211 <option value="standard" data-i18n="Standard">Standard</option>
@@ -248,15 +275,23 @@
248 <div id="sd_comfy_new_workflow" class="menu_button menu_button_icon" data-i18n="[title]Create new workflow" title="Create new workflow">275 <div id="sd_comfy_new_workflow" class="menu_button menu_button_icon" data-i18n="[title]Create new workflow" title="Create new workflow">
249 <i class="fa-solid fa-plus"></i>276 <i class="fa-solid fa-plus"></i>
250 </div>277 </div>
278 <div id="sd_comfy_rename_workflow" class="menu_button menu_button_icon" data-i18n="[title]Rename workflow" title="Rename workflow">
279 <i class="fa-solid fa-pencil"></i>
280 </div>
251 <div id="sd_comfy_delete_workflow" class="menu_button menu_button_icon" data-i18n="[title]Delete workflow" title="Delete workflow">281 <div id="sd_comfy_delete_workflow" class="menu_button menu_button_icon" data-i18n="[title]Delete workflow" title="Delete workflow">
252 <i class="fa-solid fa-trash-can"></i>282 <i class="fa-solid fa-trash-can"></i>
253 </div>283 </div>
254 </div>284 </div>
255 </div>285 </div>
256 <div data-sd-source="pollinations">286 <div data-sd-source="pollinations">
257 <p>287 <a href="https://enter.pollinations.ai">Pollinations.ai</a>
258 <a href="https://pollinations.ai">Pollinations.ai</a>288 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
259 </p>289 <strong class="flex1" data-i18n="API Key">API Key</strong>
290 <div id="sd_pollinations_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_pollinations">
291 <i class="fa-fw fa-solid fa-key"></i>
292 <span data-i18n="Click to set">Click to set</span>
293 </div>
294 </div>
260 <div class="flex-container">295 <div class="flex-container">
261 <label class="flex1 checkbox_label" for="sd_pollinations_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail).">296 <label class="flex1 checkbox_label" for="sd_pollinations_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail).">
262 <input id="sd_pollinations_enhance" type="checkbox" />297 <input id="sd_pollinations_enhance" type="checkbox" />
@@ -381,12 +416,12 @@
381 </div>416 </div>
382417
383 <div class="flex-container">418 <div class="flex-container">
384 <div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy">419 <div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy,sdcpp">
385 <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>420 <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
386 <select id="sd_sampler"></select>421 <select id="sd_sampler"></select>
387 </div>422 </div>
388423
389 <div class="flex1" data-sd-source="comfy,auto,novel">424 <div class="flex1" data-sd-source="comfy,auto,novel,sdcpp">
390 <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>425 <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
391 <select id="sd_scheduler"></select>426 <select id="sd_scheduler"></select>
392 </div>427 </div>
@@ -469,7 +504,7 @@
469 <input class="neo-range-input" type="number" id="sd_hr_second_pass_steps_value" data-for="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" >504 <input class="neo-range-input" type="number" id="sd_hr_second_pass_steps_value" data-for="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" >
470 </div>505 </div>
471506
472 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras">507 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras,sdcpp">
473 <small>508 <small>
474 <span data-i18n="CLIP Skip">CLIP Skip</span>509 <span data-i18n="CLIP Skip">CLIP Skip</span>
475 </small>510 </small>
@@ -523,7 +558,7 @@
523 </label>558 </label>
524 </div>559 </div>
525560
526 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,bfl" class="marginTop5">561 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,bfl,sdcpp" class="marginTop5">
527 <label for="sd_seed">562 <label for="sd_seed">
528 <span data-i18n="Seed">Seed</span>563 <span data-i18n="Seed">Seed</span>
529 <small data-i18n="(-1 for random)">(-1 for random)</small>564 <small data-i18n="(-1 for random)">(-1 for random)</small>
@@ -540,6 +575,9 @@
540 <div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button">575 <div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button">
541 <i class="fa-solid fa-save"></i>576 <i class="fa-solid fa-save"></i>
542 </div>577 </div>
578 <div id="sd_rename_style" data-i18n="[title]Rename style" title="Rename style" class="menu_button">
579 <i class="fa-solid fa-pencil"></i>
580 </div>
543 <div id="sd_delete_style" data-i18n="[title]Delete style" title="Delete style" class="menu_button">581 <div id="sd_delete_style" data-i18n="[title]Delete style" title="Delete style" class="menu_button">
544 <i class="fa-solid fa-trash-can"></i>582 <i class="fa-solid fa-trash-can"></i>
545 </div>583 </div>
public/scripts/extensions/tts/coqui.js+20 -20
@@ -207,13 +207,13 @@ class CoquiTtsProvider {
207 this.settings.customVoices = {};207 this.settings.customVoices = {};
208 for (let voiceName in this.settings.voiceMapDict) {208 for (let voiceName in this.settings.voiceMapDict) {
209 const voiceId = this.settings.voiceMapDict[voiceName];209 const voiceId = this.settings.voiceMapDict[voiceName];
210 this.settings.customVoices[voiceName] = voiceId['model_id'];210 this.settings.customVoices[voiceName] = voiceId.model_id;
211211
212 if (voiceId['model_language'] != null)212 if (voiceId.model_language != null)
213 this.settings.customVoices[voiceName] += '[' + voiceId['model_language'] + ']';213 this.settings.customVoices[voiceName] += '[' + voiceId.model_language + ']';
214214
215 if (voiceId['model_speaker'] != null)215 if (voiceId.model_speaker != null)
216 this.settings.customVoices[voiceName] += '[' + voiceId['model_speaker'] + ']';216 this.settings.customVoices[voiceName] += '[' + voiceId.model_speaker + ']';
217 }217 }
218218
219 // Update UI select list with voices219 // Update UI select list with voices
@@ -493,8 +493,8 @@ class CoquiTtsProvider {
493 .append('<option value="none">Select language</option>')493 .append('<option value="none">Select language</option>')
494 .val('none');494 .val('none');
495495
496 for (let i = 0; i < model_settings['languages'].length; i++) {496 for (let i = 0; i < model_settings.languages.length; i++) {
497 const language_label = JSON.stringify(model_settings['languages'][i]).replaceAll('"', '');497 const language_label = JSON.stringify(model_settings.languages[i]).replaceAll('"', '');
498 $('#coqui_api_model_settings_language').append(new Option(language_label, i));498 $('#coqui_api_model_settings_language').append(new Option(language_label, i));
499 }499 }
500 }500 }
@@ -512,8 +512,8 @@ class CoquiTtsProvider {
512 .append('<option value="none">Select speaker</option>')512 .append('<option value="none">Select speaker</option>')
513 .val('none');513 .val('none');
514514
515 for (let i = 0; i < model_settings['speakers'].length; i++) {515 for (let i = 0; i < model_settings.speakers.length; i++) {
516 const speaker_label = JSON.stringify(model_settings['speakers'][i]).replaceAll('"', '');516 const speaker_label = JSON.stringify(model_settings.speakers[i]).replaceAll('"', '');
517 $('#coqui_api_model_settings_speaker').append(new Option(speaker_label, i));517 $('#coqui_api_model_settings_speaker').append(new Option(speaker_label, i));
518 }518 }
519 }519 }
@@ -525,11 +525,11 @@ class CoquiTtsProvider {
525 $('#coqui_api_model_install_status').show();525 $('#coqui_api_model_install_status').show();
526526
527 // Check if already installed and propose to do it otherwise527 // Check if already installed and propose to do it otherwise
528 const model_id = modelDict[model_language][model_dataset][model_name]['id'];528 const model_id = modelDict[model_language][model_dataset][model_name].id;
529 console.debug(DEBUG_PREFIX,'Check if model is already installed',model_id);529 console.debug(DEBUG_PREFIX,'Check if model is already installed',model_id);
530 let result = await CoquiTtsProvider.checkmodel_state(model_id);530 const result = await CoquiTtsProvider.checkmodel_state(model_id);
531 result = await result.json();531 const resultJSON = await result.json();
532 const model_state = result['model_state'];532 const model_state = resultJSON.model_state;
533533
534 console.debug(DEBUG_PREFIX, ' Model state:', model_state);534 console.debug(DEBUG_PREFIX, ' Model state:', model_state);
535535
@@ -556,18 +556,18 @@ class CoquiTtsProvider {
556 $('#coqui_api_model_install_status').text('Downloading model...');556 $('#coqui_api_model_install_status').text('Downloading model...');
557 $('#coqui_api_model_install_button').hide();557 $('#coqui_api_model_install_button').hide();
558 //toastr.info("For model "+model_id, DEBUG_PREFIX+" Started "+action, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });558 //toastr.info("For model "+model_id, DEBUG_PREFIX+" Started "+action, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
559 let apiResult = await CoquiTtsProvider.installModel(model_id, action);559 const apiResult = await CoquiTtsProvider.installModel(model_id, action);
560 apiResult = await apiResult.json();560 const apiResultJSON = await apiResult.json();
561561
562 console.debug(DEBUG_PREFIX, 'Response:', apiResult);562 console.debug(DEBUG_PREFIX, 'Response:', apiResult);
563563
564 if (apiResult['status'] == 'done') {564 if (apiResultJSON.status == 'done') {
565 $('#coqui_api_model_install_status').text('Model installed and ready to use!');565 $('#coqui_api_model_install_status').text('Model installed and ready to use!');
566 $('#coqui_api_model_install_button').hide();566 $('#coqui_api_model_install_button').hide();
567 onModelNameChange_pointer();567 onModelNameChange_pointer();
568 }568 }
569569
570 if (apiResult['status'] == 'downloading') {570 if (apiResultJSON.status == 'downloading') {
571 toastr.error('Check extras console for progress', DEBUG_PREFIX + ' already downloading', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });571 toastr.error('Check extras console for progress', DEBUG_PREFIX + ' already downloading', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
572 $('#coqui_api_model_install_status').text('Already downloading a model, check extras console!');572 $('#coqui_api_model_install_status').text('Already downloading a model, check extras console!');
573 $('#coqui_api_model_install_button').show();573 $('#coqui_api_model_install_button').show();
@@ -750,10 +750,10 @@ async function initLocalModels() {
750750
751 // Initialized local model once751 // Initialized local model once
752 if (!coquiLocalModelsReceived) {752 if (!coquiLocalModelsReceived) {
753 let result = await CoquiTtsProvider.getLocalModelList();753 const result = await CoquiTtsProvider.getLocalModelList();
754 result = await result.json();754 const resultJSON = await result.json();
755755
756 coquiLocalModels = result['models_list'];756 coquiLocalModels = resultJSON.models_list;
757757
758 $('#coqui_local_model_name').show();758 $('#coqui_local_model_name').show();
759 $('#coqui_local_model_name')759 $('#coqui_local_model_name')
public/scripts/extensions/tts/cosyvoice.js+1 -1
@@ -175,7 +175,7 @@ class CosyVoiceProvider {
175 };175 };
176176
177 if (streaming) {177 if (streaming) {
178 params['streaming'] = 1;178 params.streaming = 1;
179 }179 }
180180
181 const url = `${this.settings.provider_endpoint}/`;181 const url = `${this.settings.provider_endpoint}/`;
public/scripts/extensions/tts/elevenlabs.js+0 -1
@@ -126,16 +126,16 @@ class ElevenLabsTtsProvider {
126 this.settings = this.defaultSettings;126 this.settings = this.defaultSettings;
127127
128 // Migrate old settings128 // Migrate old settings
129 if (settings['multilingual'] !== undefined) {
public/scripts/extensions/tts/gpt-sovits-adapter.js+0 -0
public/scripts/extensions/tts/index.js+0 -0
public/scripts/extensions/tts/settings.html+0 -0
public/scripts/extensions/tts/volcengine.js+0 -0
public/scripts/extensions/vectors/index.js+0 -0
public/scripts/extensions/vectors/settings.html+0 -0
public/scripts/group-chats.js+0 -0
public/scripts/horde.js+0 -0
public/scripts/i18n.js+0 -0
public/scripts/instruct-mode.js+0 -0
public/scripts/itemized-prompts.js+0 -0
public/scripts/macros.js+0 -0
public/scripts/macros/definitions/chat-macros.js+0 -0
public/scripts/macros/definitions/core-macros.js+0 -0
public/scripts/macros/definitions/state-macros.js+0 -0
public/scripts/macros/definitions/variable-macros.js+0 -0
public/scripts/macros/MacroBrowser.js → public/scripts/macros/engine/MacroBrowser.js+0 -0
public/scripts/macros/engine/MacroCstWalker.js+0 -0
public/scripts/macros/engine/MacroDiagnostics.js+0 -0
public/scripts/macros/engine/MacroEngine.js+0 -0
public/scripts/macros/engine/MacroEnv.types.js+0 -0
public/scripts/macros/engine/MacroEnvBuilder.js+0 -0
public/scripts/macros/engine/MacroFlags.js+0 -0
public/scripts/macros/engine/MacroLexer.js+0 -0
public/scripts/macros/engine/MacroParser.js+0 -0
public/scripts/macros/engine/MacroRegistry.js+0 -0
public/scripts/macros/macro-system.js+0 -0
public/scripts/openai.js+0 -0
public/scripts/personas.js+0 -0
public/scripts/power-user.js+0 -0
public/scripts/preset-manager.js+0 -0
public/scripts/reasoning.js+0 -0
public/scripts/samplerSelect.js+0 -0
public/scripts/secrets.js+0 -0
public/scripts/slash-commands.js+0 -0
public/scripts/slash-commands/SlashCommandClosure.js+0 -0
public/scripts/slash-commands/SlashCommandParser.js+0 -0
public/scripts/st-context.js+0 -0
public/scripts/system-messages.js+0 -0
public/scripts/tags.js+0 -0
public/scripts/templates/welcomePanel.html+0 -0
public/scripts/textgen-models.js+0 -0
public/scripts/textgen-settings.js+0 -0
public/scripts/tokenizers.js+0 -0
public/scripts/tool-calling.js+0 -0
public/scripts/util/AccountStorage.js+0 -0
public/scripts/utils.js+0 -0
public/scripts/variables.js+0 -0
public/scripts/welcome-screen.js+0 -0
public/scripts/world-info.js+0 -0
public/style.css+0 -0
src/command-line.js+0 -0
src/config-init.js+0 -0
src/constants.js+0 -0
src/endpoints/assets.js+0 -0
src/endpoints/backends/chat-completions.js+0 -0
src/endpoints/backends/kobold.js+0 -0
src/endpoints/backends/text-completions.js+0 -0
src/endpoints/backgrounds.js+0 -0
src/endpoints/characters.js+0 -0
src/endpoints/chats.js+0 -0
src/endpoints/content-manager.js+0 -0
src/endpoints/groups.js+0 -0
src/endpoints/image-metadata.js+0 -0
src/endpoints/openai.js+0 -0
src/endpoints/openrouter.js+0 -0
src/endpoints/search.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/speech.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/thumbnails.js+0 -0
src/endpoints/tokenizers.js+0 -0
src/endpoints/translate.js+0 -0
src/endpoints/users-admin.js+0 -0
src/endpoints/vectors.js+0 -0
src/endpoints/volcengine.js+0 -0
src/healthcheck.js+0 -0
src/middleware/basicAuth.js+0 -0
src/prompt-converters.js+0 -0
src/server-main.js+0 -0
src/server-startup.js+0 -0
src/users.js+0 -0
src/util.js+0 -0
src/vectors/openai-vectors.js+0 -0
tests/.eslintrc.cjs+0 -0
tests/frontend/MacroEngine.e2e.js+0 -0
tests/frontend/MacroEnvBuilder.e2e.js+0 -0
tests/frontend/MacroLexer.e2e.js+0 -0
tests/frontend/MacroParser.e2e.js+0 -0
tests/frontend/MacroRegistry.e2e.js+0 -0
tests/frontend/MacroSlashCommands.e2e.js+0 -0
tests/frontend/MacroStoryString.e2e.js+0 -0
tests/package-lock.json+0 -0
Diff truncated