Merge pull request #5154 from SillyTavern/staging Staging
Signed| @@ -1,21 +1,61 @@ | ||
| 1 | +# --- Git & CI --- | |
| 1 | 2 | .git |
| 2 | 3 | .github |
| 3 | 4 | .vscodegitignore |
| 4 | -node_modules | |
| 5 | + | |
| 5 | -npm-debug.log | |
| 6 | +# --- Docker --- | |
| 6 | -readme* | |
| 7 | +/Dockerfile | |
| 7 | -Start.bat | |
| 8 | +/.dockerignore | |
| 8 | -/dist | |
| 9 | +/docker/docker-compose.yml | |
| 9 | -/backups | |
| 10 | -cloudflared.exe | |
| 11 | -access.log | |
| 12 | -/data | |
| 13 | -/cache | |
| 14 | -.DS_Store | |
| 15 | -/public/scripts/extensions/third-party | |
| 16 | -/colab | |
| 17 | -.gemini | |
| 18 | 10 | /docker/config |
| 19 | 11 | /docker/extensions |
| 20 | 12 | /docker/data |
| 21 | 13 | /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 --- | |
| 36 | +readme* | |
| 37 | +*.md | |
| 38 | +Update-Instructions.txt | |
| 39 | + | |
| 40 | +# --- OS & System Junk --- | |
| 41 | +**/.DS_Store | |
| 42 | +*.bat | |
| 43 | +*.cmd | |
| 44 | +*.exe | |
| 45 | +start.sh | |
| 46 | + | |
| 47 | +# --- Dev Config --- | |
| 48 | +.editorconfig | |
| 49 | +.eslintrc.cjs | |
| 50 | +.eslintrc* | |
| 51 | +.vscode | |
| 52 | +**/jsconfig.json | |
| 53 | +.npmignore | |
| 54 | +.gemini | |
| 55 | +replit.nix | |
| 56 | +.replit | |
| 57 | +.nomedia | |
| 58 | + | |
| 59 | +# -- Logs & Temp --- | |
| 60 | +*.log | |
| 61 | +**/tmp | |
| @@ -98,7 +98,7 @@ module.exports = { | ||
| 98 | 98 | 'no-cond-assign': 'error', |
| 99 | 99 | 'no-unneeded-ternary': 'error', |
| 100 | 100 | 'no-irregular-whitespace': ['error', { skipStrings: true, skipTemplates: true }], |
| 101 | - | |
| 101 | + 'dot-notation': ['error', { 'allowPattern': '[A-Z]\\w*$' }], | |
| 102 | 102 | // These rules should eventually be enabled. |
| 103 | 103 | 'no-async-promise-executor': 'off', |
| 104 | 104 | 'no-inner-declarations': 'off', |
| @@ -1,44 +1,48 @@ | ||
| 1 | 1 | FROM node:lts-alpine3.2223 |
| 2 | 2 | |
| 3 | 3 | # Arguments |
| 4 | 4 | ARG APP_HOME=/home/node/app |
| 5 | 5 | |
| 6 | 6 | # Install system dependencies |
| 7 | -RUN apk add --no-cache gcompat tini git git-lfs | |
| 7 | +# "Don't rely on the base image for tools; if you call it, you install it." ;) | |
| 8 | +RUN apk add --no-cache gcompat tini git git-lfs su-exec shadow dos2unix | |
| 8 | 9 | |
| 9 | 10 | # Create app directory and set ownership |
| 10 | 11 | WORKDIR ${APP_HOME} |
| 12 | +RUN chown node:node ${APP_HOME} | |
| 11 | 13 | |
| 12 | 14 | # Set NODE_ENV to production |
| 13 | 15 | ENV NODE_ENV=production |
| 14 | 16 | |
| 15 | 17 | # Bundle app source and set ownership |
| 16 | 18 | COPY --chown=node:node . ./ |
| 17 | 19 | |
| 18 | 20 | RUN \ |
| 19 | 21 | echo "*** Install npm packages ***" && \ |
| 20 | 22 | npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force |
| 21 | 23 | |
| 22 | 24 | # 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. | |
| 23 | 26 | RUN \ |
| 24 | 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" || true | |
| 29 | + chown -R node:node config data plugins public/scripts/extensions/third-party backups && \ | |
| 30 | + ln -s "./config/config.yaml" "config.yaml" | |
| 27 | 31 | |
| 28 | 32 | # Pre-compile public libraries |
| 29 | 33 | RUN \ |
| 30 | 34 | echo "*** Run Webpack ***" && \ |
| 31 | 35 | node "./docker/build-lib.js" |
| 32 | 36 | |
| 33 | 37 | # Set the entrypoint script and cleanup |
| 34 | 38 | RUN \ |
| 35 | 39 | echo "*** Cleanup ***" && \ |
| 36 | 40 | mv "./docker/docker-entrypoint.sh" "./" && \ |
| 37 | - rm -rf "./docker" && \ | |
| 38 | 41 | echo "*** Make docker-entrypoint.sh executable ***" && \ |
| 39 | 42 | chmod +x "./docker-entrypoint.sh" && \ |
| 40 | 43 | echo "*** Convert line endings to Unix format ***" && \ |
| 41 | 44 | dos2unix "./docker-entrypoint.sh" && \ |
| 45 | + rm -rf "./docker" | |
| 42 | 46 | |
| 43 | 47 | # Fix extension repos permissions |
| 44 | 48 | RUN git config --global --add safe.directory "*" |
| @@ -38,6 +38,9 @@ browserLaunch: | ||
| 38 | 38 | avoidLocalhost: false |
| 39 | 39 | # Server port |
| 40 | 40 | port: 8000 |
| 41 | +# Interval in seconds to write a heartbeat file. Set to 0 to disable. | |
| 42 | +# This is used primarily for Docker healthchecks. | |
| 43 | +heartbeatInterval: 0 | |
| 41 | 44 | # -- SSL options -- |
| 42 | 45 | ssl: |
| 43 | 46 | # Enable SSL/TLS encryption |
| @@ -68,6 +71,25 @@ basicAuthUser: | ||
| 68 | 71 | password: "password" |
| 69 | 72 | # Enables CORS proxy middleware |
| 70 | 73 | enableCorsProxy: false |
| 74 | +# CORS settings (applied to all routes) | |
| 75 | +cors: | |
| 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 | 93 | # -- REQUEST PROXY CONFIGURATION -- |
| 72 | 94 | requestProxy: |
| 73 | 95 | # If a proxy is enabled, all outgoing HTTP/HTTPS requests will be routed through it. |
| @@ -200,7 +222,6 @@ whitelistImportDomains: | ||
| 200 | 222 | - cdn.discordapp.com |
| 201 | 223 | - files.catbox.moe |
| 202 | 224 | - raw.githubusercontent.com |
| 203 | - - char-archive.evulid.cc | |
| 204 | 225 | # API request overrides (for KoboldAI and Text Completion APIs) |
| 205 | 226 | ## Note: host includes the port number if it's not the default (80 or 443) |
| 206 | 227 | ## Format is an array of objects: |
| @@ -265,7 +286,7 @@ ollama: | ||
| 265 | 286 | # -- ANTHROPIC CLAUDE API CONFIGURATION -- |
| 266 | 287 | claude: |
| 267 | 288 | # Enables caching of the system prompt (if supported). |
| 268 | 289 | # https://docsplatform.anthropicclaude.com/en/docs/en/build-with-claude/prompt-caching |
| 269 | 290 | # -- IMPORTANT! -- |
| 270 | 291 | # Use only when the prompt before the chat history is static and doesn't change between requests |
| 271 | 292 | # (e.g {{random}} macro or lorebooks not as in-chat injections). |
| @@ -287,6 +308,8 @@ claude: | ||
| 287 | 308 | gemini: |
| 288 | 309 | # API endpoint version ("v1beta" or "v1alpha") |
| 289 | 310 | apiVersion: 'v1beta' |
| 311 | + # Adds thought signatures to requests (if available). Only for Gemini 3 and above. | |
| 312 | + thoughtSignatures: true | |
| 290 | 313 | # Enables caching of the system prompt (if supported). Only for OpenRouter. |
| 291 | 314 | # -- IMPORTANT! -- |
| 292 | 315 | # Use only when the prompt before the chat history is static and doesn't change between requests |
| @@ -7,6 +7,7 @@ services: | ||
| 7 | 7 | environment: |
| 8 | 8 | - NODE_ENV=production |
| 9 | 9 | - FORCE_COLOR=1 |
| 10 | + - SILLYTAVERN_HEARTBEATINTERVAL=30 | |
| 10 | 11 | ports: |
| 11 | 12 | - "8000:8000" |
| 12 | 13 | volumes: |
| @@ -14,4 +15,10 @@ services: | ||
| 14 | 15 | - "./data:/home/node/app/data" |
| 15 | 16 | - "./plugins:/home/node/app/plugins" |
| 16 | 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 | 24 | restart: unless-stopped |
| @@ -1,12 +1,99 @@ | ||
| 1 | 1 | #!/bin/sh |
| 2 | 2 | |
| 3 | -if [ ! -e "config/config.yaml" ]; then | |
| 3 | +# Function to handle startup logic (Config check + Postinstall + Start) | |
| 4 | - echo "Resource not found, copying from defaults: config.yaml" | |
| 4 | +start_sillytavern() { | |
| 5 | - cp -r "default/config.yaml" "config/config.yaml" | |
| 5 | + local PREFIX="$1" | |
| 6 | -fi | |
| 6 | + shift # Remove the first argument (PREFIX) so $@ contains the rest | |
| 7 | + | |
| 8 | + # Config Check | |
| 9 | + if [ ! -e "config/config.yaml" ]; then | |
| 10 | + echo "Resource not found, copying from defaults: config.yaml" | |
| 11 | + $PREFIX cp "default/config.yaml" "config/config.yaml" | |
| 12 | + fi | |
| 13 | + | |
| 14 | + # Execute postinstall to auto-populate config.yaml with missing values | |
| 15 | + $PREFIX npm run postinstall | |
| 16 | + | |
| 17 | + # Start the server | |
| 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. | |
| 23 | +CORE_DIRS="config data plugins public/scripts/extensions/third-party backups" | |
| 24 | + | |
| 25 | +# Mounted Volumes (External) | |
| 26 | +# Parse mounts, handling files vs directories | |
| 27 | +RAW_MOUNTS=$(awk -v app_path="/home/node/app" '$2 ~ "^" app_path {print $2}' /proc/mounts) | |
| 28 | +MOUNTED_DIRS="" | |
| 29 | + | |
| 30 | +for 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 | |
| 43 | +done | |
| 44 | + | |
| 45 | +# Combine dirs for checks | |
| 46 | +CHECK_DIRS=$(echo "$CORE_DIRS $MOUNTED_DIRS" | tr ' ' '\n' | sort -u) | |
| 7 | 47 | |
| 8 | -# Execute postinstall to auto-populate config.yaml with missing values | |
| 48 | +# Ensure the needed directories exist | |
| 9 | -npm run postinstall | |
| 49 | +for 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 | |
| 54 | +done | |
| 55 | + | |
| 56 | +# Mode Selection | |
| 57 | +if [ "$(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 | + | |
| 90 | +else | |
| 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="" | |
| 96 | +fi | |
| 10 | 97 | |
| 11 | 98 | # StartCalling function with the serverdetermined prefix |
| 12 | -exec node server.js --listen "$@" | |
| 99 | +start_sillytavern "$EXEC_PREFIX" "$@" | |
| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "sillytavern", |
| 3 | 3 | "version": "1.1516.0", |
| 4 | 4 | "lockfileVersion": 3, |
| 5 | 5 | "requires": true, |
| 6 | 6 | "packages": { |
| 7 | 7 | "": { |
| 8 | 8 | "name": "sillytavern", |
| 9 | 9 | "version": "1.1516.0", |
| 10 | 10 | "hasInstallScript": true, |
| 11 | 11 | "license": "AGPL-3.0", |
| 12 | 12 | "dependencies": { |
| @@ -45,7 +45,7 @@ | ||
| 45 | 45 | "bowser": "^2.12.1", |
| 46 | 46 | "bytes": "^3.1.2", |
| 47 | 47 | "chalk": "^5.6.0", |
| 48 | 48 | "chevrotain": "^11.01.31", |
| 49 | 49 | "command-exists": "^1.2.9", |
| 50 | 50 | "compression": "^1.8.1", |
| 51 | 51 | "cookie-parser": "^1.4.6", |
| @@ -67,6 +67,7 @@ | ||
| 67 | 67 | "host-validation-middleware": "^0.1.1", |
| 68 | 68 | "html-entities": "^2.6.0", |
| 69 | 69 | "iconv-lite": "^0.6.3", |
| 70 | + "image-size": "^2.0.2", | |
| 70 | 71 | "ip-matching": "^2.1.2", |
| 71 | 72 | "ip-regex": "^5.0.0", |
| 72 | 73 | "ipaddr.js": "^2.2.0", |
| @@ -174,42 +175,42 @@ | ||
| 174 | 175 | "license": "Apache-2.0" |
| 175 | 176 | }, |
| 176 | 177 | "node_modules/@chevrotain/cst-dts-gen": { |
| 177 | 178 | "version": "11.1.01", |
| 178 | 179 | "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.01.tgz", |
| 179 | 180 | "integrity": "sha512-SafRHyv6/G9XD23V4StfHMeQNnXbFmj8CsYUBmf+L895f542qQqiRGalrfJl/hKm0RFDhxAfHzV6e58NA8j5ninntT5yqMzBW8QEbYxLkNUwevD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==", |
| 180 | 181 | "license": "Apache-2.0", |
| 181 | 182 | "dependencies": { |
| 182 | 183 | "@chevrotain/gast": "11.1.01", |
| 183 | 184 | "@chevrotain/types": "11.1.01", |
| 184 | 185 | "lodash-es": "4.17.2123" |
| 185 | 186 | } |
| 186 | 187 | }, |
| 187 | 188 | "node_modules/@chevrotain/gast": { |
| 188 | 189 | "version": "11.1.01", |
| 189 | 190 | "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.01.tgz", |
| 190 | 191 | "integrity": "sha512-0fyRYDFneUhbyV6k22R6bBY02Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+FasLqcxXYVt8z51IWTZ10l2Z2Lc0hiPTgm8MNRbYZnDbNv78b9zY5DoIJKjQdfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==", |
| 191 | 192 | "license": "Apache-2.0", |
| 192 | 193 | "dependencies": { |
| 193 | 194 | "@chevrotain/types": "11.1.01", |
| 194 | 195 | "lodash-es": "4.17.2123" |
| 195 | 196 | } |
| 196 | 197 | }, |
| 197 | 198 | "node_modules/@chevrotain/regexp-to-ast": { |
| 198 | 199 | "version": "11.1.01", |
| 199 | 200 | "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.01.tgz", |
| 200 | 201 | "integrity": "sha512-3rW046uSp36liIAc/5G6A6h3gGbDN1eONpmJQpybIbctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+G2kSz0BNRc9ziT4DYrCUUbgNLd6bNVROqN9r7ZaajYg82C2uylg/TEwFRgwLmbhlln4qkmDyteg==", |
| 201 | 202 | "license": "Apache-2.0" |
| 202 | 203 | }, |
| 203 | 204 | "node_modules/@chevrotain/types": { |
| 204 | 205 | "version": "11.1.01", |
| 205 | 206 | "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.01.tgz", |
| 206 | 207 | "integrity": "sha512-GXni/dwJAkClMfwCtrbGU19RXQ9O76hFxq3sgy/zufXNj3ov6J/8FOWIXxJLhnKx7gzSweATmRccjlpmr5W2nAwb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==", |
| 207 | 208 | "license": "Apache-2.0" |
| 208 | 209 | }, |
| 209 | 210 | "node_modules/@chevrotain/utils": { |
| 210 | 211 | "version": "11.1.01", |
| 211 | 212 | "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.01.tgz", |
| 212 | 213 | "integrity": "sha512-DrS2yldzFnjmBV0O/kDngcFxWuqg2FdmUpaD6KyTmgIIE6lR53dq80R71eTYMzYXYSFPrbg/ZzZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+o6LpUrXsLJk192kXuaeIPic4WVgFE1TVQ==", |
| 213 | 214 | "license": "Apache-2.0" |
| 214 | 215 | }, |
| 215 | 216 | "node_modules/@es-joy/jsdoccomment": { |
| @@ -1474,17 +1475,13 @@ | ||
| 1474 | 1475 | } |
| 1475 | 1476 | }, |
| 1476 | 1477 | "node_modules/@jridgewell/gen-mapping": { |
| 1477 | 1478 | "version": "0.3.813", |
| 1478 | 1479 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.813.tgz", |
| 1479 | 1480 | "integrity": "sha512-imAbBGkb2kkt/7niJ6MgEPxF0bYdQ6etZaA+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWAfQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", |
| 1480 | 1481 | "license": "MIT", |
| 1481 | 1482 | "dependencies": { |
| 1482 | 1483 | "@jridgewell/setsourcemap-arraycodec": "^1.25.10", |
| 1483 | - "@jridgewell/sourcemap-codec": "^1.4.10", | |
| 1484 | 1484 | "@jridgewell/trace-mapping": "^0.3.24" |
| 1485 | - }, | |
| 1486 | - "engines": { | |
| 1487 | - "node": ">=6.0.0" | |
| 1488 | 1485 | } |
| 1489 | 1486 | }, |
| 1490 | 1487 | "node_modules/@jridgewell/resolve-uri": { |
| @@ -1496,19 +1493,10 @@ | ||
| 1496 | 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 | 1496 | "node_modules/@jridgewell/source-map": { |
| 1509 | 1497 | "version": "0.3.611", |
| 1510 | 1498 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.611.tgz", |
| 1511 | 1499 | "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGudZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWEA0G8V/tt+shMQXWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", |
| 1512 | 1500 | "license": "MIT", |
| 1513 | 1501 | "dependencies": { |
| 1514 | 1502 | "@jridgewell/gen-mapping": "^0.3.5", |
| @@ -1516,15 +1504,15 @@ | ||
| 1516 | 1504 | } |
| 1517 | 1505 | }, |
| 1518 | 1506 | "node_modules/@jridgewell/sourcemap-codec": { |
| 1519 | 1507 | "version": "1.5.05", |
| 1520 | 1508 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.05.tgz", |
| 1521 | 1509 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgfcYQ9310grqxueWbl+PwPaM7GQWuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", |
| 1522 | 1510 | "license": "MIT" |
| 1523 | 1511 | }, |
| 1524 | 1512 | "node_modules/@jridgewell/trace-mapping": { |
| 1525 | 1513 | "version": "0.3.2531", |
| 1526 | 1514 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.2531.tgz", |
| 1527 | 1515 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTFzzNR+8Lb57DwOb3Aa0o9CApepiYQSdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", |
| 1528 | 1516 | "license": "MIT", |
| 1529 | 1517 | "dependencies": { |
| 1530 | 1518 | "@jridgewell/resolve-uri": "^3.1.0", |
| @@ -1921,9 +1909,9 @@ | ||
| 1921 | 1909 | } |
| 1922 | 1910 | }, |
| 1923 | 1911 | "node_modules/@types/estree": { |
| 1924 | 1912 | "version": "1.0.68", |
| 1925 | 1913 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.68.tgz", |
| 1926 | 1914 | "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cEdWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+gHpnPyXjHWxcwJuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", |
| 1927 | 1915 | "license": "MIT" |
| 1928 | 1916 | }, |
| 1929 | 1917 | "node_modules/@types/express": { |
| @@ -2618,9 +2606,9 @@ | ||
| 2618 | 2606 | } |
| 2619 | 2607 | }, |
| 2620 | 2608 | "node_modules/acorn": { |
| 2621 | 2609 | "version": "8.1415.0", |
| 2622 | 2610 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1415.0.tgz", |
| 2623 | 2611 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7KNZyJarBfL7nWwIq+t0cXIrH5siy5S4XkFycAFDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", |
| 2624 | 2612 | "license": "MIT", |
| 2625 | 2613 | "peer": true, |
| 2626 | 2614 | "bin": { |
| @@ -2630,6 +2618,18 @@ | ||
| 2630 | 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 | 2633 | "node_modules/acorn-jsx": { |
| 2634 | 2634 | "version": "5.3.2", |
| 2635 | 2635 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", |
| @@ -2998,13 +2998,13 @@ | ||
| 2998 | 2998 | } |
| 2999 | 2999 | }, |
| 3000 | 3000 | "node_modules/axios": { |
| 3001 | 3001 | "version": "1.1213.05", |
| 3002 | 3002 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.1213.05.tgz", |
| 3003 | 3003 | "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzVcz4ur7Vb0xS4/SPDEhMB3QKUN0tPWe44eqxrIu31me+DSurgfbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", |
| 3004 | 3004 | "license": "MIT", |
| 3005 | 3005 | "dependencies": { |
| 3006 | 3006 | "follow-redirects": "^1.15.611", |
| 3007 | 3007 | "form-data": "^4.0.45", |
| 3008 | 3008 | "proxy-from-env": "^1.1.0" |
| 3009 | 3009 | } |
| 3010 | 3010 | }, |
| @@ -3050,6 +3050,15 @@ | ||
| 3050 | 3050 | ], |
| 3051 | 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 | 3062 | "node_modules/basic-ftp": { |
| 3054 | 3063 | "version": "5.0.5", |
| 3055 | 3064 | "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", |
| @@ -3182,9 +3191,9 @@ | ||
| 3182 | 3191 | } |
| 3183 | 3192 | }, |
| 3184 | 3193 | "node_modules/browserslist": { |
| 3185 | 3194 | "version": "4.2428.01", |
| 3186 | 3195 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.2428.01.tgz", |
| 3187 | 3196 | "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIWZC5Bd0LgJXgwGqUknZY/cLM5HSJvkUQ04r8NXnJZ3yYi4vDmSiZmC/9k884ApdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", |
| 3188 | 3197 | "funding": [ |
| 3189 | 3198 | { |
| 3190 | 3199 | "type": "opencollective", |
| @@ -3202,10 +3211,11 @@ | ||
| 3202 | 3211 | "license": "MIT", |
| 3203 | 3212 | "peer": true, |
| 3204 | 3213 | "dependencies": { |
| 3205 | 3214 | "caniusebaseline-litebrowser-mapping": "^12.09.300016630", |
| 3206 | 3215 | "electron-tocaniuse-chromiumlite": "^1.50.2830001759", |
| 3207 | 3216 | "nodeelectron-releasesto-chromium": "^21.05.18263", |
| 3208 | 3217 | "update-browserslistnode-dbreleases": "^1.12.0.27", |
| 3218 | + "update-browserslist-db": "^1.2.0" | |
| 3209 | 3219 | }, |
| 3210 | 3220 | "bin": { |
| 3211 | 3221 | "browserslist": "cli.js" |
| @@ -3364,9 +3374,9 @@ | ||
| 3364 | 3374 | } |
| 3365 | 3375 | }, |
| 3366 | 3376 | "node_modules/caniuse-lite": { |
| 3367 | 3377 | "version": "1.0.3000166930001768", |
| 3368 | 3378 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.3000166930001768.tgz", |
| 3369 | 3379 | "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4qY3aDRZC5nWPgHUgIB84WL+9HrLaYei4w8BIAL7IBnySuo19wk0VJpp/UEDu889d8vhCTPA0wXI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", |
| 3370 | 3380 | "funding": [ |
| 3371 | 3381 | { |
| 3372 | 3382 | "type": "opencollective", |
| @@ -3452,17 +3462,17 @@ | ||
| 3452 | 3462 | } |
| 3453 | 3463 | }, |
| 3454 | 3464 | "node_modules/chevrotain": { |
| 3455 | 3465 | "version": "11.1.01", |
| 3456 | 3466 | "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.01.tgz", |
| 3457 | 3467 | "integrity": "sha512-BqwSf3RDQlHQ+EyWqTLDd23IwJ3clav6QyNQM4FNj0RF2f0yv5CPKaFxfsPTBzX7vGuim4oIC1/HfXESPjrApKkEstV5jbyJtUB8U4zrUFdLd2Cx1oAgcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", |
| 3458 | 3468 | "license": "Apache-2.0", |
| 3459 | 3469 | "dependencies": { |
| 3460 | 3470 | "@chevrotain/cst-dts-gen": "11.1.01", |
| 3461 | 3471 | "@chevrotain/gast": "11.1.01", |
| 3462 | 3472 | "@chevrotain/regexp-to-ast": "11.1.01", |
| 3463 | 3473 | "@chevrotain/types": "11.1.01", |
| 3464 | 3474 | "@chevrotain/utils": "11.1.01", |
| 3465 | 3475 | "lodash-es": "4.17.2123" |
| 3466 | 3476 | } |
| 3467 | 3477 | }, |
| 3468 | 3478 | "node_modules/chrome-trace-event": { |
| @@ -4311,9 +4321,9 @@ | ||
| 4311 | 4321 | "license": "MIT" |
| 4312 | 4322 | }, |
| 4313 | 4323 | "node_modules/electron-to-chromium": { |
| 4314 | 4324 | "version": "1.5.39286", |
| 4315 | 4325 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39286.tgz", |
| 4316 | 4326 | "integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+6bhLThdB+plgMeou98CAaHu/GrgWATj2iHOOHTp1hWtABj2A==", |
| 4317 | 4327 | "license": "ISC" |
| 4318 | 4328 | }, |
| 4319 | 4329 | "node_modules/emoji-regex": { |
| @@ -4341,13 +4351,13 @@ | ||
| 4341 | 4351 | } |
| 4342 | 4352 | }, |
| 4343 | 4353 | "node_modules/enhanced-resolve": { |
| 4344 | 4354 | "version": "5.1719.10", |
| 4345 | 4355 | "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.1719.10.tgz", |
| 4346 | 4356 | "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQphv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwygAbf4g187lUUAvH+H26omrqia2aGg==", |
| 4347 | 4357 | "license": "MIT", |
| 4348 | 4358 | "dependencies": { |
| 4349 | 4359 | "graceful-fs": "^4.2.4", |
| 4350 | 4360 | "tapable": "^2.23.0" |
| 4351 | 4361 | }, |
| 4352 | 4362 | "engines": { |
| 4353 | 4363 | "node": ">=10.13.0" |
| @@ -4402,6 +4412,7 @@ | ||
| 4402 | 4412 | "version": "1.5.4", |
| 4403 | 4413 | "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", |
| 4404 | 4414 | "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", |
| 4415 | + "dev": true, | |
| 4405 | 4416 | "license": "MIT" |
| 4406 | 4417 | }, |
| 4407 | 4418 | "node_modules/es-object-atoms": { |
| @@ -4964,9 +4975,9 @@ | ||
| 4964 | 4975 | "license": "MIT" |
| 4965 | 4976 | }, |
| 4966 | 4977 | "node_modules/fast-uri": { |
| 4967 | 4978 | "version": "3.01.60", |
| 4968 | 4979 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.01.60.tgz", |
| 4969 | 4980 | "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHwiPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", |
| 4970 | 4981 | "funding": [ |
| 4971 | 4982 | { |
| 4972 | 4983 | "type": "github", |
| @@ -5119,15 +5130,16 @@ | ||
| 5119 | 5130 | "license": "ISC" |
| 5120 | 5131 | }, |
| 5121 | 5132 | "node_modules/follow-redirects": { |
| 5122 | 5133 | "version": "1.15.611", |
| 5123 | 5134 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.611.tgz", |
| 5124 | 5135 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpWdeG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", |
| 5125 | 5136 | "funding": [ |
| 5126 | 5137 | { |
| 5127 | 5138 | "type": "individual", |
| 5128 | 5139 | "url": "https://github.com/sponsors/RubenVerborgh" |
| 5129 | 5140 | } |
| 5130 | 5141 | ], |
| 5142 | + "license": "MIT", | |
| 5131 | 5143 | "engines": { |
| 5132 | 5144 | "node": ">=4.0" |
| 5133 | 5145 | }, |
| @@ -5153,9 +5165,9 @@ | ||
| 5153 | 5165 | } |
| 5154 | 5166 | }, |
| 5155 | 5167 | "node_modules/form-data": { |
| 5156 | 5168 | "version": "4.0.45", |
| 5157 | 5169 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.45.tgz", |
| 5158 | 5170 | "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+4IlGTMF0OwwuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", |
| 5159 | 5171 | "license": "MIT", |
| 5160 | 5172 | "dependencies": { |
| 5161 | 5173 | "asynckit": "^0.4.0", |
| @@ -5836,6 +5848,18 @@ | ||
| 5836 | 5848 | "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", |
| 5837 | 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 | 5863 | "node_modules/immediate": { |
| 5840 | 5864 | "version": "3.0.6", |
| 5841 | 5865 | "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", |
| @@ -6338,12 +6362,16 @@ | ||
| 6338 | 6362 | } |
| 6339 | 6363 | }, |
| 6340 | 6364 | "node_modules/loader-runner": { |
| 6341 | 6365 | "version": "4.3.01", |
| 6342 | 6366 | "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.01.tgz", |
| 6343 | 6367 | "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJVIWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/oGJXo8qCatFGTfDbY6W6ipGOYXfgKjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", |
| 6344 | 6368 | "license": "MIT", |
| 6345 | 6369 | "engines": { |
| 6346 | 6370 | "node": ">=6.11.5" |
| 6371 | + }, | |
| 6372 | + "funding": { | |
| 6373 | + "type": "opencollective", | |
| 6374 | + "url": "https://opencollective.com/webpack" | |
| 6347 | 6375 | } |
| 6348 | 6376 | }, |
| 6349 | 6377 | "node_modules/localforage": { |
| @@ -6732,10 +6760,37 @@ | ||
| 6732 | 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 | 6790 | "node_modules/node-releases": { |
| 6736 | 6791 | "version": "2.0.1827", |
| 6737 | 6792 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1827.tgz", |
| 6738 | 6793 | "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpLnmh3lCkYZ3grZvqcCH+eWPooLIfjmQ7X+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvImH0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+tIoVOdodFS40gNDLCwm2iorIlA==", |
| 6739 | 6794 | "license": "MIT" |
| 6740 | 6795 | }, |
| 6741 | 6796 | "node_modules/normalize-path": { |
| @@ -6941,10 +6996,27 @@ | ||
| 6941 | 6996 | "node": ">=8" |
| 6942 | 6997 | } |
| 6943 | 6998 | }, |
| 6944 | 6999 | "node_modules/p-limitlocate": { |
| 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 | 7016 | "version": "3.1.0", |
| 6946 | 7017 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", |
| 6947 | 7018 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", |
| 7019 | + "dev": true, | |
| 6948 | 7020 | "license": "MIT", |
| 6949 | 7021 | "dependencies": { |
| 6950 | 7022 | "yocto-queue": "^0.1.0" |
| @@ -6956,15 +7028,12 @@ | ||
| 6956 | 7028 | "url": "https://github.com/sponsors/sindresorhus" |
| 6957 | 7029 | } |
| 6958 | 7030 | }, |
| 6959 | 7031 | "node_modules/p-locate/node_modules/yocto-queue": { |
| 6960 | 7032 | "version": "5.0.1.0", |
| 6961 | 7033 | "resolved": "https://registry.npmjs.org/pyocto-locatequeue/-/pyocto-locatequeue-5.0.1.0.tgz", |
| 6962 | 7034 | "integrity": "sha512-LaNjtRWUBY++zB5nErVksvsnNCdJ/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7XohGc6xgPwyN8eheCxsiLM8mxuE/tlt/QYq3TIeE6nxHppbo2LGymrG5PwmOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", |
| 6963 | 7035 | "dev": true, |
| 6964 | 7036 | "license": "MIT", |
| 6965 | - "dependencies": { | |
| 6966 | - "p-limit": "^3.0.2" | |
| 6967 | - }, | |
| 6968 | 7037 | "engines": { |
| 6969 | 7038 | "node": ">=10" |
| 6970 | 7039 | }, |
| @@ -7221,9 +7290,9 @@ | ||
| 7221 | 7290 | } |
| 7222 | 7291 | }, |
| 7223 | 7292 | "node_modules/picocolors": { |
| 7224 | 7293 | "version": "1.1.01", |
| 7225 | 7294 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.01.tgz", |
| 7226 | 7295 | "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzyxceH2snhtb5M9liqDsmEw56le376mTZkEX/kWr8lkdjEb/hp3mTg7wYK7zJhuBStmGMBG0BdeDZSRxNFyegNul7eNslCXP9FDj/dZx1IukaX6Bk11zcln25o1AwLcu0X8KEyMceP2ntpaHrDEVA==", |
| 7227 | 7296 | "license": "ISC" |
| 7228 | 7297 | }, |
| 7229 | 7298 | "node_modules/picomatch": { |
| @@ -7419,9 +7488,9 @@ | ||
| 7419 | 7488 | } |
| 7420 | 7489 | }, |
| 7421 | 7490 | "node_modules/qs": { |
| 7422 | 7491 | "version": "6.14.12", |
| 7423 | 7492 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.12.tgz", |
| 7424 | 7493 | "integrity": "sha512-4EK3+xJl8Ts67nLYNwqwV/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQyCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", |
| 7425 | 7494 | "license": "BSD-3-Clause", |
| 7426 | 7495 | "dependencies": { |
| 7427 | 7496 | "side-channel": "^1.1.0" |
| @@ -7811,9 +7880,9 @@ | ||
| 7811 | 7880 | "license": "ISC" |
| 7812 | 7881 | }, |
| 7813 | 7882 | "node_modules/schema-utils": { |
| 7814 | 7883 | "version": "4.3.23", |
| 7815 | 7884 | "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.23.tgz", |
| 7816 | 7885 | "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19ueflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQOUYdb48v4k4WWHQurA==", |
| 7817 | 7886 | "license": "MIT", |
| 7818 | 7887 | "dependencies": { |
| 7819 | 7888 | "@types/json-schema": "^7.0.9", |
| @@ -8447,12 +8516,16 @@ | ||
| 8447 | 8516 | } |
| 8448 | 8517 | }, |
| 8449 | 8518 | "node_modules/tapable": { |
| 8450 | 8519 | "version": "2.23.10", |
| 8451 | 8520 | "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.23.10.tgz", |
| 8452 | 8521 | "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCpg9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+kqaQQaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", |
| 8453 | 8522 | "license": "MIT", |
| 8454 | 8523 | "engines": { |
| 8455 | 8524 | "node": ">=6" |
| 8525 | + }, | |
| 8526 | + "funding": { | |
| 8527 | + "type": "opencollective", | |
| 8528 | + "url": "https://opencollective.com/webpack" | |
| 8456 | 8529 | } |
| 8457 | 8530 | }, |
| 8458 | 8531 | "node_modules/tar-stream": { |
| @@ -8466,13 +8539,13 @@ | ||
| 8466 | 8539 | } |
| 8467 | 8540 | }, |
| 8468 | 8541 | "node_modules/terser": { |
| 8469 | 8542 | "version": "5.3946.0", |
| 8470 | 8543 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.3946.0.tgz", |
| 8471 | 8544 | "integrity": "sha512-LBAhFyLho16harJoWMgjTwoImyr/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWwQbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", |
| 8472 | 8545 | "license": "BSD-2-Clause", |
| 8473 | 8546 | "dependencies": { |
| 8474 | 8547 | "@jridgewell/source-map": "^0.3.3", |
| 8475 | 8548 | "acorn": "^8.815.20", |
| 8476 | 8549 | "commander": "^2.20.0", |
| 8477 | 8550 | "source-map-support": "~0.5.20" |
| 8478 | 8551 | }, |
| @@ -8484,9 +8557,9 @@ | ||
| 8484 | 8557 | } |
| 8485 | 8558 | }, |
| 8486 | 8559 | "node_modules/terser-webpack-plugin": { |
| 8487 | 8560 | "version": "5.3.1216", |
| 8488 | 8561 | "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1216.tgz", |
| 8489 | 8562 | "integrity": "sha512-jDLYqo7oF8tJIttjXO6jBY5Hk8p3A8W4ttih7cCEq64fQFWmgJ4VqAQjKr7WwIDlmXKEc6QeoRb5ecjZh9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+2afcgCsUZYmYEIZ3mR+Q==", |
| 8490 | 8563 | "license": "MIT", |
| 8491 | 8564 | "dependencies": { |
| 8492 | 8565 | "@jridgewell/trace-mapping": "^0.3.25", |
| @@ -8740,9 +8813,9 @@ | ||
| 8740 | 8813 | } |
| 8741 | 8814 | }, |
| 8742 | 8815 | "node_modules/update-browserslist-db": { |
| 8743 | 8816 | "version": "1.12.13", |
| 8744 | 8817 | "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.12.13.tgz", |
| 8745 | 8818 | "integrity": "sha512-R8UzCaa9AzJs0m9cx+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5qOgDxo0eMiFGEueWztz+lo5r94l29Ad4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", |
| 8746 | 8819 | "funding": [ |
| 8747 | 8820 | { |
| 8748 | 8821 | "type": "opencollective", |
| @@ -8760,7 +8833,7 @@ | ||
| 8760 | 8833 | "license": "MIT", |
| 8761 | 8834 | "dependencies": { |
| 8762 | 8835 | "escalade": "^3.2.0", |
| 8763 | 8836 | "picocolors": "^1.1.01" |
| 8764 | 8837 | }, |
| 8765 | 8838 | "bin": { |
| 8766 | 8839 | "update-browserslist-db": "cli.js" |
| @@ -8865,9 +8938,9 @@ | ||
| 8865 | 8938 | "license": "Apache-2.0" |
| 8866 | 8939 | }, |
| 8867 | 8940 | "node_modules/watchpack": { |
| 8868 | 8941 | "version": "2.45.21", |
| 8869 | 8942 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.45.21.tgz", |
| 8870 | 8943 | "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJwZn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", |
| 8871 | 8944 | "license": "MIT", |
| 8872 | 8945 | "dependencies": { |
| 8873 | 8946 | "glob-to-regexp": "^0.4.1", |
| @@ -8908,34 +8981,36 @@ | ||
| 8908 | 8981 | } |
| 8909 | 8982 | }, |
| 8910 | 8983 | "node_modules/webpack": { |
| 8911 | 8984 | "version": "5.98105.0", |
| 8912 | 8985 | "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98105.0.tgz", |
| 8913 | 8986 | "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJgX/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXAdMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", |
| 8914 | 8987 | "license": "MIT", |
| 8915 | 8988 | "dependencies": { |
| 8916 | 8989 | "@types/eslint-scope": "^3.7.7", |
| 8917 | 8990 | "@types/estree": "^1.0.68", |
| 8991 | + "@types/json-schema": "^7.0.15", | |
| 8918 | 8992 | "@webassemblyjs/ast": "^1.14.1", |
| 8919 | 8993 | "@webassemblyjs/wasm-edit": "^1.14.1", |
| 8920 | 8994 | "@webassemblyjs/wasm-parser": "^1.14.1", |
| 8921 | 8995 | "acorn": "^8.1415.0", |
| 8922 | 8996 | "browserslistacorn-import-phases": "^4.241.0.3", |
| 8997 | + "browserslist": "^4.28.1", | |
| 8923 | 8998 | "chrome-trace-event": "^1.0.2", |
| 8924 | 8999 | "enhanced-resolve": "^5.1719.10", |
| 8925 | 9000 | "es-module-lexer": "^1.2.10.0", |
| 8926 | 9001 | "eslint-scope": "5.1.1", |
| 8927 | 9002 | "events": "^3.2.0", |
| 8928 | 9003 | "glob-to-regexp": "^0.4.1", |
| 8929 | 9004 | "graceful-fs": "^4.2.11", |
| 8930 | 9005 | "json-parse-even-better-errors": "^2.3.1", |
| 8931 | 9006 | "loader-runner": "^4.23.01", |
| 8932 | 9007 | "mime-types": "^2.1.27", |
| 8933 | 9008 | "neo-async": "^2.6.2", |
| 8934 | 9009 | "schema-utils": "^4.3.03", |
| 8935 | 9010 | "tapable": "^2.13.10", |
| 8936 | 9011 | "terser-webpack-plugin": "^5.3.1116", |
| 8937 | 9012 | "watchpack": "^2.45.1", |
| 8938 | 9013 | "webpack-sources": "^3.23.3" |
| 8939 | 9014 | }, |
| 8940 | 9015 | "bin": { |
| 8941 | 9016 | "webpack": "bin/webpack.js" |
| @@ -8954,14 +9029,20 @@ | ||
| 8954 | 9029 | } |
| 8955 | 9030 | }, |
| 8956 | 9031 | "node_modules/webpack-sources": { |
| 8957 | 9032 | "version": "3.23.3", |
| 8958 | 9033 | "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.23.3.tgz", |
| 8959 | 9034 | "integrity": "sha512-/DyMEOrDgLKKIG0fmvtzyd1RBzSGanHkitROoPFd6qsrxt+4dUXoFhg/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", |
| 8960 | 9035 | "license": "MIT", |
| 8961 | 9036 | "engines": { |
| 8962 | 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 | 9046 | "node_modules/webpack/node_modules/eslint-scope": { |
| 8966 | 9047 | "version": "5.1.1", |
| 8967 | 9048 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", |
| @@ -9236,18 +9317,6 @@ | ||
| 9236 | 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 | 9320 | "node_modules/zip-stream": { |
| 9252 | 9321 | "version": "6.0.1", |
| 9253 | 9322 | "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", |
| @@ -35,7 +35,7 @@ | ||
| 35 | 35 | "bowser": "^2.12.1", |
| 36 | 36 | "bytes": "^3.1.2", |
| 37 | 37 | "chalk": "^5.6.0", |
| 38 | 38 | "chevrotain": "^11.01.31", |
| 39 | 39 | "command-exists": "^1.2.9", |
| 40 | 40 | "compression": "^1.8.1", |
| 41 | 41 | "cookie-parser": "^1.4.6", |
| @@ -57,6 +57,7 @@ | ||
| 57 | 57 | "host-validation-middleware": "^0.1.1", |
| 58 | 58 | "html-entities": "^2.6.0", |
| 59 | 59 | "iconv-lite": "^0.6.3", |
| 60 | + "image-size": "^2.0.2", | |
| 60 | 61 | "ip-matching": "^2.1.2", |
| 61 | 62 | "ip-regex": "^5.0.0", |
| 62 | 63 | "ipaddr.js": "^2.2.0", |
| @@ -99,14 +100,8 @@ | ||
| 99 | 100 | "vectra": { |
| 100 | 101 | "openai": "^4.17.0" |
| 101 | 102 | }, |
| 102 | - "axios": { | |
| 103 | - "follow-redirects": "^1.15.4" | |
| 104 | - }, | |
| 105 | 103 | "node-fetch": { |
| 106 | 104 | "whatwg-url": "^14.0.0" |
| 107 | - }, | |
| 108 | - "chevrotain": { | |
| 109 | - "lodash-es": "^4.17.23" | |
| 110 | 105 | } |
| 111 | 106 | }, |
| 112 | 107 | "name": "sillytavern", |
| @@ -116,7 +111,7 @@ | ||
| 116 | 111 | "type": "git", |
| 117 | 112 | "url": "https://github.com/SillyTavern/SillyTavern.git" |
| 118 | 113 | }, |
| 119 | 114 | "version": "1.1516.0", |
| 120 | 115 | "scripts": { |
| 121 | 116 | "start": "node server.js", |
| 122 | 117 | "debug": "node --inspect server.js", |
| @@ -96,6 +96,12 @@ | ||
| 96 | 96 | font-size: calc(var(--mainFontSize) * 0.95); |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | +#bg-sort { | |
| 100 | + width: auto; | |
| 101 | + max-width: 6em; | |
| 102 | + flex-shrink: 0; | |
| 103 | +} | |
| 104 | + | |
| 99 | 105 | /* Thumbnails */ |
| 100 | 106 | .bg_example:hover .BGSampleTitle { |
| 101 | 107 | opacity: 1; |
| @@ -39,7 +39,7 @@ label[for="extensions_autoconnect"] { | ||
| 39 | 39 | text-align: left; |
| 40 | 40 | } |
| 41 | 41 | |
| 42 | 42 | .extensions_info h3:not(.margin0) { |
| 43 | 43 | margin-bottom: 0.5em; |
| 44 | 44 | } |
| 45 | 45 | |
| @@ -112,6 +112,10 @@ label[for="extensions_autoconnect"] { | ||
| 112 | 112 | color: limegreen; |
| 113 | 113 | } |
| 114 | 114 | |
| 115 | +.extensions_info .third_party_toolbar { | |
| 116 | + user-select: none; | |
| 117 | +} | |
| 118 | + | |
| 115 | 119 | input.extension_missing[type="checkbox"] { |
| 116 | 120 | opacity: 0.5; |
| 117 | 121 | } |
| @@ -157,4 +161,4 @@ input.extension_missing[type="checkbox"] { | ||
| 157 | 161 | z-index: 1; |
| 158 | 162 | margin-bottom: 10px; |
| 159 | 163 | padding: 5px; |
| 160 | 164 | } |
| 164 | \ No newline at end of file | |
| @@ -465,6 +465,89 @@ | ||
| 465 | 465 | color: #F89406; |
| 466 | 466 | } |
| 467 | 467 | |
| 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 | 551 | /* Current argument hint banner in details */ |
| 469 | 552 | .macro-ac-arg-hint { |
| 470 | 553 | display: flex; |
| @@ -483,6 +566,11 @@ | ||
| 483 | 566 | font-size: 0.8em; |
| 484 | 567 | } |
| 485 | 568 | |
| 569 | +.macro-ac-arg-hint .macro-ac-arg-hint-small { | |
| 570 | + font-size: 0.85em; | |
| 571 | + opacity: 0.8; | |
| 572 | +} | |
| 573 | + | |
| 486 | 574 | .macro-ac-hint-type { |
| 487 | 575 | font-family: var(--monoFontFamily); |
| 488 | 576 | font-size: 0.85em; |
| @@ -67,6 +67,10 @@ | ||
| 67 | 67 | display: none; |
| 68 | 68 | } |
| 69 | 69 | |
| 70 | +.tag.tag-absent { | |
| 71 | + text-decoration: line-through; | |
| 72 | +} | |
| 73 | + | |
| 70 | 74 | .tag.actionable { |
| 71 | 75 | border-radius: 50%; |
| 72 | 76 | aspect-ratio: 1 / 1; |
| @@ -564,3 +564,7 @@ label[for="bind_preset_to_connection"]:has(input:checked) { | ||
| 564 | 564 | #request_images_block:has(#openai_request_images:not(:checked)) #request_images_settings { |
| 565 | 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 | +} | |
| @@ -115,6 +115,7 @@ body.hideChatAvatars .welcomePanel .recentChatList .recentChat .avatar { | ||
| 115 | 115 | cursor: pointer; |
| 116 | 116 | gap: 10px; |
| 117 | 117 | border: 1px solid var(--SmartThemeBorderColor); |
| 118 | + position: relative; | |
| 118 | 119 | } |
| 119 | 120 | |
| 120 | 121 | .welcomeRecent .recentChatList .recentChat .avatar { |
| @@ -222,6 +223,14 @@ body.big-avatars .welcomeRecent .recentChatList .recentChat .chatMessageContaine | ||
| 222 | 223 | transform: rotate(180deg); |
| 223 | 224 | } |
| 224 | 225 | |
| 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 | 234 | @media screen and (max-width: 1000px) { |
| 226 | 235 | .welcomePanel .welcomeShortcuts a span { |
| 227 | 236 | display: none; |
| @@ -38,6 +38,7 @@ declare global { | ||
| 38 | 38 | avatar_url?: string; |
| 39 | 39 | hideMutedSprites?: boolean; |
| 40 | 40 | fav?: boolean; |
| 41 | + date_last_chat?: MessageTimestamp; | |
| 41 | 42 | } |
| 42 | 43 | |
| 43 | 44 | interface ChatFile extends Array<ChatMessage> { |
| @@ -235,3 +236,11 @@ declare global { | ||
| 235 | 236 | |
| 236 | 237 | type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>; |
| 237 | 238 | } |
| 239 | + | |
| 240 | +//Overrides for public/scripts/chats.js | |
| 241 | +declare module 'dompurify' { | |
| 242 | + interface Config { | |
| 243 | + MESSAGE_SANITIZE?: boolean; | |
| 244 | + MESSAGE_ALLOW_SYSTEM_UI?: boolean; | |
| 245 | + } | |
| 246 | +} | |
| @@ -1399,6 +1399,28 @@ | ||
| 1399 | 1399 | <input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" /> |
| 1400 | 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 | 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 | 1424 | <div data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" data-tg-samplers="smoothing_factor" id="smoothingBlock" name="smoothingBlock" class="wide100p"> |
| 1403 | 1425 | <h4 class="wide100p textAlignCenter"> |
| 1404 | 1426 | <label data-i18n="Smooth Sampling">Smooth Sampling</label> |
| @@ -1974,7 +1996,7 @@ | ||
| 1974 | 1996 | </b> |
| 1975 | 1997 | </div> |
| 1976 | 1998 | </div> |
| 1977 | 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 | 2000 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> |
| 1979 | 2001 | <input id="openai_function_calling" type="checkbox" /> |
| 1980 | 2002 | <span data-i18n="Enable function calling">Enable function calling</span> |
| @@ -2002,7 +2024,7 @@ | ||
| 2002 | 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 | 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 | 2026 | </div> |
| 2005 | 2027 | <div id="openai_audio_inlining_supported" data-source="makersuite,vertexai,openrouter,openai,custom"> |
| 2006 | 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 | 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 | 2030 | </div> |
| @@ -2083,12 +2105,12 @@ | ||
| 2083 | 2105 | <span data-i18n="Allows the model to return its thinking process."> |
| 2084 | 2106 | Allows the model to return its thinking process. |
| 2085 | 2107 | </span> |
| 2086 | 2108 | <strong data-i18n="This setting affects visibility only." data-source-mode="except" data-source="zai,moonshot"> |
| 2087 | 2109 | This setting affects visibility only. |
| 2088 | 2110 | </strong> |
| 2089 | 2111 | </div> |
| 2090 | 2112 | </div> |
| 2091 | 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 | 2114 | <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models. 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 | 2115 | <label for="openai_reasoning_effort"> |
| 2094 | 2116 | <span data-i18n="Reasoning Effort">Reasoning Effort</span> |
| @@ -2419,6 +2441,20 @@ | ||
| 2419 | 2441 | <span data-i18n="Allow fallback providers">Allow fallback providers</span> |
| 2420 | 2442 | </label> |
| 2421 | 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 | 2458 | </div> |
| 2423 | 2459 | <div data-tg-type="infermaticai" class="flex-container flexFlowColumn"> |
| 2424 | 2460 | <h4 data-i18n="InfermaticAI API Key">InfermaticAI API Key</h4> |
| @@ -2845,7 +2881,7 @@ | ||
| 2845 | 2881 | <option value="zai">Z.AI (GLM)</option> |
| 2846 | 2882 | </optgroup> |
| 2847 | 2883 | </select> |
| 2848 | 2884 | <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,vertexai,deepseek,xai,zai,moonshot"> |
| 2849 | 2885 | <div class="inline-drawer-toggle inline-drawer-header"> |
| 2850 | 2886 | <b data-i18n="Reverse Proxy">Reverse Proxy</b> |
| 2851 | 2887 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> |
| @@ -2909,7 +2945,7 @@ | ||
| 2909 | 2945 | </div> |
| 2910 | 2946 | </div> |
| 2911 | 2947 | </div> |
| 2912 | 2948 | <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,vertexai,deepseek,xai,zai,moonshot"> |
| 2913 | 2949 | <div class="reverse_proxy_warning"> |
| 2914 | 2950 | <b> |
| 2915 | 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 | 3104 | <h4 data-i18n="Claude Model">Claude Model</h4> |
| 3069 | 3105 | <select id="model_claude_select"> |
| 3070 | 3106 | <optgroup label="Versions"> |
| 3107 | + <option value="claude-opus-4-6">claude-opus-4-6</option> | |
| 3071 | 3108 | <option value="claude-opus-4-5">claude-opus-4-5</option> |
| 3072 | 3109 | <option value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option> |
| 3073 | 3110 | <option value="claude-sonnet-4-5">claude-sonnet-4-5</option> |
| @@ -3161,6 +3198,20 @@ | ||
| 3161 | 3198 | <i class="fa-solid fa-lightbulb"></i> |
| 3162 | 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 | 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 | 3215 | </form> |
| 3165 | 3216 | <form id="ai21_form" data-source="ai21" action="javascript:void(null);" method="post" enctype="multipart/form-data"> |
| 3166 | 3217 | <h4 data-i18n="AI21 API Key">AI21 API Key</h4> |
| @@ -3750,19 +3801,22 @@ | ||
| 3750 | 3801 | </select> |
| 3751 | 3802 | </div> |
| 3752 | 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 | 3816 | <h4 data-i18n="Pollinations Model">Pollinations Model</h4> |
| 3754 | 3817 | <select id="model_pollinations_select"> |
| 3755 | 3818 | <!-- Populated by JavaScript --> |
| 3756 | 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 | 3820 | </div> |
| 3767 | 3821 | <div id="moonshot_form" data-source="moonshot"> |
| 3768 | 3822 | <h4> |
| @@ -3811,7 +3865,10 @@ | ||
| 3811 | 3865 | </select> |
| 3812 | 3866 | <h4 data-i18n="Z.AI Model">Z.AI Model</h4> |
| 3813 | 3867 | <select id="model_zai_select"> |
| 3868 | + <option value="glm-5">glm-5</option> | |
| 3814 | 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 | 3872 | <option value="glm-4.6">glm-4.6</option> |
| 3816 | 3873 | <option value="glm-4.6v">glm-4.6v</option> |
| 3817 | 3874 | <option value="glm-4.6v-flash">glm-4.6v-flash</option> |
| @@ -3982,7 +4039,7 @@ | ||
| 3982 | 4039 | <small data-i18n="Story String">Story String</small> |
| 3983 | 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 | 4041 | </label> |
| 3985 | 4042 | <textarea id="context_story_string" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 3986 | 4043 | </div> |
| 3987 | 4044 | <div class="flex-container flexFlowColumn" data-cc-null> |
| 3988 | 4045 | <div id="context_story_string_position_block"> |
| @@ -4019,7 +4076,7 @@ | ||
| 4019 | 4076 | <small data-i18n="Example Separator">Example Separator</small> |
| 4020 | 4077 | </label> |
| 4021 | 4078 | <div> |
| 4022 | 4079 | <textarea id="context_example_separator" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4023 | 4080 | </div> |
| 4024 | 4081 | </div> |
| 4025 | 4082 | <div class="flex1"> |
| @@ -4027,7 +4084,7 @@ | ||
| 4027 | 4084 | <small data-i18n="Chat Start">Chat Start</small> |
| 4028 | 4085 | </label> |
| 4029 | 4086 | <div> |
| 4030 | 4087 | <textarea id="context_chat_start" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4031 | 4088 | </div> |
| 4032 | 4089 | </div> |
| 4033 | 4090 | </div> |
| @@ -4191,11 +4248,11 @@ | ||
| 4191 | 4248 | <div class="flex-container"> |
| 4192 | 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 | 4250 | <small data-i18n="User Prefix">User Message Prefix</small> |
| 4194 | 4251 | <textarea id="instruct_input_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4195 | 4252 | </div> |
| 4196 | 4253 | <div class="flexAuto" title="Inserted after a User message." data-i18n="[title]Inserted after a User message."> |
| 4197 | 4254 | <small data-i18n="User Suffix">User Message Suffix</small> |
| 4198 | 4255 | <textarea id="instruct_input_suffix" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea> |
| 4199 | 4256 | </div> |
| 4200 | 4257 | </div> |
| 4201 | 4258 | </details> |
| @@ -4204,11 +4261,11 @@ | ||
| 4204 | 4261 | <div class="flex-container"> |
| 4205 | 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 | 4263 | <small data-i18n="Assistant Prefix">Assistant Message Prefix</small> |
| 4207 | 4264 | <textarea id="instruct_output_sequence" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea> |
| 4208 | 4265 | </div> |
| 4209 | 4266 | <div class="flexAuto" title="Inserted after an Assistant message." data-i18n="[title]Inserted after an Assistant message."> |
| 4210 | 4267 | <small data-i18n="Assistant Suffix">Assistant Message Suffix</small> |
| 4211 | 4268 | <textarea id="instruct_output_suffix" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea> |
| 4212 | 4269 | </div> |
| 4213 | 4270 | </div> |
| 4214 | 4271 | </details> |
| @@ -4217,11 +4274,11 @@ | ||
| 4217 | 4274 | <div class="flex-container"> |
| 4218 | 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 | 4276 | <small data-i18n="System Prefix">System Message Prefix</small> |
| 4220 | 4277 | <textarea id="instruct_system_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4221 | 4278 | </div> |
| 4222 | 4279 | <div class="flexAuto" id="instruct_system_suffix_block" title="Inserted after a System message." data-i18n="[title]Inserted after a System message."> |
| 4223 | 4280 | <small data-i18n="System Suffix">System Message Suffix</small> |
| 4224 | 4281 | <textarea id="instruct_system_suffix" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea> |
| 4225 | 4282 | </div> |
| 4226 | 4283 | </div> |
| 4227 | 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 | 4293 | <div class="flex-container"> |
| 4237 | 4294 | <div class="flexAuto" title="Inserted before the first Assistant's message." data-i18n="[title]Inserted before the first Assistant's message."> |
| 4238 | 4295 | <small data-i18n="First Assistant Prefix">First Assistant Prefix</small> |
| 4239 | 4296 | <textarea id="instruct_first_output_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4240 | 4297 | </div> |
| 4241 | 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 | 4299 | <small data-i18n="Last Assistant Prefix">Last Assistant Prefix</small> |
| 4243 | 4300 | <textarea id="instruct_last_output_sequence" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea> |
| 4244 | 4301 | </div> |
| 4245 | 4302 | </div> |
| 4246 | 4303 | <div class="flex-container"> |
| 4247 | 4304 | <div class="flexAuto" title="Inserted before the first User's message." data-i18n="[title]Inserted before the first User's message."> |
| 4248 | 4305 | <small data-i18n="First User Prefix">First User Prefix</small> |
| 4249 | 4306 | <textarea id="instruct_first_input_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4250 | 4307 | </div> |
| 4251 | 4308 | <div class="flexAuto" title="Inserted before the last User's message." data-i18n="[title]instruct_last_input_sequence"> |
| 4252 | 4309 | <small data-i18n="Last User Prefix">Last User Prefix</small> |
| 4253 | 4310 | <textarea id="instruct_last_input_sequence" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea> |
| 4254 | 4311 | </div> |
| 4255 | 4312 | </div> |
| 4256 | 4313 | <div class="flex-container"> |
| 4257 | 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 | 4315 | <small data-i18n="System Instruction Prefix">System Instruction Prefix</small> |
| 4259 | 4316 | <textarea id="instruct_last_system_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4260 | 4317 | </div> |
| 4261 | 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 | 4319 | <small data-i18n="Stop Sequence">Stop Sequence</small> |
| 4263 | 4320 | <textarea id="instruct_stop_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4264 | 4321 | </div> |
| 4265 | 4322 | </div> |
| 4266 | 4323 | <div class="flex-container"> |
| 4267 | 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 | 4325 | <small data-i18n="User Filler Message">User Filler Message</small> |
| 4269 | 4326 | <textarea id="instruct_user_alignment_message" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4270 | 4327 | </div> |
| 4271 | 4328 | </div> |
| 4272 | 4329 | </details> |
| @@ -4304,7 +4361,7 @@ | ||
| 4304 | 4361 | <small data-i18n="Prompt Content">Prompt Content</small> |
| 4305 | 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 | 4363 | </label> |
| 4307 | 4364 | <textarea id="sysprompt_content" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4308 | 4365 | </div> |
| 4309 | 4366 | |
| 4310 | 4367 | <div> |
| @@ -4312,7 +4369,7 @@ | ||
| 4312 | 4369 | <small data-i18n="Post-History Instructions">Post-History Instructions</small> |
| 4313 | 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 | 4371 | </label> |
| 4315 | 4372 | <textarea id="sysprompt_post_history" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4316 | 4373 | </div> |
| 4317 | 4374 | </div> |
| 4318 | 4375 | |
| @@ -4437,17 +4494,17 @@ | ||
| 4437 | 4494 | <div class="flex-container"> |
| 4438 | 4495 | <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix"> |
| 4439 | 4496 | <small data-i18n="Prefix">Prefix</small> |
| 4440 | 4497 | <textarea id="reasoning_prefix" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4441 | 4498 | </div> |
| 4442 | 4499 | <div class="flex1" title="Inserted after the reasoning content." data-i18n="[title]reasoning_suffix"> |
| 4443 | 4500 | <small data-i18n="Suffix">Suffix</small> |
| 4444 | 4501 | <textarea id="reasoning_suffix" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4445 | 4502 | </div> |
| 4446 | 4503 | </div> |
| 4447 | 4504 | <div class="flex-container"> |
| 4448 | 4505 | <div class="flex1" title="Inserted between the reasoning and the message content." data-i18n="[title]reasoning_separator"> |
| 4449 | 4506 | <small data-i18n="Separator">Separator</small> |
| 4450 | 4507 | <textarea id="reasoning_separator" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4451 | 4508 | </div> |
| 4452 | 4509 | </div> |
| 4453 | 4510 | </details> |
| @@ -4469,7 +4526,7 @@ | ||
| 4469 | 4526 | </span> |
| 4470 | 4527 | </small> |
| 4471 | 4528 | <div> |
| 4472 | 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 | 4530 | </div> |
| 4474 | 4531 | </div> |
| 4475 | 4532 | |
| @@ -4481,7 +4538,7 @@ | ||
| 4481 | 4538 | </span> |
| 4482 | 4539 | </small> |
| 4483 | 4540 | <div> |
| 4484 | 4541 | <textarea id="start_reply_with" data-macros class="text_pole textarea_compact autoSetHeight"></textarea> |
| 4485 | 4542 | </div> |
| 4486 | 4543 | <label class="checkbox_label" for="chat-show-reply-prefix-checkbox"> |
| 4487 | 4544 | <input id="chat-show-reply-prefix-checkbox" type="checkbox" /> |
| @@ -4780,7 +4837,7 @@ | ||
| 4780 | 4837 | <div name="themeElements" class="flex-container flexFlowColumn flexNoGap"> |
| 4781 | 4838 | <!-- <h4><span data-i18n="UI Colors">Theme Settings</span></h4> --> |
| 4782 | 4839 | <div name="AvatarAndChatDisplay" class="flex-container flexFlowColumn"> |
| 4783 | 4840 | <div class="flex-container alignItemsBaseline" title="This style applies to all avatars globaly, including your Persona, Character ManagmentManagement, Account selection, etc." data-i18n="[title]This style applies to all avatars globaly, including your Persona, Character ManagmentManagement, Account selection, etc."> |
| 4784 | 4841 | <span data-i18n="Avatar Style:">Avatars:</span> |
| 4785 | 4842 | <select id="avatar_style" class="widthNatural flex1 margin0 text_pole"> |
| 4786 | 4843 | <option value="0" data-i18n="Circle">Circle</option> |
| @@ -5362,20 +5419,28 @@ | ||
| 5362 | 5419 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> |
| 5363 | 5420 | </div> |
| 5364 | 5421 | <div class="inline-drawer-content"> |
| 5365 | - <label for="stscript_autocomplete_state"> | |
| 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."> | |
| 5366 | 5423 | <smalllabel data-i18nfor="Visibilitystscript_autocomplete_state">Visibility</small> |
| 5367 | - </label> | |
| 5424 | + <small data-i18n="Visibility">Visibility</small> | |
| 5368 | - <select id="stscript_autocomplete_state"> | |
| 5425 | + </label> | |
| 5369 | - <option value="0" data-i18n="Don't show">Don't show</option> | |
| 5426 | + <select id="stscript_autocomplete_state"> | |
| 5370 | 5427 | <option value="10" data-i18n="Input length >Don't 1show">Input length >Don't 1show</option> |
| 5371 | 5428 | <option value="21" data-i18n="AlwaysInput showlength > 1">AlwaysInput showlength > 1</option> |
| 5372 | - </select> | |
| 5429 | + <option value="2" data-i18n="Always show">Always show</option> | |
| 5430 | + </select> | |
| 5431 | + </div> | |
| 5373 | 5432 | <label class="checkbox_label" for="stscript_autocomplete_autoHide"> |
| 5374 | 5433 | <input id="stscript_autocomplete_autoHide" type="checkbox" /> |
| 5375 | 5434 | <small data-i18n="Automatically hide details"> |
| 5376 | 5435 | Automatically hide details |
| 5377 | 5436 | </small> |
| 5378 | 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 | 5444 | <div class="flex-container"> |
| 5380 | 5445 | <div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete."> |
| 5381 | 5446 | <label for="stscript_matching"> |
| @@ -5499,6 +5564,12 @@ | ||
| 5499 | 5564 | </div> |
| 5500 | 5565 | <div class="bg-header-row-2"> |
| 5501 | 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 | 5573 | </div> |
| 5503 | 5574 | </div> |
| 5504 | 5575 | <div id="bg_tabs" class="heading-container-with-controls"> |
| @@ -5695,7 +5766,7 @@ | ||
| 5695 | 5766 | <span data-i18n="Persona Description">Persona Description</span> |
| 5696 | 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 | 5768 | </h4> |
| 5698 | 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: [{{user}} is a 28-year-old Romanian cat girl.]" class="text_pole textarea_compact" value="" autocomplete="off" rows="8"></textarea> |
| 5699 | 5770 | |
| 5700 | 5771 | <div class="flex-container justifySpaceBetween"> |
| 5701 | 5772 | <h4 data-i18n="Position">Position</h4> |
| @@ -5954,7 +6025,7 @@ | ||
| 5954 | 6025 | </span> |
| 5955 | 6026 | </div> |
| 5956 | 6027 | </div> |
| 5957 | 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 | 6029 | <div class="extension_token_counter"> |
| 5959 | 6030 | <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="description_textarea" data-token-permanent="true">counting...</span> |
| 5960 | 6031 | </div> |
| @@ -5974,7 +6045,7 @@ | ||
| 5974 | 6045 | </span> |
| 5975 | 6046 | </div> |
| 5976 | 6047 | </div> |
| 5977 | 6048 | <textarea classid="mdHotkeysfirstmessage_textarea" idclass="firstmessage_textareamdHotkeys" 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 | 6049 | <div class="extension_token_counter"> |
| 5979 | 6050 | <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="firstmessage_textarea">counting...</span> |
| 5980 | 6051 | </div> |
| @@ -6104,6 +6175,12 @@ | ||
| 6104 | 6175 | </div> |
| 6105 | 6176 | <div class="inline-drawer-content"> |
| 6106 | 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 | 6184 | <div id="rm_group_members_pagination" class="rm_group_members_pagination group_pagination"></div> |
| 6108 | 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 | 6186 | </div> |
| @@ -6115,7 +6192,7 @@ | ||
| 6115 | 6192 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> |
| 6116 | 6193 | </div> |
| 6117 | 6194 | <div class="inline-drawer-content"> |
| 6118 | 6195 | <div id="unaddedCharList" name="Unadded Char List" class="flex-container flexFlowColumn overflowYAuto flex1"> |
| 6119 | 6196 | <div id="rm_group_add_members_header"> |
| 6120 | 6197 | <input id="rm_group_filter" class="text_pole margin0" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." /> |
| 6121 | 6198 | </div> |
| @@ -6292,7 +6369,7 @@ | ||
| 6292 | 6369 | <span data-i18n="Main Prompt">Main Prompt</span> |
| 6293 | 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 | 6371 | </h4> |
| 6295 | 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. (v2 spec: system_prompt)" form="form_create" class="text_pole" autocomplete="off" rows="3"></textarea> |
| 6296 | 6373 | <div class="extension_token_counter"> |
| 6297 | 6374 | <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="system_prompt_textarea">counting...</span> |
| 6298 | 6375 | </div> |
| @@ -6302,7 +6379,7 @@ | ||
| 6302 | 6379 | <span data-i18n="Post-History Instructions">Post-History Instructions</span> |
| 6303 | 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 | 6381 | </h4> |
| 6305 | 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. (v2 spec: post_history_instructions)" form="form_create" class="text_pole" autocomplete="off" rows="3"></textarea> |
| 6306 | 6383 | <div class="extension_token_counter"> |
| 6307 | 6384 | <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="post_history_instructions_textarea">counting...</span> |
| 6308 | 6385 | </div> |
| @@ -6355,7 +6432,7 @@ | ||
| 6355 | 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 | 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 | 6434 | </h4> |
| 6358 | 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 | 6436 | <div class="extension_token_counter"> |
| 6360 | 6437 | <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="personality_textarea" data-token-permanent="true">counting...</span> |
| 6361 | 6438 | </div> |
| @@ -6368,7 +6445,7 @@ | ||
| 6368 | 6445 | <span class="fa-solid fa-circle-question note-link-span"></span> |
| 6369 | 6446 | </a> |
| 6370 | 6447 | </h4> |
| 6371 | 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 | 6449 | <div class="extension_token_counter"> |
| 6373 | 6450 | <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="scenario_pole" data-token-permanent="true">counting...</span> |
| 6374 | 6451 | </div> |
| @@ -6381,7 +6458,7 @@ | ||
| 6381 | 6458 | </span> |
| 6382 | 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 | 6460 | </h4> |
| 6384 | 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 | 6462 | </div> |
| 6386 | 6463 | <div> |
| 6387 | 6464 | <h4> |
| @@ -6431,7 +6508,7 @@ | ||
| 6431 | 6508 | </a> |
| 6432 | 6509 | </h5> |
| 6433 | 6510 | </div> |
| 6434 | 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 <START> on a new line.)" form="form_create" rows="6"></textarea> |
| 6435 | 6512 | <div class="extension_token_counter"> |
| 6436 | 6513 | <span data-i18n="extension_token_counter">Tokens:</span> <span data-token-counter="mes_example_textarea">counting...</span> |
| 6437 | 6514 | </div> |
| @@ -7124,7 +7201,7 @@ | ||
| 7124 | 7201 | <span> </span> |
| 7125 | 7202 | <span id="completion_prompt_manager_popup_entry_source"></span> |
| 7126 | 7203 | </div> |
| 7127 | 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 | 7205 | </div> |
| 7129 | 7206 | <div class="completion_prompt_manager_popup_entry_form_footer"> |
| 7130 | 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 | 7487 | </div> |
| 7411 | 7488 | </div> |
| 7412 | 7489 | </summary> |
| 7413 | 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 | 7491 | </details> |
| 7415 | 7492 | </div> |
| 7416 | 7493 | </div> |
| @@ -7500,7 +7577,7 @@ | ||
| 7500 | 7577 | <b data-i18n="Unique to this chat">Unique to this chat</b>.<br> |
| 7501 | 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 | 7579 | </small> |
| 7503 | 7580 | <textarea id="extension_floating_prompt" data-macros class="text_pole textarea_compact" rows="8"></textarea> |
| 7504 | 7581 | <div class="extension_token_counter"> |
| 7505 | 7582 | <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_prompt_token_counter">0</span> |
| 7506 | 7583 | </div> |
| @@ -7557,7 +7634,7 @@ | ||
| 7557 | 7634 | <div class="inline-drawer-content"> |
| 7558 | 7635 | <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 | 7636 | can't be modified when a group chat is open.</small> |
| 7560 | 7637 | <textarea id="extension_floating_chara" data-macros class="text_pole textarea_compact" rows="8" placeholder="Example: [Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea> |
| 7561 | 7638 | <div class="extension_token_counter"> |
| 7562 | 7639 | <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_chara_token_counter">0</span> |
| 7563 | 7640 | </div> |
| @@ -7589,7 +7666,7 @@ | ||
| 7589 | 7666 | </div> |
| 7590 | 7667 | <div class="inline-drawer-content"> |
| 7591 | 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 | 7669 | <textarea id="extension_floating_default" data-macros class="text_pole textarea_compact" rows="8" placeholder="Example: [Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea> |
| 7593 | 7670 | <div class="extension_token_counter"> |
| 7594 | 7671 | <span data-i18n="extension_token_counter">Tokens:</span> <span id="extension_floating_default_token_counter">0</span> |
| 7595 | 7672 | </div> |
| @@ -290,6 +290,8 @@ | ||
| 290 | 290 | "View Remaining Credits": "Afficher les crédits restants", |
| 291 | 291 | "OpenRouter Model": "Modèle OpenRouter", |
| 292 | 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 | 295 | "InfermaticAI API Key": "Clé API InfermaticAI", |
| 294 | 296 | "InfermaticAI Model": "Modèle InfermaticAI", |
| 295 | 297 | "DreamGen API key": "Clé API DreamGen", |
| @@ -185,9 +185,7 @@ | ||
| 185 | 185 | "Mirostat (mode=1 is only for llama.cpp)": "Mirostat(mode=1 仅用于 llama.cpp)", |
| 186 | 186 | "Mirostat_desc": "Mirostat 是一个用于控制输出困惑度的恒温器", |
| 187 | 187 | "Mirostat Mode": "Mirostat 模式", |
| 188 | - "Variability parameter for Mirostat outputs": "Mirostat 输出的变异性参数。", | |
| 189 | 188 | "Mirostat Eta": "Mirostat η", |
| 190 | - "Learning rate of Mirostat": "Mirostat 的学习率。", | |
| 191 | 189 | "Beam search": "束搜索", |
| 192 | 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 | 191 | "# of Beams": "光束数量", |
| @@ -215,9 +213,9 @@ | ||
| 215 | 213 | "Spaces Between Special Tokens": "特殊词符之间的空格", |
| 216 | 214 | "Seed_desc": "一个用于生成确定性和可复现的输出的随机种子。设置为 -1 时会使用随机种子。", |
| 217 | 215 | "LLaMA / Mistral / Yi models only": "LLaMA / Mistral / Yi模型专用。首先确保您选择了适当的词符化器。\n这项设置决定了你不想在结果中看到的字符串。\n每行一个字符串。可以是文本或者[词符id]。\n许多词符以空格开头。如果不确定,请使用词符计数器。", |
| 218 | 216 | "Global list": "Global list全局列表", |
| 219 | 217 | "Example: some text [42, 69, 1337]": "例如:\n一些文本\n[42, 69, 1337]", |
| 220 | 218 | "Preset-specific list": "Preset-specific list预设特有的列表", |
| 221 | 219 | "CFG": "CFG", |
| 222 | 220 | "Classifier Free Guidance. More helpful tip coming soon": "无分类器指导(CFG)。更多有用的提示敬请期待。", |
| 223 | 221 | "Scale": "缩放比例", |
| @@ -228,6 +226,7 @@ | ||
| 228 | 226 | "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。", |
| 229 | 227 | "JSON Schema": "JSON 结构", |
| 230 | 228 | "Type in the desired JSON schema": "输入所需的 JSON 结构", |
| 229 | + "Allow empty schema objects": "允许空结构对象", | |
| 231 | 230 | "Top P & Min P": "Top P 和 Min P", |
| 232 | 231 | "Load default order": "加载默认顺序", |
| 233 | 232 | "Sampler Order": "取样器顺序", |
| @@ -250,14 +249,12 @@ | ||
| 250 | 249 | "Space": "空格", |
| 251 | 250 | "Newline": "换行", |
| 252 | 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 | 252 | "Continue prefill": "继续预填充", |
| 258 | 253 | "Continue sends the last message as assistant role instead of system message with instruction.": "继续发送的是作为助手角色的最后一条消息,而不是带有指示的系统消息。", |
| 259 | 254 | "Squash system messages": "压缩系统消息", |
| 260 | 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 | 258 | "Enable web search": "启用联网搜索", |
| 262 | 259 | "Use search capabilities provided by the backend.": "使用后端提供的联网搜索功能。", |
| 263 | 260 | "openrouter_web_search_fee": "收费,每个提示词会多收 $0.02。", |
| @@ -268,20 +265,19 @@ | ||
| 268 | 265 | "enable_functions_desc_2": "功能工具", |
| 269 | 266 | "enable_functions_desc_3": "可以被各种扩展利用来提供附加功能。", |
| 270 | 267 | "enable_functions_desc_4": "当提示词后处理没有选择工具时不支持。", |
| 271 | 268 | "Send inline imagesmedia": "发送图片发送内联媒体", |
| 272 | 269 | "image_inlining_hint_1": "如果模型支持,就可以在提示词中发送媒体文件。", |
| 270 | + "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。", | |
| 271 | + "audio_inlining_hint_2": "音频必须小于 20 MB。", | |
| 273 | 272 | "Inline Image Quality": "图片画质", |
| 274 | 273 | "openai_inline_image_quality_auto": "自动", |
| 275 | 274 | "openai_inline_image_quality_low": "低", |
| 276 | 275 | "openai_inline_image_quality_high": "高", |
| 277 | - "Send inline videos": "发送视频", | |
| 278 | - "video_inlining_hint_4": "视频必须在 20MB 以下且时长不超过1分钟。", | |
| 279 | 276 | "Request inline images": "请求图片返回", |
| 280 | 277 | "Allows the model to return image attachments.": "允许模型返回图片附件。", |
| 281 | 278 | "Request inline images_desc_2": "与以下几个功能不兼容:函数调用、联网搜搜、系统提示词。", |
| 282 | 279 | "Use system promptResolution": "使用系统提示词分辨率", |
| 283 | 280 | "Merges_all_system_messages_desc_1Aspect Ratio": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过长宽比", |
| 284 | - "Merges_all_system_messages_desc_2": "字段发送。", | |
| 285 | 281 | "Request model reasoning": "请求思维链", |
| 286 | 282 | "Allows the model to return its thinking process.": "允许模型返回其思维过程。", |
| 287 | 283 | "This setting affects visibility only.": "此设置只影响思维链是否可见。", |
| @@ -295,12 +291,18 @@ | ||
| 295 | 291 | "openai_reasoning_effort_maximum": "极高", |
| 296 | 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 | 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 | 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 | 302 | "Assistant Prefill": "AI预填", |
| 300 | 303 | "Expand the editor": "展开编辑器", |
| 301 | 304 | "Start Claude's answer with...": "以如下内容开始Claude的回答...", |
| 302 | 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 | 306 | "Confirm token parsing with": "确认使用以下工具进行词符解析", |
| 305 | 307 | "Tokenizer": "分词器", |
| 306 | 308 | "New preset": "新预设", |
| @@ -381,7 +383,7 @@ | ||
| 381 | 383 | "Date Desc": "日期倒序", |
| 382 | 384 | "category": "分类", |
| 383 | 385 | "Top": "热门", |
| 384 | 386 | "New": "新建最新", |
| 385 | 387 | "All": "全部", |
| 386 | 388 | "All Classes": "所有分类", |
| 387 | 389 | "Toggle grid view": "切换网格视图", |
| @@ -398,6 +400,7 @@ | ||
| 398 | 400 | "Aphrodite Model": "Aphrodite 模型", |
| 399 | 401 | "ggml-org/llama.cpp": "ggml-org/llama.cpp", |
| 400 | 402 | "Example: http://127.0.0.1:8080": "示例:http://127.0.0.1:8080", |
| 403 | + "llama.cpp Model": "llama.cpp 模型", | |
| 401 | 404 | "Example: http://127.0.0.1:11434": "示例:http://127.0.0.1:11434", |
| 402 | 405 | "Ollama Model": "Ollama 模型", |
| 403 | 406 | "Download": "下载", |
| @@ -465,7 +468,7 @@ | ||
| 465 | 468 | "(Express mode)": "(快速模式)", |
| 466 | 469 | "API Key": "API 密钥", |
| 467 | 470 | "Project ID": "项目ID:", |
| 468 | 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 | 472 | "Service Account Configuration": "服务帐户配置", |
| 470 | 473 | "Service Account JSON Content": "服务帐户 JSON 内容:", |
| 471 | 474 | "For privacy reasons, your Service Account JSON content will be hidden after you click 'Validate JSON'.": "出于隐私考虑,你的服务账号 JSON 内容将在点击“验证JSON”后隐藏。", |
| @@ -478,6 +481,13 @@ | ||
| 478 | 481 | "Groq Model": "Groq 模型", |
| 479 | 482 | "Electron Hub API Key": "Electron Hub API 密钥", |
| 480 | 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 | 491 | "NanoGPT API Key": "NanoGPT API 密钥", |
| 482 | 492 | "NanoGPT Model": "NanoGPT 模型", |
| 483 | 493 | "DeepSeek API Key": "DeepSeek API 密钥", |
| @@ -506,6 +516,19 @@ | ||
| 506 | 516 | "Avoid sending sensitive information. Provider's outputs may include ads.": "请避免发送敏感信息。输出可能有提供商的广告。", |
| 507 | 517 | "Moonshot AI API Key": "Moonshot AI API 密钥", |
| 508 | 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 | 532 | "Prompt Post-Processing": "提示词后处理", |
| 510 | 533 | "Applies additional processing to the prompt before sending it to the API.": "在将提示词发送到 API 之前对其进行额外处理。", |
| 511 | 534 | "prompt_post_processing_none": "未选择", |
| @@ -529,6 +552,7 @@ | ||
| 529 | 552 | "Master Import": "全局导入", |
| 530 | 553 | "Export Advanced Formatting settings": "导出高级格式化设置", |
| 531 | 554 | "Master Export": "全局导出", |
| 555 | + "Grayed-out options have no effect when Chat Completion API is used.": "灰色选项在使用 聊天补全API 时无效。", | |
| 532 | 556 | "Context Template": "上下文模板", |
| 533 | 557 | "context_derived": "若可能,从模型的元数据获取。", |
| 534 | 558 | "Select your current Context Template": "选择你当前的上下文模板", |
| @@ -728,6 +752,7 @@ | ||
| 728 | 752 | "Delete a theme": "删除主题", |
| 729 | 753 | "Update a theme file": "更新主题文件", |
| 730 | 754 | "Save as a new theme": "另存为新主题", |
| 755 | + "This style applies to all avatars globaly, including your Persona, Character Management, Account selection, etc.": "此样式将应用在所有头像,包括您的用户设定、角色管理、帐户选择等。", | |
| 731 | 756 | "Avatar Style:": "头像样式:", |
| 732 | 757 | "Circle": "圆形", |
| 733 | 758 | "Square": "正方形", |
| @@ -737,6 +762,10 @@ | ||
| 737 | 762 | "Flat": "扁平", |
| 738 | 763 | "Bubbles": "气泡", |
| 739 | 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 | 769 | "Notifications:": "通知:", |
| 741 | 770 | "Top Left": "左上", |
| 742 | 771 | "Top Center": "顶部居中", |
| @@ -835,9 +864,13 @@ | ||
| 835 | 864 | "Find and delete backups, unused chats, files, images, etc.": "寻找和删除备份、未使用的聊天、文件、图片等。", |
| 836 | 865 | "Clean-Up": "清理", |
| 837 | 866 | "Smooth Streaming": "平滑流式传输", |
| 838 | 867 | "Experimental feature. MayBypass notsmooth workstreaming forin allreasoning backendsblocks.": "实验性功能。可能不适用于所有后端在推理块中不使用平滑流式传输。", |
| 868 | + "Exclude 'Thinking...'": "排除“思考中...”", | |
| 839 | 869 | "Slow": "慢", |
| 840 | 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 | 874 | "Play a sound when a message generation finishes": "当消息生成完毕时播放声音", |
| 842 | 875 | "Message Sound": "消息声音", |
| 843 | 876 | "Only play a sound when ST's browser tab is unfocused": "仅在ST的浏览器标签页未被打开时播放声音", |
| @@ -871,6 +904,9 @@ | ||
| 871 | 904 | "Gradual push-out": "逐渐推出", |
| 872 | 905 | "Always include examples": "始终包含示例", |
| 873 | 906 | "Never include examples": "永不包含示例", |
| 907 | + "Image Swipe Behavior:": "图片滑动刷新行为:", | |
| 908 | + "Generate new": "生成新的", | |
| 909 | + "Roll over": "循环现有", | |
| 874 | 910 | "Send on Enter": "按 Enter 发送", |
| 875 | 911 | "Disabled": "已禁用", |
| 876 | 912 | "Automatic (PC)": "自动(PC)", |
| @@ -896,6 +932,8 @@ | ||
| 896 | 932 | "Allow {{user}}: in bot messages": "在机器人消息中允许 {{user}}: ", |
| 897 | 933 | "Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "跳过消息文本中的编码和字符,允许一部分HTML标记以及Markdown", |
| 898 | 934 | "Show tags in responses": "在响应中显示标签", |
| 935 | + "Experimental Macro Engine": "实验性宏引擎", | |
| 936 | + "Experimental feature. Currently in development to test.": "实验性功能。目前正在开发测试中。", | |
| 899 | 937 | "Allow AI messages in groups to contain lines spoken by other group members": "允许群聊中的AI输出群中其他成员说的话", |
| 900 | 938 | "Relax message trim in Groups": "减轻群聊中的消息修剪", |
| 901 | 939 | "Log prompts to console": "将提示词输出到控制台", |
| @@ -960,10 +998,15 @@ | ||
| 960 | 998 | "Center": "居中", |
| 961 | 999 | "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景", |
| 962 | 1000 | "Auto-select": "自动选择", |
| 1001 | + "Add a new background": "添加新背景", | |
| 963 | 1002 | "Add Background": "添加背景", |
| 964 | 1003 | "Global": "全局", |
| 1004 | + "Chat": "聊天", | |
| 1005 | + "Make thumbnails smaller": "缩小缩略图", | |
| 1006 | + "Make thumbnails larger": "放大缩略图", | |
| 965 | 1007 | "bg_chat_hint_1": "使用生成的聊天背景", |
| 966 | 1008 | "bg_chat_hint_2": "扩展名将出现在这里。", |
| 1009 | + "Scroll backgrounds to top": "回顶", | |
| 967 | 1010 | "Extensions": "扩展", |
| 968 | 1011 | "Notify on extension updates": "在扩展更新时通知", |
| 969 | 1012 | "Manage extensions": "管理扩展", |
| @@ -1004,7 +1047,6 @@ | ||
| 1004 | 1047 | "Click to lock your selected persona to the current character. Click again to remove the lock.": "点击将选择的用户设定与当前角色绑定。再次点击以解绑。", |
| 1005 | 1048 | "Character": "角色", |
| 1006 | 1049 | "Click to lock your selected persona to the current chat. Click again to remove the lock.": "点击将选择的人设与当前聊天绑定。再次点击以解绑。", |
| 1007 | - "Chat": "聊天", | |
| 1008 | 1050 | "Global Settings": "全局设置", |
| 1009 | 1051 | "Show notifications on switching personas": "切换用户设定时显示通知", |
| 1010 | 1052 | "When multiple personas are connected to a character, a popup will appear to select which one to use": "当多个用户设定与一个角色绑定时,会弹出一个弹窗让用户选择使用哪一个。", |
| @@ -1038,7 +1080,7 @@ | ||
| 1038 | 1080 | "More...": "更多...", |
| 1039 | 1081 | "Link to World Info": "链接到世界书", |
| 1040 | 1082 | "Import Card Lore": "导入角色卡的世界书", |
| 1041 | 1083 | "ScenarioCharacter OverrideSettings Overrides": "场景覆盖角色设置覆盖", |
| 1042 | 1084 | "Convert to Persona": "转换为用户角色", |
| 1043 | 1085 | "Rename": "重命名", |
| 1044 | 1086 | "Link to Source": "来源链接", |
| @@ -1078,7 +1120,7 @@ | ||
| 1078 | 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 | 1121 | "Inserted after each part of the joined fields.": "插入到加入字段的每个部分之后。", |
| 1080 | 1122 | "Join Suffix": "加入后缀", |
| 1081 | 1123 | "Set a group chat scenariocharacter settings overrides": "设置群聊背景设置群聊角色设置覆盖", |
| 1082 | 1124 | "Click to allow/forbid the use of external media for this group.": "单击以允许/禁止该组使用外部媒体。", |
| 1083 | 1125 | "Restore collage avatar": "恢复拼贴头像", |
| 1084 | 1126 | "Allow self responses": "允许自我回复", |
| @@ -1156,9 +1198,9 @@ | ||
| 1156 | 1198 | "Save": "保存", |
| 1157 | 1199 | "Chat History": "聊天记录", |
| 1158 | 1200 | "Import Chat": "导入聊天", |
| 1159 | - "Copy to global backgrounds": "复制到全局背景", | |
| 1160 | 1201 | "Lock": "锁定", |
| 1161 | 1202 | "Unlock": "解锁", |
| 1203 | + "Copy to global backgrounds": "复制到全局背景", | |
| 1162 | 1204 | "Rename Background": "重命名背景", |
| 1163 | 1205 | "Delete Background": "删除背景", |
| 1164 | 1206 | "Select a World Info file for": "选择一个世界书文件给", |
| @@ -1191,6 +1233,8 @@ | ||
| 1191 | 1233 | "Optional Filter": "可选过滤器", |
| 1192 | 1234 | "Keywords or Regexes (ignored if empty)": "关键字或正则表达式(如果为空则忽略)", |
| 1193 | 1235 | "Comma separated list (ignored if empty)": "逗号分隔列表(如果为空则忽略)", |
| 1236 | + "wi_outlet_name": "为此世界信息条目设置锚点名称。\n\n位置为“锚点”的世界信息条目不会自动添加到提示词中。相反,它们将被收集并可作为提示词中的宏使用。\n在提示词中任何想要添加此特定锚点的所有世界信息条目的位置添加 {{outlet::YourName}}。", | |
| 1237 | + "Outlet Name": "锚点名称", | |
| 1194 | 1238 | "Use global setting": "使用全局设置", |
| 1195 | 1239 | "Case-Sensitive": "区分大小写", |
| 1196 | 1240 | "Use global": "使用全局", |
| @@ -1260,6 +1304,7 @@ | ||
| 1260 | 1304 | "at Depth System": "@D ⚙ [系统]在深度️", |
| 1261 | 1305 | "at Depth User": "@D 👤 [用户]在深度", |
| 1262 | 1306 | "at Depth AI": "@D 🤖 [AI]在深度", |
| 1307 | + "Outlet": "➡️ 锚点", | |
| 1263 | 1308 | "Depth": "深度", |
| 1264 | 1309 | "Order:": "顺序:", |
| 1265 | 1310 | "Order": "顺序", |
| @@ -1302,6 +1347,7 @@ | ||
| 1302 | 1347 | "Narrate": "朗读", |
| 1303 | 1348 | "Exclude message from prompts": "从提示词中排除消息", |
| 1304 | 1349 | "Include message in prompts": "将消息包含在提示词中", |
| 1350 | + "Toggle media display style": "切换媒体显示样式", | |
| 1305 | 1351 | "Embed file or image": "嵌入文件或图像", |
| 1306 | 1352 | "Create checkpoint": "创建检查点", |
| 1307 | 1353 | "Create Branch": "创建分支", |
| @@ -1321,10 +1367,6 @@ | ||
| 1321 | 1367 | "Collapse all reasoning blocks": "折叠所有推理块", |
| 1322 | 1368 | "Copy reasoning": "复制推理内容", |
| 1323 | 1369 | "Edit reasoning": "编辑推理内容", |
| 1324 | - "Expand and zoom": "展开并缩放", | |
| 1325 | - "Caption": "标题", | |
| 1326 | - "Swipe left": "向左滑动", | |
| 1327 | - "Swipe right": "向右滑动", | |
| 1328 | 1370 | "Welcome to SillyTavern!": "欢迎来到 SillyTavern!", |
| 1329 | 1371 | "SillyTavern is aimed at advanced users.": "SillyTavern 面向高级用户。", |
| 1330 | 1372 | "welcome_message_part_1": "阅读", |
| @@ -1362,6 +1404,12 @@ | ||
| 1362 | 1404 | "(This will be the first message from the character that starts every chat)": "(这是每次聊天开始时角色的第一条消息)", |
| 1363 | 1405 | "View contents": "查看内容", |
| 1364 | 1406 | "Remove the file": "删除文件", |
| 1407 | + "Expand and zoom": "展开并缩放", | |
| 1408 | + "Caption": "标题", | |
| 1409 | + "Swipe left": "向左滑动", | |
| 1410 | + "Swipe right": "向右滑动", | |
| 1411 | + "Play": "播放", | |
| 1412 | + "Mute": "静音", | |
| 1365 | 1413 | "Author's Note": "作者注释", |
| 1366 | 1414 | "Unique to this chat": "仅对此聊天生效", |
| 1367 | 1415 | "Checkpoints inherit the Note from their parent, and can be changed individually after that.": "检查点从其父级继承注释,之后可以单独更改。", |
| @@ -1479,6 +1527,7 @@ | ||
| 1479 | 1527 | "API": "API", |
| 1480 | 1528 | "Text Generation WebUI (oobabooga)": "文本生成 WebUI (oobabooga)", |
| 1481 | 1529 | "Model": "模型", |
| 1530 | + "Refresh model list": "刷新模型列表", | |
| 1482 | 1531 | "currently_selected": "[当前选定]", |
| 1483 | 1532 | "currently_loaded": "[当前正在加载]", |
| 1484 | 1533 | "Custom Model Tag": "自定义模型标签", |
| @@ -1513,21 +1562,21 @@ | ||
| 1513 | 1562 | "Character Expressions": "角色表情", |
| 1514 | 1563 | "Use the selected API from Chat Translation extension settings.": "使用聊天翻译扩展程序中已选择的API。", |
| 1515 | 1564 | "Translate text to English before classification": "分类之前将文本翻译成英文", |
| 1516 | 1565 | "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.使单个关键词可以有多个表情包。每当出现该关键词时,将随机选择其中一个。", |
| 1517 | 1566 | "Allow multiple sprites per expression": "Allow multiple sprites per expression允许关键词重复", |
| 1518 | 1567 | "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.再次使用相同关键词时,将重新刷新表情包。仅适用于关键词重复的表情包。", |
| 1519 | 1568 | "Re-roll if same expression is used again": "Re-roll if same sprite is used again再次使用相同关键词时刷新表情包。", |
| 1520 | 1569 | "Classifier API": "分类器 API", |
| 1521 | 1570 | "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。", |
| 1522 | 1571 | "Main API": "当前连接的 API", |
| 1523 | 1572 | "WebLLM Extension": "WebLLM 扩展程序", |
| 1524 | 1573 | "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.分类器时,仅显示和使用已分配表情包的关键词。", |
| 1525 | 1574 | "Filter expressions for available sprites": "Filter expressions for available sprites筛选已有表情包的关键词", |
| 1526 | 1575 | "LLM Prompt": "大语言模型提示词", |
| 1527 | 1576 | "Used in addition to JSON schemas and function calling.": "Used in addition to JSON可与 schemasJSON结构 and和 function函数调用 calling.一同使用。", |
| 1528 | 1577 | "LLM Prompt Strategy": "LLM Prompt Strategy提示词策略", |
| 1529 | 1578 | "Limited Context": "Limited Context限制上下文", |
| 1530 | 1579 | "Full Context": "Full Context完整上下文", |
| 1531 | 1580 | "Default / Fallback Expression": "默认/后备表达式", |
| 1532 | 1581 | "Set the default and fallback expression being used when no matching expression is found.": "设置在未找到匹配表达式时使用的默认表达式和后备表达式。", |
| 1533 | 1582 | "Custom Expressions": "自定义表达式", |
| @@ -1640,46 +1689,53 @@ | ||
| 1640 | 1689 | "macro for manual injection)": "宏用于手动注入)", |
| 1641 | 1690 | "Color": "颜色", |
| 1642 | 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 | 1692 | "ext_regex_debugger_active_rules": "激活的规则", |
| 1693 | + "ext_regex_debugger_save_order_help": "保存当前规则顺序", | |
| 1647 | 1694 | "ext_regex_debugger_save_order": "保存此顺序", |
| 1648 | 1695 | "ext_regex_debugger_testing_area": "测试区域", |
| 1649 | 1696 | "ext_regex_debugger_raw_input": "原始输入", |
| 1697 | + "ext_regex_debugger_run_test_help": "运行测试流程", | |
| 1650 | 1698 | "ext_regex_debugger_run_test": "运行测试", |
| 1651 | 1699 | "ext_regex_debugger_display_replace": "替换", |
| 1652 | 1700 | "ext_regex_debugger_display_highlight": "高亮", |
| 1653 | 1701 | "ext_regex_debugger_render_text": "渲染为文本", |
| 1654 | 1702 | "ext_regex_debugger_render_message": "渲染为消息", |
| 1655 | 1703 | "ext_regex_debugger_step_by_step": "逐步转换", |
| 1704 | + "Expand view": "展开视图", | |
| 1656 | 1705 | "ext_regex_debugger_final_output": "最终输出", |
| 1706 | + "Edit Rule": "编辑规则", | |
| 1657 | 1707 | "ext_regex_title": "正则", |
| 1658 | 1708 | "ext_regex_presetsext_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 | 1709 | "ext_regex_new_global_script": "新建全局正则", |
| 1665 | 1710 | "ext_regex_new_scoped_scriptext_regex_new_preset_script_desc": "新建局部正则新增「预设」正则表达式", |
| 1666 | 1711 | "ext_regex_new_preset_script": "新建预设正则", |
| 1712 | + "ext_regex_new_scoped_script_desc": "新增「局部」正则表达式", | |
| 1713 | + "ext_regex_new_scoped_script": "新建局部正则", | |
| 1667 | 1714 | "ext_regex_import_script": "导入正则", |
| 1668 | 1715 | "ext_regex_bulk_edit": "批量编辑", |
| 1669 | 1716 | "ext_regex_debugger_desc": "高级正则调试工具", |
| 1670 | 1717 | "ext_regex_debugger": "调试工具", |
| 1718 | + "ext_regex_move_to_global": "移至全局", | |
| 1719 | + "ext_regex_move_to_preset": "移至预设", | |
| 1720 | + "ext_regex_move_to_scoped": "移至局部", | |
| 1671 | 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 | 1728 | "ext_regex_global_scripts": "全局正则脚本", |
| 1673 | 1729 | "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中", |
| 1674 | 1730 | "No scripts found": "没有找到脚本", |
| 1675 | - "ext_regex_scoped_scripts": "局部正则脚本", | |
| 1676 | - "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中", | |
| 1677 | 1731 | "ext_regex_preset_scripts": "预设正则脚本", |
| 1732 | + "ext_regex_disallow_preset": "不允许使用预设正则", | |
| 1733 | + "ext_regex_allow_preset": "允许使用预设正则", | |
| 1678 | 1734 | "ext_regex_preset_scripts_desc": "只影响当前预设,保存在预设中", |
| 1735 | + "ext_regex_scoped_scripts": "局部正则脚本", | |
| 1679 | 1736 | "ext_regex_disallow_scoped": "不允许使用局部正则", |
| 1680 | 1737 | "ext_regex_allow_scoped": "允许使用局部正则", |
| 1681 | 1738 | "ext_regex_disallow_presetext_regex_scoped_scripts_desc": "不允许使用预设正则只影响当前角色,保存在角色卡片中", |
| 1682 | - "ext_regex_allow_preset": "允许使用预设正则", | |
| 1683 | 1739 | "Regex Editor": "正则表达式编辑器", |
| 1684 | 1740 | "Test Mode": "测试模式", |
| 1685 | 1741 | "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。", |
| @@ -1725,22 +1781,17 @@ | ||
| 1725 | 1781 | "Would you like to allow using them?": "你想要启用它们吗?", |
| 1726 | 1782 | "If you want to do it later, select 'Regex' from the extensions menu.": "你可以稍后在扩展栏的 \"正则\" 区域管理它们。", |
| 1727 | 1783 | "ext_regex_import_target": "导入至:", |
| 1784 | + "This preset has embedded regex script(s).": "此预设包含内置正则脚本。", | |
| 1728 | 1785 | "ext_regex_disable_script": "禁用脚本", |
| 1729 | 1786 | "ext_regex_enable_script": "启用脚本", |
| 1730 | 1787 | "ext_regex_edit_scriptShow more options": "编辑脚本展示更多选项", |
| 1731 | - "ext_regex_move_to_global": "移至全局", | |
| 1732 | - "ext_regex_move_to_scoped": "移至局部", | |
| 1733 | - "ext_regex_move_to_preset": "移至预设", | |
| 1734 | 1788 | "ext_regex_export_script": "导出脚本", |
| 1789 | + "ext_regex_edit_script": "编辑脚本", | |
| 1735 | 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 | 1791 | "Trigger Stable Diffusion": "触发Stable Diffusion", |
| 1742 | 1792 | "Abort current image generation task": "中止当前图像生成", |
| 1743 | 1793 | "Stop Image Generation": "停止图像生成", |
| 1794 | + "Send me a picture of:": "给我发一张……的照片:", | |
| 1744 | 1795 | "sd_Yourself": "你自己", |
| 1745 | 1796 | "sd_Your_Face": "你的脸", |
| 1746 | 1797 | "sd_Me": "我", |
| @@ -1751,8 +1802,8 @@ | ||
| 1751 | 1802 | "Image Generation": "图像生成", |
| 1752 | 1803 | "sd_refine_mode": "允许在将提示词发送到生成 API 之前手动编辑提示词", |
| 1753 | 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 | 1806 | "sd_function_tool_txt": "Use function tool使用函数工具", |
| 1756 | 1807 | "sd_interactive_mode": "发送消息时自动生成图像,例如“给我发一张猫的照片”。", |
| 1757 | 1808 | "sd_interactive_mode_txt": "交互模式", |
| 1758 | 1809 | "sd_multimodal_captioning": "使用多模态字幕根据用户和角色的头像生成提示词。", |
| @@ -1770,35 +1821,45 @@ | ||
| 1770 | 1821 | "sd_auto_auth_warning_2": "注意!服务器必须可从 SillyTavern 主机访问。", |
| 1771 | 1822 | "sd_drawthings_url": "例如:{{drawthings_url}}", |
| 1772 | 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 | 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 | 1831 | "sd_vlad_url": "例如:{{vlad_url}}", |
| 1776 | 1832 | "The server must be accessible from the SillyTavern host machine.": "必须能够从 SillyTavern 主机访问该服务器。", |
| 1777 | 1833 | "Hint: Save an API key in AI Horde API settings to use it here.": "提示:在 Horde AI API 设置中保存一个 API 密钥以便在此处使用它密钥以在此处使用。", |
| 1778 | 1834 | "Allow NSFW images from Horde": "允许来自 Horde 的 NSFW 图片", |
| 1779 | 1835 | "Sanitize prompts (recommended)": "净化提示词(推荐)", |
| 1780 | 1836 | "Automatically adjust generation parameters to ensure free image generations.": "自动调整生成参数,确保图像生成自由。", |
| 1781 | 1837 | "Avoid spending Anlas": "避免花费 Anlas", |
| 1782 | 1838 | "Opus tier": "(作品层Opus 级别)", |
| 1783 | 1839 | "View my Anlas": "查看我的目录查看我的 Anlas", |
| 1840 | + "Hint: Save an API key in the NovelAI API settings to use it here.": "提示:在 NovelAI API 设置中保存一个 API 密钥以在此处使用。", | |
| 1784 | 1841 | "Click to set": "点击设置", |
| 1785 | - "These settings only apply to DALL-E 3": "这些设置仅适用于 DALL-E 3", | |
| 1786 | 1842 | "Image Style": "图像风格", |
| 1787 | - "Image Quality": "画面质量", | |
| 1788 | 1843 | "Standard": "标准", |
| 1789 | 1844 | "HD": "高清", |
| 1845 | + "Duration": "持续时间", | |
| 1846 | + "Short (4 seconds)": "短(4秒)", | |
| 1847 | + "Medium (8 seconds)": "中(8秒)", | |
| 1848 | + "Long (16 seconds)": "长(16秒)", | |
| 1790 | 1849 | "sd_comfy_url": "例如:{{comfy_url}}", |
| 1850 | + "sd_comfy_runpod_url": "eg: https://api.runpod.ai/v2/<your endpoint id>", | |
| 1791 | 1851 | "Open workflow editor": "打开工作流编辑器", |
| 1792 | 1852 | "Create new workflow": "创建新的工作流", |
| 1793 | 1853 | "Delete workflow": "删除工作流", |
| 1854 | + "Enables prompt enhancing (passes prompts through an LLM to add detail).": "允许提示词增强(通过大语言模型处理提示词以添加细节)。", | |
| 1794 | 1855 | "Enhance": "提高", |
| 1795 | 1856 | "You can find your API key in the Stability AI dashboard.": "您可以在 Stability AI 仪表板中找到您的 API 密钥。", |
| 1796 | 1857 | "Style Preset": "风格预设", |
| 1797 | 1858 | "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.": "是否对提示词使用提示词增强(Upsampling)。若开启,则会自动修改提示词,使回复更有创造力。", |
| 1798 | 1859 | "Prompt Upsampling": "提示词增强(Upsampling)", |
| 1860 | + "Duration (Veo)": "持续时间(Veo)", | |
| 1799 | 1861 | "Sampling method": "采样方法", |
| 1800 | 1862 | "Scheduler": "调度器", |
| 1801 | - "Resolution": "分辨率", | |
| 1802 | 1863 | "Upscaler": "图像扩大器", |
| 1803 | 1864 | "Sampling steps": "采样步数", |
| 1804 | 1865 | "Width": "宽度", |
| @@ -1812,15 +1873,15 @@ | ||
| 1812 | 1873 | "Hires. Fix": "高清修复", |
| 1813 | 1874 | "Karras": "Karras", |
| 1814 | 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 | 1877 | "Use ADetailer (Face)": "使用 ADetailer(脸部)", |
| 1817 | 1878 | "SMEA versions of samplers are modified to perform better at high resolution.": "SMEA 版本的采样器经过修改,在高分辨率下性能更佳。", |
| 1818 | 1879 | "SMEA": "中小企业协会", |
| 1819 | 1880 | "DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.": "SMEA 采样器的 DYN 变体通常会产生更加多样化的输出,但在非常高的分辨率下可能会失败。", |
| 1820 | 1881 | "DYN": "动态", |
| 1821 | 1882 | "Decrisper": "去伪器", |
| 1822 | 1883 | "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仅在图像主体形成后启用引导,以提高样本的多样性和饱和度。可能会降低相关性", |
| 1823 | 1884 | "Variety+": "Variety多样性+", |
| 1824 | 1885 | "(-1 for random)": "(“-1”为随机)", |
| 1825 | 1886 | "Preset for prompt prefix and negative prompt": "提示词前缀和负面提示词的预设", |
| 1826 | 1887 | "Style": "风格", |
| @@ -1863,6 +1924,7 @@ | ||
| 1863 | 1924 | "ext_translate_target_lang": "目标语言", |
| 1864 | 1925 | "ext_translate_clear": "清空设置", |
| 1865 | 1926 | "Select TTS Provider": "选择 文本转语音 的服务提供商", |
| 1927 | + "tts_refresh": "刷新", | |
| 1866 | 1928 | "tts_enabled": "已启用", |
| 1867 | 1929 | "Narrate user messages": "朗读用户消息", |
| 1868 | 1930 | "Auto Generation": "自动生成", |
| @@ -1875,18 +1937,22 @@ | ||
| 1875 | 1937 | "Skip codeblocks": "跳过代码块", |
| 1876 | 1938 | "Skip tagged blocks": "跳过标签块里的内容(<标签>跳过这里</标签>)", |
| 1877 | 1939 | "Pass Asterisks to TTS Engine": "将星号传递给文本转语音服务", |
| 1878 | 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.": "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 | 1941 | "Different voices for quotes and text inside asterisks": "Different voices for \"quotes\", 为“引号内文本”、*text inside asterisks星号内文本* and other text使用不同的声音", |
| 1880 | 1942 | "Audio Playback Speed": "音频播放速度", |
| 1943 | + "Available voices": "可用声音", | |
| 1881 | 1944 | "Vector Storage": "向量存储", |
| 1882 | 1945 | "Vectorization Source": "向量化源", |
| 1883 | 1946 | "Local (Transformers)": "本地(Transformers)", |
| 1884 | - "Secondary Embedding endpoint URL": "Secondary Embedding endpoint URL", | |
| 1885 | 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 | 1951 | "Keep model in memory": "将模型保存在内存中", |
| 1887 | 1952 | "Hint: Set the URL in the API connection settings.": "提示:在 API 连接设置中设置 URL。", |
| 1888 | 1953 | "The server MUST be started with the --embedding flag to use this feature!": "服务器必须使用 --embedding 标志启动才能使用此功能!", |
| 1889 | 1954 | "NomicAI API Key": "NomicAI API 密钥", |
| 1955 | + "Hint: Set your OpenRouter API key in API Connections.": "提示:在 API 连接设置中设置 OpenRouter API 密钥。", | |
| 1890 | 1956 | "Query messages": "查询消息", |
| 1891 | 1957 | "Score threshold": "分数阈值", |
| 1892 | 1958 | "Chunk boundary": "区块边界", |
| @@ -2115,106 +2181,16 @@ | ||
| 2115 | 2181 | "World Info:": "世界书:", |
| 2116 | 2182 | "Chat History:": "聊天记录:", |
| 2117 | 2183 | "Extensions:": "扩展程序:", |
| 2118 | 2184 | "Bias:": "Bias:偏置:", |
| 2119 | 2185 | "Total Tokens in Prompt:": "提示词的总Token数量:", |
| 2120 | 2186 | "Max Context": "最大上下文:", |
| 2121 | 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 | 2188 | "Choose what to export": "选择您想要导出什么:", |
| 2213 | 2189 | "Choose what to import": "选择您想要导入什么:", |
| 2214 | 2190 | "If necessary, you can later restore this chat file from the /backups folder": "若需要,您可稍后在 /backups 文件夹中恢复此聊天文件。", |
| 2215 | 2191 | "Also delete the current chat file": "同时删除当前聊天文件", |
| 2216 | 2192 | "Persona Lorebook for": "Persona LorebookTa for的角色世界书:", |
| 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 | 2194 | "Insert prompt": "插入提示词", |
| 2219 | 2195 | "Import a prompt list": "导入提示词列表", |
| 2220 | 2196 | "Export this prompt list": "导出此提示词列表", |
| @@ -2234,13 +2210,21 @@ | ||
| 2234 | 2210 | "Don't forget to save a snapshot of your settings before proceeding.": "在继续之前,不要忘记保存您的设置快照。", |
| 2235 | 2211 | "Enter your password below to confirm:": "输入您的密码以确认:", |
| 2236 | 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 | 2216 | "Here you can toggle the display of individual samplers. (WIP)": "在此可以切换单个采样器的显示。(开发中)", |
| 2238 | 2217 | "Chat ScenarioCharacter Settings Override": "聊天场景覆盖聊天角色设置覆盖", |
| 2239 | 2218 | "Remove": "移除", |
| 2240 | 2219 | "Unique to this chat.": "仅对此聊天生效。", |
| 2241 | 2220 | "All group members will use the following scenario textvalues 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.所有群成员将使用以下值,而不是其角色卡中指定的值。", |
| 2242 | 2221 | "The following scenario textvalues 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.以下值将替代角色卡中设置的值。", |
| 2243 | 2222 | "Checkpoints inherit the scenario overrideoverrides 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.检查点继承其父级的覆盖设置,并可后续单独更改。", |
| 2223 | + "Type Scenario here...": "在此输入场景...", | |
| 2224 | + "Type Example Messages here...": "在此输入示例消息...", | |
| 2225 | + "Prefer Char. Prompt": "优先角色提示词", | |
| 2226 | + "MUST be enabled!": "必须启用!", | |
| 2227 | + "Type System Prompt here...": "在此输入系统提示词...", | |
| 2244 | 2228 | "API:": "API:", |
| 2245 | 2229 | "Key:": "密钥:", |
| 2246 | 2230 | "Add Secret": "添加密钥", |
| @@ -2256,7 +2240,7 @@ | ||
| 2256 | 2240 | "Extra parameters for downloading/HuggingFace API": "下载/HuggingFace API 的额外参数。如果不确定,请将其留空。", |
| 2257 | 2241 | "Revision": "修订", |
| 2258 | 2242 | "Folder Name": "输出文件夹名称", |
| 2259 | 2243 | "HF Token": "HF代币HF 令牌", |
| 2260 | 2244 | "Include Patterns": "包含模式", |
| 2261 | 2245 | "Glob patterns of files to include in the download.": "要包含在下载中的文件的全局模式。每个模式用换行符分隔。", |
| 2262 | 2246 | "Exclude Patterns": "排除模式", |
| @@ -2267,14 +2251,12 @@ | ||
| 2267 | 2251 | "Save your tags to a file": "将标签保存为文件", |
| 2268 | 2252 | "Restore tags from a file": "从文件中恢复标签", |
| 2269 | 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 | 2254 | "Sort mode": "排序模式", |
| 2274 | 2255 | "Manual (Drag & Drop)": "手动 (拖放)", |
| 2275 | 2256 | "Alphabetical (A-Z)": "按字母 (A-Z)", |
| 2276 | 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 | 2260 | "Are you sure you want to delete the theme?": "你确定要删除这个主题吗?", |
| 2279 | 2261 | "Hi,": "嗨,", |
| 2280 | 2262 | "To enable multi-account features, restart the SillyTavern server with": "要启用多帐户功能,请使用以下命令重新启动 SillyTavern 服务器", |
| @@ -2297,7 +2279,7 @@ | ||
| 2297 | 2279 | "Wipe all user data and reset your account to factory settings.": "删除所有用户数据并将您的账号重置为默认设置。", |
| 2298 | 2280 | "Reset Everything": "重置一切", |
| 2299 | 2281 | "This will delete all your settings and data. There will be no undo button. Make sure you have a backup before proceeding.": "这将删除您所有的设置和数据,不可撤销。请确保您已备份数据。", |
| 2300 | 2282 | "Account reset code has been posted to the server console.": "账户重置代码已发布到服务器控制台账户重置代码已发送至服务器控制台。", |
| 2301 | 2283 | "Reset Code:": "重置代码:", |
| 2302 | 2284 | "Want to update?": "获取最新版本", |
| 2303 | 2285 | "How to start chatting?": "如何快速开始聊天?", |
| @@ -8,6 +8,7 @@ import { | ||
| 8 | 8 | Popper, |
| 9 | 9 | initLibraryShims, |
| 10 | 10 | default as libs, |
| 11 | + lodash, | |
| 11 | 12 | } from './lib.js'; |
| 12 | 13 | |
| 13 | 14 | import { 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, | ||
| 188 | 189 | |
| 189 | 190 | import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js'; |
| 190 | 191 | import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, initSlashCommandAutoComplete, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js'; |
| 192 | +import { initMacroAutoComplete } from './scripts/autocomplete/MacroAutoComplete.js'; | |
| 191 | 193 | import { |
| 192 | 194 | tag_map, |
| 193 | 195 | tags, |
| @@ -271,7 +273,7 @@ import { extractReasoningFromData, extractReasoningSignatureFromData, initReason | ||
| 271 | 273 | import { accountStorage } from './scripts/util/AccountStorage.js'; |
| 272 | 274 | import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js'; |
| 273 | 275 | import { initDataMaid } from './scripts/data-maid.js'; |
| 274 | 276 | import { clearItemizedPrompts, deleteItemizedPromptForMessage, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts, swapItemizedPrompts } from './scripts/itemized-prompts.js'; |
| 275 | 277 | import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js'; |
| 276 | 278 | import { event_types, eventSource } from './scripts/events.js'; |
| 277 | 279 | import { initAccessibility } from './scripts/a11y.js'; |
| @@ -282,6 +284,7 @@ import { AudioPlayer } from './scripts/audio-player.js'; | ||
| 282 | 284 | import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js'; |
| 283 | 285 | import { MacroEngine } from './scripts/macros/engine/MacroEngine.js'; |
| 284 | 286 | import { addChatBackupsBrowser } from './scripts/chat-backups.js'; |
| 287 | +import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js'; | |
| 285 | 288 | |
| 286 | 289 | // API OBJECT FOR EXTERNAL WIRING |
| 287 | 290 | globalThis.SillyTavern = { |
| @@ -386,7 +389,7 @@ let chatSaveTimeout; | ||
| 386 | 389 | let importFlashTimeout; |
| 387 | 390 | export let isChatSaving = false; |
| 388 | 391 | let firstRun = false; |
| 389 | 392 | export let settingsReady = false; |
| 390 | 393 | let currentVersion = '0.0.0'; |
| 391 | 394 | export let displayVersion = 'SillyTavern'; |
| 392 | 395 | |
| @@ -701,7 +704,6 @@ async function firstLoadInit() { | ||
| 701 | 704 | initDynamicStyles(); |
| 702 | 705 | initTags(); |
| 703 | 706 | initBookmarks(); |
| 704 | - initMacros(); | |
| 705 | 707 | await getUserAvatars(true, user_avatar); |
| 706 | 708 | await getCharacters(); |
| 707 | 709 | await getBackgrounds(); |
| @@ -710,6 +712,7 @@ async function firstLoadInit() { | ||
| 710 | 712 | initAuthorsNote(); |
| 711 | 713 | await initPersonas(); |
| 712 | 714 | await initSlashCommandAutoComplete(); |
| 715 | + initMacroAutoComplete(); | |
| 713 | 716 | initWorldInfo(); |
| 714 | 717 | initHorde(); |
| 715 | 718 | initRossMods(); |
| @@ -729,6 +732,7 @@ async function firstLoadInit() { | ||
| 729 | 732 | initAccessibility(); |
| 730 | 733 | addDebugFunctions(); |
| 731 | 734 | doDailyExtensionUpdatesCheck(); |
| 735 | + await eventSource.emit(event_types.APP_INITIALIZED); | |
| 732 | 736 | await hideLoader(); |
| 733 | 737 | await fixViewport(); |
| 734 | 738 | await eventSource.emit(event_types.APP_READY); |
| @@ -833,13 +837,14 @@ export async function selectCharacterById(id, { switchMenu = true } = {}) { | ||
| 833 | 837 | if (selected_group || String(this_chid) !== String(id)) { |
| 834 | 838 | //if clicked on a different character from what was currently selected |
| 835 | 839 | if (!is_send_press) { |
| 836 | 840 | await clearChatsetCharacterId(undefined); |
| 837 | 841 | cancelTtsPlaysetCharacterName(''); |
| 838 | 842 | resetSelectedGroup(); |
| 843 | + await clearChat({ clearData: true }); | |
| 844 | + cancelTtsPlay(); | |
| 839 | 845 | this_edit_mes_id = undefined; |
| 840 | 846 | selected_button = 'character_edit'; |
| 841 | 847 | setCharacterId(id); |
| 842 | - chat.length = 0; | |
| 843 | 848 | chat_metadata = {}; |
| 844 | 849 | await getChat(); |
| 845 | 850 | } |
| @@ -952,7 +957,8 @@ export async function printCharacters(fullRefresh = false) { | ||
| 952 | 957 | |
| 953 | 958 | // We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date |
| 954 | 959 | printTagFilters(tag_filter_type.character); |
| 955 | 960 | printTagFilters(tag_filter_type.group_membergroup_members_list); |
| 961 | + printTagFilters(tag_filter_type.group_candidates_list); | |
| 956 | 962 | |
| 957 | 963 | // We are also always reprinting the lists on character/group edit window, as these ones doesn't get updated otherwise |
| 958 | 964 | applyTagsOnCharacterSelect(); |
| @@ -1175,8 +1181,8 @@ export async function getOneCharacter(avatarUrl) { | ||
| 1175 | 1181 | |
| 1176 | 1182 | if (response.ok) { |
| 1177 | 1183 | const getData = await response.json(); |
| 1178 | 1184 | getData['.name'] = DOMPurify.sanitize(getData['.name']); |
| 1179 | 1185 | getData['.chat'] = String(getData['.chat']); |
| 1180 | 1186 | |
| 1181 | 1187 | const indexOf = characters.findIndex(x => x.avatar === avatarUrl); |
| 1182 | 1188 | |
| @@ -1188,7 +1194,7 @@ export async function getOneCharacter(avatarUrl) { | ||
| 1188 | 1194 | } |
| 1189 | 1195 | } |
| 1190 | 1196 | |
| 1191 | 1197 | export function getCharacterSource(chId = this_chid) { |
| 1192 | 1198 | const character = characters[chId]; |
| 1193 | 1199 | |
| 1194 | 1200 | if (!character) { |
| @@ -1247,14 +1253,14 @@ export async function getCharacters() { | ||
| 1247 | 1253 | const getData = await response.json(); |
| 1248 | 1254 | for (let i = 0; i < getData.length; i++) { |
| 1249 | 1255 | characters[i] = getData[i]; |
| 1250 | 1256 | characters[i]['.name'] = DOMPurify.sanitize(characters[i]['.name']); |
| 1251 | 1257 | |
| 1252 | 1258 | // For dropped-in cards |
| 1253 | 1259 | if (!characters[i]['.chat']) { |
| 1254 | 1260 | characters[i]['.chat'] = `${characters[i]['.name']} - ${humanizedDateTime()}`; |
| 1255 | 1261 | } |
| 1256 | 1262 | |
| 1257 | 1263 | characters[i]['.chat'] = String(characters[i]['.chat']); |
| 1258 | 1264 | } |
| 1259 | 1265 | |
| 1260 | 1266 | if (previousAvatar) { |
| @@ -1346,8 +1352,7 @@ export async function deleteCharacterChatByName(characterId, fileName) { | ||
| 1346 | 1352 | } |
| 1347 | 1353 | |
| 1348 | 1354 | export async function replaceCurrentChat() { |
| 1349 | 1355 | await clearChat({ clearData: true }); |
| 1350 | - chat.length = 0; | |
| 1351 | 1356 | |
| 1352 | 1357 | const chatsResponse = await fetch('/api/characters/chats', { |
| 1353 | 1358 | method: 'POST', |
| @@ -1390,18 +1395,26 @@ export async function showMoreMessages(messagesToLoad = null) { | ||
| 1390 | 1395 | |
| 1391 | 1396 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); |
| 1392 | 1397 | const prevHeight = chatElement.prop('scrollHeight'); |
| 1393 | 1398 | const isButtonInViewshowMoreButton = isElementInViewport($('#show_more_messages')[0]); |
| 1394 | - | |
| 1399 | + const isButtonInView = isElementInViewport(showMoreButton[0]); | |
| 1395 | - while (messageId > 0 && count > 0) { | |
| 1400 | + | |
| 1396 | 1401 | let const newMessageIdfirstId = clamp(messageId - 1count, 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 | 1414 | refreshSwipeButtons(); |
| 1402 | 1415 | |
| 1403 | 1416 | if (messageIdfirstId === 0) { |
| 1404 | 1417 | $('#show_more_messages')showMoreButton.remove(); |
| 1405 | 1418 | } |
| 1406 | 1419 | |
| 1407 | 1420 | if (isButtonInView) { |
| @@ -1422,19 +1435,54 @@ export async function printMessages() { | ||
| 1422 | 1435 | chatElement.append('<div id="show_more_messages">Show more messages</div>'); |
| 1423 | 1436 | } |
| 1424 | 1437 | |
| 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 | - } | |
| 1429 | 1439 | |
| 1430 | - chatElement.find('.mes').removeClass('last_mes'); | |
| 1431 | - chatElement.find('.mes').last().addClass('last_mes'); | |
| 1432 | - refreshSwipeButtons(false, false); | |
| 1433 | - applyStylePins(); | |
| 1434 | 1440 | scrollChatToBottom({ waitForFrame: true }); |
| 1435 | 1441 | delay(debounce_timeout.short).then(() => scrollOnMediaLoad()); |
| 1436 | 1442 | } |
| 1437 | 1443 | |
| 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 | + */ | |
| 1451 | +export 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 | + | |
| 1438 | 1486 | export function scrollOnMediaLoad() { |
| 1439 | 1487 | const started = Date.now(); |
| 1440 | 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 | } |
| 1484 | 1532 | |
| 1485 | -export 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 | + */ | |
| 1538 | +export async function clearChat({ clearData = false } = {}) { | |
| 1486 | 1539 | cancelDebouncedChatSave(); |
| 1487 | 1540 | cancelDebouncedMetadataSave(); |
| 1488 | 1541 | closeMessageEditor(); |
| @@ -1499,9 +1552,12 @@ export async function clearChat() { | ||
| 1499 | 1552 | |
| 1500 | 1553 | await saveItemizedPrompts(getCurrentChatId()); |
| 1501 | 1554 | itemizedPrompts.length = 0; |
| 1555 | + | |
| 1556 | + if (clearData) chat.length = 0; | |
| 1502 | 1557 | } |
| 1503 | 1558 | |
| 1504 | 1559 | export async function deleteLastMessage() { |
| 1560 | + deleteItemizedPromptForMessage(chat.length - 1); | |
| 1505 | 1561 | chat.length = chat.length - 1; |
| 1506 | 1562 | chatElement.children('.mes').last().remove(); |
| 1507 | 1563 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); |
| @@ -1554,9 +1610,10 @@ export async function deleteMessage(id, swipeDeletionIndex = undefined, askConfi | ||
| 1554 | 1610 | chat.splice(id, 1); |
| 1555 | 1611 | messageElement.remove(); |
| 1556 | 1612 | |
| 1557 | 1613 | chat_metadata['.tainted'] = true; |
| 1558 | 1614 | |
| 1559 | 1615 | const startIndex = [0, minId].includes(id) ? id : null; |
| 1616 | + deleteItemizedPromptForMessage(id); | |
| 1560 | 1617 | updateViewMessageIds(startIndex); |
| 1561 | 1618 | saveChatDebounced(); |
| 1562 | 1619 | |
| @@ -1569,10 +1626,17 @@ export async function deleteMessage(id, swipeDeletionIndex = undefined, askConfi | ||
| 1569 | 1626 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); |
| 1570 | 1627 | } |
| 1571 | 1628 | |
| 1572 | -export async function reloadCurrentChat() { | |
| 1629 | +export const reloadChatMutex = new SimpleMutex(reloadCurrentChatUnsafe); | |
| 1630 | +export 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 | + */ | |
| 1637 | +export async function reloadCurrentChatUnsafe() { | |
| 1573 | 1638 | preserveNeutralChat(); |
| 1574 | 1639 | await clearChat({ clearData: true }); |
| 1575 | - chat.length = 0; | |
| 1576 | 1640 | |
| 1577 | 1641 | if (selected_group) { |
| 1578 | 1642 | await getGroupChat(selected_group, true); |
| @@ -1610,13 +1674,14 @@ export async function sendTextareaMessage() { | ||
| 1610 | 1674 | // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last |
| 1611 | 1675 | // message was sent from a character (not the user or the system). |
| 1612 | 1676 | const textareaText = String($('#send_textarea').val()); |
| 1677 | + const lastMessage = chat[chat.length - 1]; | |
| 1613 | 1678 | if (power_user.continue_on_send && |
| 1614 | 1679 | !hasPendingFileAttachment() && |
| 1615 | 1680 | !textareaText && |
| 1616 | 1681 | !selected_group && |
| 1617 | 1682 | chat.length && |
| 1618 | 1683 | !chat[chatlastMessage.length - 1]['is_user'] && |
| 1619 | - !chat[chat.length - 1]['is_system'] | |
| 1684 | + !lastMessage.is_system | |
| 1620 | 1685 | ) { |
| 1621 | 1686 | generateType = 'continue'; |
| 1622 | 1687 | } |
| @@ -1637,7 +1702,7 @@ export async function sendTextareaMessage() { | ||
| 1637 | 1702 | * @param {boolean} isSystem If the message was sent by the system |
| 1638 | 1703 | * @param {boolean} isUser If the message was sent by the user |
| 1639 | 1704 | * @param {number} messageId Message index in chat array |
| 1640 | 1705 | * @param {objectPartial<DOMPurify.Config>} [sanitizerOverrides] DOMPurify sanitizer option overrides |
| 1641 | 1706 | * @param {boolean} [isReasoning] If the message is reasoning output |
| 1642 | 1707 | * @returns {string} HTML string |
| 1643 | 1708 | */ |
| @@ -1786,7 +1851,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | ||
| 1786 | 1851 | mes = mes.replace(new RegExp(`(^|\n)${escapeRegex(ch_name)}:`, 'g'), '$1'); |
| 1787 | 1852 | } |
| 1788 | 1853 | |
| 1789 | 1854 | /** @type {import('dompurify')DOMPurify.Config & { RETURN_DOM_FRAGMENT: false; RETURN_DOM: false }} */ |
| 1790 | 1855 | const config = { |
| 1791 | 1856 | RETURN_DOM: false, |
| 1792 | 1857 | RETURN_DOM_FRAGMENT: false, |
| @@ -1810,9 +1875,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | ||
| 1810 | 1875 | * the value in `extra.api`. |
| 1811 | 1876 | * |
| 1812 | 1877 | * @param {JQuery<HTMLElement>} mes - The message element containing the timestamp where the icon should be inserted or replaced. |
| 1813 | 1878 | * @param {ObjectChatMessageExtra} 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 | */ |
| 1817 | 1880 | function insertSVGIcon(mes, extra) { |
| 1818 | 1881 | // Determine the SVG filename |
| @@ -1860,56 +1923,6 @@ function insertSVGIcon(mes, extra) { | ||
| 1860 | 1923 | createModelImage('thinking-icon', '.mes_reasoning_header_title', true); |
| 1861 | 1924 | } |
| 1862 | 1925 | |
| 1863 | - | |
| 1864 | -function 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 | 1927 | * Re-renders a message block with updated content. |
| 1915 | 1928 | * @param {number} messageId Message ID |
| @@ -2382,183 +2395,214 @@ export function addCopyToCodeBlocks(messageElement) { | ||
| 2382 | 2395 | } |
| 2383 | 2396 | } |
| 2384 | 2397 | |
| 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 | + */ | |
| 2406 | +function 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 | + */ | |
| 2423 | +function 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 | +} | |
| 2385 | 2438 | |
| 2386 | 2439 | /** |
| 2387 | 2440 | * Adds a single message to the chat. |
| 2388 | 2441 | * @param {ChatMessage} mes Message object |
| 2389 | 2442 | * @param {object} [options] Options |
| 2390 | 2443 | * @param {string} [options.type=undefined|'normalswipe'] MessageDeprecated. typeUse updateMessageElement instead. |
| 2391 | 2444 | * @param {number} [options.insertAfter=null] Message ID to insert the new message after |
| 2392 | 2445 | * @param {boolean} [options.scroll=true] Whether to scroll to the new message |
| 2393 | 2446 | * @param {number} [options.insertBefore=null] Message ID to insert the new message before |
| 2394 | 2447 | * @param {number} [options.forceId=null] Force the message ID |
| 2395 | 2448 | * @param {boolean} [options.showSwipes=true] Whether to refresh the swipe buttons. |
| 2396 | 2449 | * @returns {voidJQuery<HTMLElement>} The newly added message element |
| 2397 | 2450 | */ |
| 2398 | 2451 | export function addOneMessage(mes, { type = 'normal'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 | 2453 | const momentDatemessageId = timestampToMoment(mes.send_date(); => { |
| 2401 | - const timestamp = momentDate.isValid() ? momentDate.format('LL LT') : ''; | |
| 2454 | + if (typeof forceId === 'number') { | |
| 2455 | + return forceId; | |
| 2456 | + } | |
| 2457 | + if (typeof insertBefore === 'number') { | |
| 2458 | + return insertBefore - 1; | |
| 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; | |
| 2402 | 2471 | |
| 2403 | - if (mes?.extra?.display_text) { | |
| 2472 | + if (type === 'swipe') { | |
| 2404 | - messageText = mes.extra.display_text; | |
| 2473 | + // Forbidden black magic | |
| 2474 | + // This allows to use "continue" on user messages | |
| 2475 | + mes.swipe_id ??= 0; | |
| 2476 | + 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 | + } | |
| 2405 | 2491 | } |
| 2406 | 2492 | |
| 2407 | - // Forbidden black magic | |
| 2493 | + | |
| 2408 | - // This allows to use "continue" on user messages | |
| 2494 | + //last_mes should always be updated. | |
| 2409 | - if (type === 'swipe' && mes.swipe_id === undefined) { | |
| 2495 | + chatElement.find('.mes').removeClass('last_mes'); | |
| 2410 | - mes.swipe_id = 0; | |
| 2496 | + chatElement.find('.mes').last().addClass('last_mes'); | |
| 2411 | - mes.swipes = [mes.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 | } |
| 2413 | 2503 | |
| 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 | + */ | |
| 2518 | +export function updateMessageElement(mes, { messageId = chat.length - 1, messageElement = messageTemplate.clone(), adjustMediaScroll = SCROLL_BEHAVIOR.NONE } = {}) { | |
| 2519 | + | |
| 2414 | 2520 | let avatarImg = getThumbnailUrl('persona', user_avatar); |
| 2415 | - const isSystem = mes.is_system; | |
| 2416 | - const title = mes.title; | |
| 2417 | 2521 | |
| 2418 | 2522 | //for non-user mesagesmessages |
| 2419 | 2523 | if (!mes['.is_user']) { |
| 2420 | 2524 | if (mes.force_avatar) { |
| 2421 | 2525 | avatarImg = mes.force_avatar; |
| 2422 | 2526 | } else if (this_chid === undefined) { |
| 2423 | 2527 | avatarImg = system_avatar; |
| 2528 | + } else if (characters[this_chid] && characters[this_chid].avatar !== 'none') { | |
| 2529 | + avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar); | |
| 2424 | 2530 | } else { |
| 2425 | - if (characters[this_chid].avatar !== 'none') { | |
| 2531 | + avatarImg = default_avatar; | |
| 2426 | - avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar); | |
| 2427 | - } else { | |
| 2428 | - avatarImg = default_avatar; | |
| 2429 | - } | |
| 2430 | 2532 | } |
| 2431 | 2533 | //old processing: |
| 2432 | 2534 | //if messgemessage is from sytemsystem, use the name provided in the message JSONL to proceed, |
| 2433 | 2535 | //if not system message, use name2 (char's name) to proceed |
| 2434 | 2536 | //characterName = mes.is_system || mes.force_avatar ? mes.name : name2; |
| 2435 | 2537 | } else if (mes['.is_user'] && mes['.force_avatar']) { |
| 2436 | 2538 | // Special case for persona images. |
| 2437 | 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 | + }); | |
| 2439 | 2560 | |
| 2440 | - // if mes.extra.uses_system_ui is true, set an override on the sanitizer options | |
| 2561 | + 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); | |
| 2442 | - | |
| 2563 | + 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); | |
| 2474 | 2569 | |
| 2475 | 2570 | if (typemes.extra?.bias !== 'swipe') { |
| 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 | } |
| 2487 | 2574 | |
| 2488 | - // Callers push the new message to chat before calling addOneMessage | |
| 2575 | + updateReasoningUI(messageElement); | |
| 2489 | - const newMessageId = typeof forceId == 'number' ? forceId : chat.length - 1; | |
| 2490 | 2576 | |
| 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 | + } | |
| 2493 | 2580 | |
| 2494 | 2581 | if (mes?.extra?.isSmallSys === true) { |
| 2495 | 2582 | newMessagemessageElement.addClass('smallSysMes'); |
| 2496 | 2583 | } |
| 2497 | 2584 | |
| 2498 | 2585 | if (Array.isArray(mes?.extra?.tool_invocations)) { |
| 2499 | 2586 | newMessagemessageElement.addClass('toolCall'); |
| 2500 | 2587 | } |
| 2501 | 2588 | |
| 2502 | - //shows or hides the Prompt display button | |
| 2589 | + updateMessageItemizedPromptButton(mes, { messageId, messageElement }); | |
| 2503 | - let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId')); | |
| 2504 | 2590 | |
| 2505 | - //if we have itemized messages, and the array isn't null.. | |
| 2591 | + messageElement.find('.avatar img').on('error', function () { | |
| 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 | - } | |
| 2512 | - | |
| 2513 | - newMessage.find('.avatar img').on('error', function () { | |
| 2514 | 2592 | $(this).hide(); |
| 2515 | 2593 | $(this).parent().html('<div class="missing-avatar fa-solid fa-user-slash"></div>'); |
| 2516 | 2594 | }); |
| 2517 | 2595 | |
| 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); | |
| 2542 | 2599 | |
| 2543 | 2600 | // Set the swipes counter for all non-user messages. |
| 2544 | 2601 | if (!paramsmes.isUseris_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 | - } | |
| 2554 | - | |
| 2555 | - // Don't scroll if not inserting last | |
| 2556 | - if (!insertAfter && !insertBefore && scroll) { | |
| 2557 | - scrollChatToBottom({ waitForFrame: true }); | |
| 2558 | 2603 | } |
| 2559 | 2604 | |
| 2560 | - applyCharacterTagsToMessageDivs({ mesIds: newMessageId }); | |
| 2605 | + return messageElement; | |
| 2561 | - updateEditArrowClasses(); | |
| 2562 | 2606 | } |
| 2563 | 2607 | |
| 2564 | 2608 | /** |
| @@ -2703,6 +2747,20 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou | ||
| 2703 | 2747 | }); |
| 2704 | 2748 | } |
| 2705 | 2749 | |
| 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 | 2764 | const environment = {}; |
| 2707 | 2765 | |
| 2708 | 2766 | if (typeof _original === 'string') { |
| @@ -2816,7 +2874,7 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou | ||
| 2816 | 2874 | * @param {string} [options.original] - The original message for {{original}} substitution. |
| 2817 | 2875 | * @param {string} [options.groupOverride] - The group members list for {{group}} substitution. |
| 2818 | 2876 | * @param {boolean} [options.replaceCharacterCard=true] - Whether to replace character card macros. |
| 2819 | 2877 | * @param {Record<string,string|MacroHandler import('./scripts/macros/engine/MacroEnv.types.js').DynamicMacroValue>} [options.dynamicMacros={}] - Additional environment variables as dynamic macros for substitution. Registered as macro functions. |
| 2820 | 2878 | * @param {(x: string) => string} [options.postProcessFn=(x) => x] - Post-processing function for each substituted macro. |
| 2821 | 2879 | * @returns {string} The string with substituted parameters. |
| 2822 | 2880 | */ |
| @@ -3241,7 +3299,7 @@ export function getCharacterCardFieldsLazy({ chid = undefined } = {}) { | ||
| 3241 | 3299 | persona: () => baseChatReplace(power_user.persona_description?.trim()), |
| 3242 | 3300 | system: () => { |
| 3243 | 3301 | if (!character) return ''; |
| 3244 | 3302 | const systemPrompt = chat_metadata['.system_prompt'] || character.data?.system_prompt || ''; |
| 3245 | 3303 | return power_user.prefer_character_prompt ? baseChatReplace(systemPrompt.trim()) : ''; |
| 3246 | 3304 | }, |
| 3247 | 3305 | jailbreak: () => { |
| @@ -3271,13 +3329,13 @@ export function getCharacterCardFieldsLazy({ chid = undefined } = {}) { | ||
| 3271 | 3329 | scenario: () => { |
| 3272 | 3330 | if (groupCardsLazy) return groupCardsLazy.scenario; |
| 3273 | 3331 | if (!character) return ''; |
| 3274 | 3332 | const scenarioText = chat_metadata['.scenario'] || character.scenario || ''; |
| 3275 | 3333 | return baseChatReplace(scenarioText.trim()); |
| 3276 | 3334 | }, |
| 3277 | 3335 | mesExamples: () => { |
| 3278 | 3336 | if (groupCardsLazy) return groupCardsLazy.mesExamples; |
| 3279 | 3337 | if (!character) return ''; |
| 3280 | 3338 | const exampleDialog = chat_metadata['.mes_example'] || character.mes_example || ''; |
| 3281 | 3339 | return baseChatReplace(exampleDialog.trim()); |
| 3282 | 3340 | }, |
| 3283 | 3341 | }; |
| @@ -3492,39 +3550,39 @@ class StreamingProcessor { | ||
| 3492 | 3550 | this.sendTextarea.value = processedText; |
| 3493 | 3551 | this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); |
| 3494 | 3552 | } else { |
| 3495 | 3553 | const mesChanged = chat[messageId]['.mes'] !== processedText; |
| 3496 | 3554 | await this.#checkDomElements(messageId); |
| 3497 | 3555 | this.#updateMessageBlockVisibility(); |
| 3498 | 3556 | const currentTime = new Date(); |
| 3499 | 3557 | chat[messageId]['.mes'] = processedText; |
| 3500 | 3558 | chat[messageId]['.gen_started'] = this.timeStarted; |
| 3501 | 3559 | chat[messageId]['.gen_finished'] = currentTime; |
| 3502 | 3560 | if (!chat[messageId]['.extra']) { |
| 3503 | 3561 | chat[messageId]['.extra'] = {}; |
| 3504 | 3562 | } |
| 3505 | 3563 | chat[messageId]['.extra']['.time_to_first_token'] = this.timeToFirstToken; |
| 3506 | 3564 | |
| 3507 | 3565 | // Update reasoning |
| 3508 | 3566 | await this.reasoningHandler.process(messageId, mesChanged, this.promptReasoning); |
| 3509 | 3567 | processedText = chat[messageId]['.mes']; |
| 3510 | 3568 | |
| 3511 | 3569 | // Token count update. |
| 3512 | 3570 | const tokenCountText = this.reasoningHandler.reasoning + processedText; |
| 3513 | 3571 | const currentTokenCount = isFinal && power_user.message_token_count_enabled ? await getTokenCountAsync(tokenCountText, 0) : 0; |
| 3514 | 3572 | if (currentTokenCount) { |
| 3515 | 3573 | chat[messageId]['.extra']['.token_count'] = currentTokenCount; |
| 3516 | 3574 | if (this.messageTokenCounterDom instanceof HTMLElement) { |
| 3517 | 3575 | this.messageTokenCounterDom.textContent = `${currentTokenCount}t`; |
| 3518 | 3576 | } |
| 3519 | 3577 | } |
| 3520 | 3578 | |
| 3521 | 3579 | if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId]['.swipes'])) { |
| 3522 | 3580 | chat[messageId]['.swipes'][chat[messageId]['.swipe_id']] = processedText; |
| 3523 | 3581 | chat[messageId]['.swipe_info'][chat[messageId]['.swipe_id']] = { |
| 3524 | 3582 | 'send_date': chat[messageId]['.send_date'], |
| 3525 | 3583 | 'gen_started': chat[messageId]['.gen_started'], |
| 3526 | 3584 | 'gen_finished': chat[messageId]['.gen_finished'], |
| 3527 | 3585 | 'extra': structuredClone(chat[messageId]['.extra']), |
| 3528 | 3586 | }; |
| 3529 | 3587 | } |
| 3530 | 3588 | |
| @@ -3633,13 +3691,13 @@ class StreamingProcessor { | ||
| 3633 | 3691 | |
| 3634 | 3692 | setFirstSwipe(messageId) { |
| 3635 | 3693 | if (this.type !== 'swipe' && this.type !== 'impersonate') { |
| 3636 | 3694 | if (Array.isArray(chat[messageId]['.swipes']) && chat[messageId]['.swipes'].length === 1 && chat[messageId]['.swipe_id'] === 0) { |
| 3637 | 3695 | chat[messageId]['.swipes'][0] = chat[messageId]['.mes']; |
| 3638 | 3696 | chat[messageId]['.swipe_info'][0] = { |
| 3639 | 3697 | 'send_date': chat[messageId]['.send_date'], |
| 3640 | 3698 | 'gen_started': chat[messageId]['.gen_started'], |
| 3641 | 3699 | 'gen_finished': chat[messageId]['.gen_finished'], |
| 3642 | 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 | 4140 | const isInstruct = power_user.instruct.enabled && main_api !== 'openai'; |
| 4083 | 4141 | const isImpersonate = type == 'impersonate'; |
| 4084 | 4142 | |
| 4085 | 4143 | if (!(dryRun || depth || type == 'regenerate' || type == 'swipe' || type == 'quiet')) { |
| 4086 | 4144 | const interruptedByCommand = await processCommands(String($('#send_textarea').val())); |
| 4087 | 4145 | |
| 4088 | 4146 | if (interruptedByCommand) { |
| @@ -4119,7 +4177,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4119 | 4177 | // Hide swipes if not in a dry run. |
| 4120 | 4178 | hideSwipeButtons(); |
| 4121 | 4179 | // If generated any message, set the flag to indicate it can't be recreated again. |
| 4122 | 4180 | chat_metadata['.tainted'] = true; |
| 4123 | 4181 | } |
| 4124 | 4182 | |
| 4125 | 4183 | if (selected_group && !is_group_generating) { |
| @@ -4168,17 +4226,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4168 | 4226 | return Promise.resolve(); |
| 4169 | 4227 | } |
| 4170 | 4228 | |
| 4229 | + const lastMessage = chat[chat.length - 1]; | |
| 4230 | + | |
| 4171 | 4231 | let textareaText; |
| 4172 | 4232 | if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun && !depth) { |
| 4173 | 4233 | is_send_press = true; |
| 4174 | 4234 | textareaText = String($('#send_textarea').val()); |
| 4175 | 4235 | $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true })); |
| 4176 | 4236 | } else { |
| 4177 | 4237 | textareaText = ''; |
| 4178 | 4238 | if (chat.length && chat[chatlastMessage.length - 1]['is_user']) { |
| 4179 | 4239 | //do nothing? why does this check exist? |
| 4180 | 4240 | } |
| 4181 | 4241 | else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && !depth && chat.length) { |
| 4242 | + deleteItemizedPromptForMessage(chat.length - 1); | |
| 4182 | 4243 | chat.length = chat.length - 1; |
| 4183 | 4244 | await removeLastMessage(); |
| 4184 | 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 | ||
| 4189 | 4250 | |
| 4190 | 4251 | // Rewrite the generation timer to account for the time passed for all the continuations. |
| 4191 | 4252 | if (isContinue && chat.length) { |
| 4192 | 4253 | const prevFinished = chat[chatlastMessage.length - 1]['gen_finished']; |
| 4193 | 4254 | const prevStarted = chat[chatlastMessage.length - 1]['gen_started']; |
| 4194 | 4255 | |
| 4195 | 4256 | if (prevFinished && prevStarted) { |
| 4196 | 4257 | const timePassed = Number(prevFinished) - Number(prevStarted); |
| 4197 | 4258 | generation_started = new Date(Date.now() - timePassed); |
| 4198 | 4259 | chat[chatlastMessage.length - 1]['gen_started'] = generation_started; |
| 4199 | 4260 | } |
| 4200 | 4261 | } |
| 4201 | 4262 | |
| @@ -4218,7 +4279,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4218 | 4279 | 'continue', |
| 4219 | 4280 | ]; |
| 4220 | 4281 | //for normal messages sent from user.. |
| 4221 | 4282 | if ((textareaText != '' || (hasPendingFileAttachment() && !noAttachTypes.includes(type))) && !automatic_trigger && type !== 'quiet' && !dryRun && !depth) { |
| 4222 | 4283 | // If user message contains no text other than bias - send as a system message |
| 4223 | 4284 | if (messageBias && !removeMacros(textareaText)) { |
| 4224 | 4285 | sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias }); |
| @@ -4227,7 +4288,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4227 | 4288 | await sendMessageAsUser(textareaText, messageBias); |
| 4228 | 4289 | } |
| 4229 | 4290 | } |
| 4230 | 4291 | else if (textareaText == '' && !automatic_trigger && !dryRun && [undefined, 'normal'].includes(type) && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0 && !depth) { |
| 4231 | 4292 | // Use send_if_empty if set and the user message is empty. Only when sending messages normally |
| 4232 | 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 | 4938 | let thisPromptContextSize = await getTokenCountAsync(prompt, power_user.token_padding); |
| 4878 | 4939 | |
| 4879 | 4940 | if (thisPromptContextSize > this_max_context) { //if the prepared prompt is larger than the max context size... |
| 4880 | 4941 | if (count_exm_add > 0) { // ..and we have example mesagesmessages.. |
| 4881 | 4942 | count_exm_add--; // remove the example messages... |
| 4882 | 4943 | await checkPromptSize(); // and try agin... |
| 4883 | 4944 | } 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 | 5178 | chatInjects: injectedIndices?.map(index => arrMes[arrMes.length - index - 1])?.join('') || '', |
| 5118 | 5179 | summarizeString: (extension_prompts['1_memory']?.value || ''), |
| 5119 | 5180 | authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''), |
| 5120 | 5181 | smartContextString: (extension_prompts['.chromadb']?.value || ''), |
| 5121 | 5182 | chatVectorsString: (extension_prompts['3_vectors']?.value || ''), |
| 5122 | 5183 | dataBankVectorsString: (extension_prompts['4_vectors_data_bank']?.value || ''), |
| 5123 | 5184 | worldInfoString: worldInfoString, |
| @@ -5605,7 +5666,7 @@ export function getBiasStrings(textareaText, type) { | ||
| 5605 | 5666 | function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) { |
| 5606 | 5667 | const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR; |
| 5607 | 5668 | const characterName = chatItem?.name ? chatItem.name : name2; |
| 5608 | 5669 | const itemName = chatItem.is_user ? chatItem['.name'] : characterName; |
| 5609 | 5670 | const shouldPrependName = !isNarratorType; |
| 5610 | 5671 | |
| 5611 | 5672 | // If this symbol flag is set, completely ignore the message. |
| @@ -5674,7 +5735,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul | ||
| 5674 | 5735 | await populateFileAttachment(message); |
| 5675 | 5736 | statMesProcess(message, 'user', characters, this_chid, ''); |
| 5676 | 5737 | |
| 5677 | 5738 | chat_metadata['.tainted'] = true; |
| 5678 | 5739 | |
| 5679 | 5740 | if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) { |
| 5680 | 5741 | chat.splice(insertAt, 0, message); |
| @@ -5825,7 +5886,7 @@ function setInContextMessages(msgInContextCount, type) { | ||
| 5825 | 5886 | |
| 5826 | 5887 | // Update last id to chat. No metadata save on purpose, gets hopefully saved via another call |
| 5827 | 5888 | const lastMessageId = Math.max(0, chat.length - msgInContextCount); |
| 5828 | 5889 | chat_metadata['.lastInContextMessageId'] = lastMessageId; |
| 5829 | 5890 | } |
| 5830 | 5891 | |
| 5831 | 5892 | /** |
| @@ -6364,18 +6425,20 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6364 | 6425 | [type, getMessage, fromStreaming, title, swipes, reasoning, imageUrls, reasoningSignature] = arguments; |
| 6365 | 6426 | } |
| 6366 | 6427 | |
| 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 | 6432 | type = 'normal'; |
| 6370 | 6433 | } |
| 6371 | 6434 | |
| 6372 | 6435 | if (chat.length && (!chat[chatlastMessage.length - 1]['extra'] || typeof chat[chatlastMessage.length - 1]['extra'] !== 'object')) { |
| 6373 | 6436 | chat[chatlastMessage.length - 1]['extra'] = {}; |
| 6374 | 6437 | } |
| 6375 | 6438 | |
| 6376 | 6439 | // Coerce null/undefined to empty string |
| 6377 | 6440 | if (chat.length && !chat[chatlastMessage.length - 1]['extra']['.reasoning']) { |
| 6378 | 6441 | chat[chatlastMessage.length - 1]['extra']['.reasoning'] = ''; |
| 6379 | 6442 | } |
| 6380 | 6443 | |
| 6381 | 6444 | if (!reasoning) { |
| @@ -6385,70 +6448,70 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6385 | 6448 | let oldMessage = ''; |
| 6386 | 6449 | const generationFinished = new Date(); |
| 6387 | 6450 | if (type === 'swipe') { |
| 6388 | 6451 | oldMessage = chat[chatlastMessage.length - 1]['mes']; |
| 6389 | 6452 | chat[chatlastMessage.length - 1]['swipes'].length++; |
| 6390 | 6453 | if (chat[chatlastMessage.length - 1]['swipe_id'] === chat[chatlastMessage.length - 1]['swipes'].length - 1) { |
| 6391 | 6454 | chat[chatlastMessage.length - 1]['title'] = title; |
| 6392 | 6455 | chat[chatlastMessage.length - 1]['mes'] = getMessage; |
| 6393 | 6456 | chat[chatlastMessage.length - 1]['gen_started'] = generation_started; |
| 6394 | 6457 | chat[chatlastMessage.length - 1]['gen_finished'] = generationFinished; |
| 6395 | 6458 | chat[chatlastMessage.length - 1]['send_date'] = getMessageTimeStamp(); |
| 6396 | 6459 | chat[chatlastMessage.length - 1]['extra']['.api'] = getGeneratingApi(); |
| 6397 | 6460 | chat[chatlastMessage.length - 1]['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 | 6464 | await processImageAttachment(chat[chat.length - 1]lastMessage, { imageUrls }); |
| 6402 | 6465 | if (power_user.message_token_count_enabled) { |
| 6403 | 6466 | const tokenCountText = (reasoning || '') + chat[chatlastMessage.length - 1]['mes']; |
| 6404 | 6467 | chat[chatlastMessage.length - 1]['extra']['.token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 6405 | 6468 | } |
| 6406 | 6469 | const chat_id = (chat.length - 1); |
| 6407 | 6470 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| 6408 | 6471 | addOneMessage(chat[chat_id], { type: 'swipe' }); |
| 6409 | 6472 | !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); |
| 6410 | 6473 | } else { |
| 6411 | 6474 | chat[chatlastMessage.length - 1]['mes'] = getMessage; |
| 6412 | 6475 | } |
| 6413 | 6476 | } else if (type === 'append' || type === 'continue') { |
| 6414 | 6477 | console.debug('Trying to append.'); |
| 6415 | 6478 | oldMessage = chat[chatlastMessage.length - 1]['mes']; |
| 6416 | 6479 | chat[chatlastMessage.length - 1]['title'] = title; |
| 6417 | 6480 | chat[chatlastMessage.length - 1]['mes'] += getMessage; |
| 6418 | 6481 | chat[chatlastMessage.length - 1]['gen_started'] = generation_started; |
| 6419 | 6482 | chat[chatlastMessage.length - 1]['gen_finished'] = generationFinished; |
| 6420 | 6483 | chat[chatlastMessage.length - 1]['send_date'] = getMessageTimeStamp(); |
| 6421 | 6484 | chat[chatlastMessage.length - 1]['extra']['.api'] = getGeneratingApi(); |
| 6422 | 6485 | chat[chatlastMessage.length - 1]['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 | 6489 | await processImageAttachment(chat[chat.length - 1]lastMessage, { imageUrls }); |
| 6427 | 6490 | if (power_user.message_token_count_enabled) { |
| 6428 | 6491 | const tokenCountText = (reasoning || '') + chat[chatlastMessage.length - 1]['mes']; |
| 6429 | 6492 | chat[chatlastMessage.length - 1]['extra']['.token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 6430 | 6493 | } |
| 6431 | 6494 | const chat_id = (chat.length - 1); |
| 6432 | 6495 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| 6433 | 6496 | addOneMessage(chat[chat_id], { type: 'swipe' }); |
| 6434 | 6497 | !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); |
| 6435 | 6498 | } else if (type === 'appendFinal') { |
| 6436 | 6499 | oldMessage = chat[chatlastMessage.length - 1]['mes']; |
| 6437 | 6500 | console.debug('Trying to appendFinal.'); |
| 6438 | 6501 | chat[chatlastMessage.length - 1]['title'] = title; |
| 6439 | 6502 | chat[chatlastMessage.length - 1]['mes'] = getMessage; |
| 6440 | 6503 | chat[chatlastMessage.length - 1]['gen_started'] = generation_started; |
| 6441 | 6504 | chat[chatlastMessage.length - 1]['gen_finished'] = generationFinished; |
| 6442 | 6505 | chat[chatlastMessage.length - 1]['send_date'] = getMessageTimeStamp(); |
| 6443 | 6506 | chat[chatlastMessage.length - 1]['extra']['.api'] = getGeneratingApi(); |
| 6444 | 6507 | chat[chatlastMessage.length - 1]['extra']['.model'] = getGeneratingModel(); |
| 6445 | 6508 | chat[chatlastMessage.length - 1]['extra']['.reasoning'] += reasoning; |
| 6446 | - chat[chat.length - 1]['extra']['reasoning_signature'] = reasoningSignature; | |
| 6509 | + lastMessage.extra.reasoning_signature = reasoningSignature; | |
| 6447 | 6510 | await processImageAttachment(chat[chat.length - 1]lastMessage, { imageUrls }); |
| 6448 | 6511 | // We don't know if the reasoning duration extended, so we don't update it here on purpose. |
| 6449 | 6512 | if (power_user.message_token_count_enabled) { |
| 6450 | 6513 | const tokenCountText = (reasoning || '') + chat[chatlastMessage.length - 1]['mes']; |
| 6451 | 6514 | chat[chatlastMessage.length - 1]['extra']['.token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 6452 | 6515 | } |
| 6453 | 6516 | const chat_id = (chat.length - 1); |
| 6454 | 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 | ||
| 6457 | 6520 | |
| 6458 | 6521 | } else { |
| 6459 | 6522 | console.debug('entering chat update routine for non-swipe post'); |
| 6460 | 6523 | chat[chat.length]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 | 6534 | if (power_user.trim_spaces) { |
| 6471 | 6535 | getMessage = getMessage.trim(); |
| 6472 | 6536 | } |
| 6473 | 6537 | chat[chatnewMessage.length - 1]['mes'] = getMessage; |
| 6474 | 6538 | chat[chatnewMessage.length - 1]['title'] = title; |
| 6475 | 6539 | chat[chatnewMessage.length - 1]['gen_started'] = generation_started; |
| 6476 | 6540 | chat[chatnewMessage.length - 1]['gen_finished'] = generationFinished; |
| 6477 | 6541 | |
| 6478 | 6542 | if (power_user.message_token_count_enabled) { |
| 6479 | 6543 | const tokenCountText = (reasoning || '') + chat[chatnewMessage.length - 1]['mes']; |
| 6480 | 6544 | chat[chatnewMessage.length - 1]['extra']['.token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 6481 | 6545 | } |
| 6482 | 6546 | |
| 6483 | 6547 | if (selected_group) { |
| @@ -6486,12 +6550,12 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6486 | 6550 | if (characters[this_chid].avatar != 'none') { |
| 6487 | 6551 | avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar); |
| 6488 | 6552 | } |
| 6489 | 6553 | chat[chatnewMessage.length - 1]['force_avatar'] = avatarImg; |
| 6490 | 6554 | chat[chatnewMessage.length - 1]['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 | } |
| 6493 | 6557 | |
| 6494 | 6558 | await processImageAttachment(chat[chat.length - 1]newMessage, { imageUrls }); |
| 6495 | 6559 | const chat_id = (chat.length - 1); |
| 6496 | 6560 | |
| 6497 | 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 | } |
| 6501 | 6565 | |
| 6502 | 6566 | const item = chat[chat.length - 1]; |
| 6503 | 6567 | if (item['.swipe_info'] === undefined) { |
| 6504 | 6568 | item['.swipe_info'] = []; |
| 6505 | 6569 | } |
| 6506 | 6570 | if (item['.swipe_id'] !== undefined) { |
| 6507 | 6571 | const swipeId = item['.swipe_id']; |
| 6508 | 6572 | item['.swipes'][swipeId] = item['.mes']; |
| 6509 | 6573 | item['.swipe_info'][swipeId] = { |
| 6510 | 6574 | send_date: item['.send_date'], |
| 6511 | 6575 | gen_started: item['.gen_started'], |
| 6512 | 6576 | gen_finished: item['.gen_finished'], |
| 6513 | 6577 | extra: structuredClone(item['.extra']), |
| 6514 | 6578 | }; |
| 6515 | 6579 | } else { |
| 6516 | 6580 | item['.swipe_id'] = 0; |
| 6517 | 6581 | item['.swipes'] = []; |
| 6518 | 6582 | item['.swipes'][0] = chat[chatitem.length - 1]['mes']; |
| 6519 | 6583 | item['.swipe_info'][0] = { |
| 6520 | 6584 | send_date: chat[chatitem.length - 1]['send_date'], |
| 6521 | 6585 | gen_started: chat[chatitem.length - 1]['gen_started'], |
| 6522 | 6586 | gen_finished: chat[chatitem.length - 1]['gen_finished'], |
| 6523 | 6587 | extra: structuredClone(chat[chatitem.length - 1]['extra']), |
| 6524 | 6588 | }; |
| 6525 | 6589 | } |
| 6526 | 6590 | |
| @@ -6541,7 +6605,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6541 | 6605 | item.swipe_info.push(...swipeInfoArray); |
| 6542 | 6606 | } |
| 6543 | 6607 | |
| 6544 | 6608 | statMesProcess(chat[chat.length - 1]item, type, characters, this_chid, oldMessage); |
| 6545 | 6609 | return { type, getMessage }; |
| 6546 | 6610 | } |
| 6547 | 6611 | |
| @@ -6644,7 +6708,10 @@ export function syncMesToSwipe(messageId = null) { | ||
| 6644 | 6708 | return false; |
| 6645 | 6709 | } |
| 6646 | 6710 | |
| 6647 | - targetMessage.swipes[targetMessage.swipe_id] = targetMessage.mes; | |
| 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) { | |
| 6713 | + targetMessage.swipes[targetMessage.swipe_id] = targetMessage.mes; | |
| 6714 | + } | |
| 6648 | 6715 | |
| 6649 | 6716 | targetSwipeInfo.send_date = targetMessage.send_date; |
| 6650 | 6717 | targetSwipeInfo.gen_started = targetMessage.gen_started; |
| @@ -7126,7 +7193,7 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false } | ||
| 7126 | 7193 | return; |
| 7127 | 7194 | } |
| 7128 | 7195 | |
| 7129 | 7196 | characters[this_chid]['.date_last_chat'] = Date.now(); |
| 7130 | 7197 | |
| 7131 | 7198 | const trimmedChat = (mesId !== undefined && mesId >= 0 && mesId < chat.length) |
| 7132 | 7199 | ? chat.slice(0, Number(mesId) + 1) |
| @@ -7343,41 +7410,49 @@ export async function unshallowCharacter(characterId) { | ||
| 7343 | 7410 | } |
| 7344 | 7411 | |
| 7345 | 7412 | export async function getChat() { |
| 7346 | - //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name); | |
| 7347 | 7413 | try { |
| 7348 | 7414 | await unshallowCharacter(this_chid); |
| 7349 | 7415 | |
| 7350 | 7416 | const response = await $.ajaxfetch('/api/chats/get', { |
| 7351 | 7417 | typemethod: 'POST', |
| 7352 | - url: '/api/chats/get', | |
| 7418 | + headers: getRequestHeaders(), | |
| 7353 | - data: JSON.stringify({ | |
| 7419 | + cache: 'no-cache', | |
| 7420 | + body: JSON.stringify({ | |
| 7354 | 7421 | ch_name: characters[this_chid].name, |
| 7355 | 7422 | file_name: characters[this_chid].chat, |
| 7356 | 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'] ?? {}; | |
| 7364 | 7426 | |
| 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 | 7437 | chat.forEach(ensureMessageMediaIsArray); |
| 7438 | + } else { | |
| 7439 | + // An empty/corrupted chat file | |
| 7440 | + chat.splice(0, chat.length); | |
| 7441 | + chat_metadata = {}; | |
| 7367 | 7442 | } |
| 7368 | 7443 | if (!chat_metadata['.integrity']) { |
| 7369 | 7444 | chat_metadata['.integrity'] = uuidv4(); |
| 7370 | 7445 | } |
| 7371 | 7446 | await getChatResult(); |
| 7372 | 7447 | eventSource.emit('chatLoaded'event_types.CHAT_LOADED, { detail: { id: this_chid, character: characters[this_chid] } }); |
| 7373 | 7448 | |
| 7374 | 7449 | // Focus on the textarea if not already focused on a visible text input |
| 7375 | - setTimeout(function () { | |
| 7450 | + delay(debounce_timeout.short).then(() => { | |
| 7376 | 7451 | if ($(document.activeElement).is('input:visible, textarea:visible')) { |
| 7377 | 7452 | return; |
| 7378 | 7453 | } |
| 7379 | 7454 | $('#send_textarea').trigger('click').trigger('focus'); |
| 7380 | 7455 | }, 200); |
| 7381 | 7456 | } catch (error) { |
| 7382 | 7457 | await getChatResult(); |
| 7383 | 7458 | console.log(error); |
| @@ -7431,9 +7506,9 @@ function getFirstMessage() { | ||
| 7431 | 7506 | message.mes = swipes[0]; |
| 7432 | 7507 | } |
| 7433 | 7508 | |
| 7434 | 7509 | message['.swipe_id'] = 0; |
| 7435 | 7510 | message['.swipes'] = swipes; |
| 7436 | 7511 | message['.swipe_info'] = swipes.map(_ => ({ |
| 7437 | 7512 | send_date: message.send_date, |
| 7438 | 7513 | gen_started: void 0, |
| 7439 | 7514 | gen_finished: void 0, |
| @@ -7446,9 +7521,8 @@ function getFirstMessage() { | ||
| 7446 | 7521 | |
| 7447 | 7522 | export async function openCharacterChat(file_name) { |
| 7448 | 7523 | await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10); |
| 7449 | 7524 | await clearChat({ clearData: true }); |
| 7450 | 7525 | characters[this_chid]['.chat'] = file_name; |
| 7451 | - chat.length = 0; | |
| 7452 | 7526 | chat_metadata = {}; |
| 7453 | 7527 | await getChat(); |
| 7454 | 7528 | $('#selected_chat_pole').val(file_name); |
| @@ -7717,6 +7791,10 @@ export async function getSettings() { | ||
| 7717 | 7791 | |
| 7718 | 7792 | selected_button = settings.selected_button; |
| 7719 | 7793 | |
| 7794 | + // TODO: Move me into firstLoadInit when experimental toggle is removed | |
| 7795 | + // power_user.experimental_macro_engine | |
| 7796 | + initMacros(); | |
| 7797 | + | |
| 7720 | 7798 | if (data.enable_extensions) { |
| 7721 | 7799 | const enableAutoUpdate = Boolean(data.enable_extensions_auto_update); |
| 7722 | 7800 | const isVersionChanged = settings.currentVersion !== currentVersion; |
| @@ -7833,7 +7911,7 @@ function updateMessage(div) { | ||
| 7833 | 7911 | const mes = chat[mesElement.attr('mesid')]; |
| 7834 | 7912 | |
| 7835 | 7913 | // editing old messages |
| 7836 | 7914 | mes['.extra'] ??= {}; |
| 7837 | 7915 | |
| 7838 | 7916 | let regexPlacement; |
| 7839 | 7917 | if (mes?.is_user) { |
| @@ -7864,10 +7942,10 @@ function updateMessage(div) { | ||
| 7864 | 7942 | if (bias) { |
| 7865 | 7943 | text = removeMacros(text); |
| 7866 | 7944 | } |
| 7867 | 7945 | mes['.mes'] = text; |
| 7868 | 7946 | if (mes['.swipe_id'] !== undefined) { |
| 7869 | 7947 | ensureSwipes(mes); |
| 7870 | 7948 | mes['.swipes'][mes['.swipe_id']] = text; |
| 7871 | 7949 | } |
| 7872 | 7950 | |
| 7873 | 7951 | if (mes?.is_system || mes?.is_user || mes.extra?.type === system_message_types.NARRATOR) { |
| @@ -7876,7 +7954,7 @@ function updateMessage(div) { | ||
| 7876 | 7954 | mes.extra.bias = null; |
| 7877 | 7955 | } |
| 7878 | 7956 | |
| 7879 | 7957 | chat_metadata['.tainted'] = true; |
| 7880 | 7958 | |
| 7881 | 7959 | return { mesBlock, text, mes, bias }; |
| 7882 | 7960 | } |
| @@ -7960,6 +8038,7 @@ export async function messageEdit(editMessageId) { | ||
| 7960 | 8038 | const editTextArea = document.createElement('textarea'); |
| 7961 | 8039 | editTextArea.id = 'curEditTextarea'; |
| 7962 | 8040 | editTextArea.className = 'edit_textarea mdHotkeys'; |
| 8041 | + editTextArea.dataset.macros = ''; | |
| 7963 | 8042 | messageText.append(editTextArea); |
| 7964 | 8043 | |
| 7965 | 8044 | const text = trimSpaces(editMessage.mes || ''); |
| @@ -7990,7 +8069,7 @@ export async function messageEdit(editMessageId) { | ||
| 7990 | 8069 | * @param {number} [messageId=this_edit_mes_id] |
| 7991 | 8070 | */ |
| 7992 | 8071 | async function messageEditCancel(messageId = this_edit_mes_id) { |
| 7993 | 8072 | let text = chat[messageId]['.mes']; |
| 7994 | 8073 | let thisMesDiv; |
| 7995 | 8074 | // If this is the button then select it's parent. Otherwise, select by messageId. |
| 7996 | 8075 | if (this?.classList?.contains('mes_edit_cancel')) { |
| @@ -8076,6 +8155,7 @@ async function messageEditMove(sourceId, targetId) { | ||
| 8076 | 8155 | this_edit_mes_id = targetId; |
| 8077 | 8156 | } |
| 8078 | 8157 | |
| 8158 | + swapItemizedPrompts(sourceId, targetId); | |
| 8079 | 8159 | updateViewMessageIds(); |
| 8080 | 8160 | refreshSwipeButtons(); |
| 8081 | 8161 | await saveChatConditional(); |
| @@ -8089,9 +8169,6 @@ async function messageEditDone(div) { | ||
| 8089 | 8169 | } |
| 8090 | 8170 | |
| 8091 | 8171 | let { mesBlock, text, mes, bias } = updateMessage(div); |
| 8092 | - if (this_edit_mes_id == 0) { | |
| 8093 | - text = substituteParams(text); | |
| 8094 | - } | |
| 8095 | 8172 | |
| 8096 | 8173 | await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id); |
| 8097 | 8174 | text = chat[this_edit_mes_id]?.mes ?? text; |
| @@ -8138,7 +8215,7 @@ async function messageEditDone(div) { | ||
| 8138 | 8215 | export async function getChatsFromFiles(data, isGroupChat) { |
| 8139 | 8216 | const context = getContext(); |
| 8140 | 8217 | let chat_dict = {}; |
| 8141 | 8218 | let chat_list = Object.values(data).sort((a, b) => a['.file_name'].localeCompare(b['.file_name'])).reverse(); |
| 8142 | 8219 | |
| 8143 | 8220 | let chat_promise = chat_list.map(({ file_name }) => { |
| 8144 | 8221 | return new Promise(async (res, rej) => { |
| @@ -8215,7 +8292,7 @@ export async function getPastCharacterChats(characterId = null) { | ||
| 8215 | 8292 | } |
| 8216 | 8293 | |
| 8217 | 8294 | const chats = Object.values(data); |
| 8218 | 8295 | return chats.sort((a, b) => a['.file_name'].localeCompare(b['.file_name'])).reverse(); |
| 8219 | 8296 | } |
| 8220 | 8297 | |
| 8221 | 8298 | /** |
| @@ -8227,9 +8304,9 @@ export function getCurrentChatDetails() { | ||
| 8227 | 8304 | } |
| 8228 | 8305 | |
| 8229 | 8306 | const group = selected_group ? groups.find(x => x.id === selected_group) : null; |
| 8230 | 8307 | const currentChat = selected_group ? group?.chat_id : characters[this_chid]['.chat']; |
| 8231 | 8308 | const displayName = selected_group ? group?.name : characters[this_chid].name; |
| 8232 | 8309 | const avatarImg = selected_group ? group?.avatar_url : getThumbnailUrl('avatar', characters[this_chid]['.avatar']); |
| 8233 | 8310 | return { sessionName: currentChat, group: group, characterName: displayName, avatarImgURL: avatarImg }; |
| 8234 | 8311 | } |
| 8235 | 8312 | |
| @@ -8272,8 +8349,6 @@ export async function displayPastChats(hightlightNames = []) { | ||
| 8272 | 8349 | |
| 8273 | 8350 | async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) { |
| 8274 | 8351 | try { |
| 8275 | - const trimExtension = (fileName) => String(fileName).replace('.jsonl', ''); | |
| 8276 | - | |
| 8277 | 8352 | const response = await fetch('/api/chats/search', { |
| 8278 | 8353 | method: 'POST', |
| 8279 | 8354 | headers: getRequestHeaders(), |
| @@ -8294,7 +8369,7 @@ async function displayChats(searchQuery, currentChat, displayName, avatarImg, se | ||
| 8294 | 8369 | filteredData.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); |
| 8295 | 8370 | |
| 8296 | 8371 | for (const chat of filteredData) { |
| 8297 | 8372 | const isSelected = trimExtension(currentChat) === trimExtension(chat.file_name); |
| 8298 | 8373 | const template = $('#past_chat_template .select_chat_block_wrapper').clone(); |
| 8299 | 8374 | template.find('.select_chat_block').attr('file_name', chat.file_name); |
| 8300 | 8375 | template.find('.avatar img').attr('src', avatarImg); |
| @@ -8693,9 +8768,9 @@ export async function setCharacterSettingsOverrides() { | ||
| 8693 | 8768 | return; |
| 8694 | 8769 | } |
| 8695 | 8770 | |
| 8696 | 8771 | const scenarioOverrideValue = chat_metadata['.scenario'] || ''; |
| 8697 | 8772 | const exampleMessagesValue = chat_metadata['.mes_example'] || ''; |
| 8698 | 8773 | const systemPromptValue = chat_metadata['.system_prompt'] || ''; |
| 8699 | 8774 | const isGroup = !!selected_group; |
| 8700 | 8775 | |
| 8701 | 8776 | const $template = $(await renderTemplateAsync('scenarioOverride')); |
| @@ -8742,9 +8817,9 @@ export async function setCharacterSettingsOverrides() { | ||
| 8742 | 8817 | allowVerticalScrolling: true, |
| 8743 | 8818 | }); |
| 8744 | 8819 | |
| 8745 | 8820 | chat_metadata['.scenario'] = pendingChanges.scenario; |
| 8746 | 8821 | chat_metadata['.mes_example'] = pendingChanges.examples; |
| 8747 | 8822 | chat_metadata['.system_prompt'] = pendingChanges.system_prompt; |
| 8748 | 8823 | await saveMetadata(); |
| 8749 | 8824 | } |
| 8750 | 8825 | |
| @@ -9042,7 +9117,7 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) { | ||
| 9042 | 9117 | // Select the next swipe, or the one before if it was the last one |
| 9043 | 9118 | const newSwipeId = Math.min(swipeId, message.swipes.length - 1); |
| 9044 | 9119 | |
| 9045 | 9120 | chat_metadata['.tainted'] = true; |
| 9046 | 9121 | |
| 9047 | 9122 | messageId = Number(messageId); |
| 9048 | 9123 | swipeId = Number(swipeId); |
| @@ -9399,6 +9474,11 @@ function addAlternateGreeting(template, greeting, index, getArray, popup) { | ||
| 9399 | 9474 | * @param {Event} [e] Event that triggered the function call. |
| 9400 | 9475 | */ |
| 9401 | 9476 | export async function createOrEditCharacter(e) { |
| 9477 | + if (!settingsReady) { | |
| 9478 | + console.warn('Settings not ready, aborting character creation/editing.'); | |
| 9479 | + return; | |
| 9480 | + } | |
| 9481 | + | |
| 9402 | 9482 | $('#rm_info_avatar').html(''); |
| 9403 | 9483 | const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0))); |
| 9404 | 9484 | formData.set('fav', String(fav_ch_checked)); |
| @@ -9556,7 +9636,7 @@ export async function createOrEditCharacter(e) { | ||
| 9556 | 9636 | !isNewChat && |
| 9557 | 9637 | message.mes && |
| 9558 | 9638 | !selected_group && |
| 9559 | 9639 | !chat_metadata['.tainted'] && |
| 9560 | 9640 | (chat.length === 0 || (chat.length === 1 && !chat[0].is_user && !chat[0].is_system)); |
| 9561 | 9641 | |
| 9562 | 9642 | if (shouldRegenerateMessage) { |
| @@ -9576,23 +9656,6 @@ export async function createOrEditCharacter(e) { | ||
| 9576 | 9656 | } |
| 9577 | 9657 | |
| 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 | - */ | |
| 9583 | -export 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 | 9659 | * Formats a counter for a swipe view. |
| 9597 | 9660 | * @param {number} current The current number of items. |
| 9598 | 9661 | * @param {number} total The total number of items. |
| @@ -9669,7 +9732,7 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 9669 | 9732 | console.error(`Message #${mesId}'s DOM element is not valid.`); |
| 9670 | 9733 | return; |
| 9671 | 9734 | } |
| 9672 | 9735 | const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0); |
| 9673 | 9736 | let newSwipeId = Number(forceSwipeId ?? originalSwipeId); |
| 9674 | 9737 | |
| 9675 | 9738 | /** |
| @@ -9716,7 +9779,7 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 9716 | 9779 | } |
| 9717 | 9780 | |
| 9718 | 9781 | //Clamp Id between swipes. |
| 9719 | 9782 | let clampedId = clamp(chat[mesId]['.swipe_id'], 0, Math.max(0, chat[mesId]['.swipes'].length - 1)); |
| 9720 | 9783 | |
| 9721 | 9784 | await updateSwipeCounter(mesId); |
| 9722 | 9785 | //Fallback. |
| @@ -9746,7 +9809,7 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 9746 | 9809 | |
| 9747 | 9810 | //Update the chat. |
| 9748 | 9811 | await loadFromSwipeId(mesId, chat[mesId].swipe_id); |
| 9749 | 9812 | await redisplayChat(chat,{ startIndex: mesId }); |
| 9750 | 9813 | } |
| 9751 | 9814 | else { |
| 9752 | 9815 | await Popup.show.confirm( |
| @@ -9808,7 +9871,7 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 9808 | 9871 | */ |
| 9809 | 9872 | async function loadFromSwipeId(mesId, newSwipeId) { |
| 9810 | 9873 | //Update the swipe_id. |
| 9811 | 9874 | chat[mesId]['.swipe_id'] = newSwipeId; |
| 9812 | 9875 | |
| 9813 | 9876 | clearMessageData(chat[mesId]); |
| 9814 | 9877 | |
| @@ -9880,7 +9943,8 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 9880 | 9943 | return true; |
| 9881 | 9944 | }; |
| 9882 | 9945 | //Wait for the animation's end. https://developer.mozilla.org/en-US/docs/Web/API/Animation/finished |
| 9883 | 9946 | const animationanimations = swipedElementsDiv[0]?.getAnimations().filter((a) => a['animationName'] ==?? 'slide')[0]; |
| 9947 | + const animation = animations.filter((a) => a instanceof globalThis.CSSAnimation && a.animationName == 'slide')[0]; | |
| 9884 | 9948 | try { |
| 9885 | 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 | 9950 | } catch (error) { |
| @@ -9968,7 +10032,7 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 9968 | 10032 | |
| 9969 | 10033 | const tokenCountText = (chat[mesId]?.extra?.reasoning || '') + chat[mesId].mes; |
| 9970 | 10034 | const tokenCount = await getTokenCountAsync(tokenCountText, 0); |
| 9971 | 10035 | chat[mesId]['.extra']['.token_count'] = tokenCount; |
| 9972 | 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 | 10041 | thisMesDiv.css('height', thisMesDivHeight); |
| 9978 | 10042 | expandNewMessage(thisMesDiv); |
| 9979 | 10043 | |
| 9980 | - appendMediaToMessage(chat[mesId], thisMesDiv); | |
| 10044 | + if (run_generate) { | |
| 10045 | + appendMediaToMessage(chat[mesId], thisMesDiv); | |
| 10046 | + } | |
| 9981 | 10047 | |
| 9982 | 10048 | await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId)); |
| 9983 | 10049 | |
| @@ -10007,20 +10073,20 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 10007 | 10073 | // Make sure ad-hoc changes to extras are saved before swiping away |
| 10008 | 10074 | syncMesToSwipe(mesId); |
| 10009 | 10075 | |
| 10010 | 10076 | if (chat[mesId]['.swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array |
| 10011 | 10077 | chat[mesId]['.swipe_id'] = 0; // set it to id 0 |
| 10012 | 10078 | chat[mesId]['.swipes'] = []; // empty the array |
| 10013 | 10079 | chat[mesId]['.swipe_info'] = []; |
| 10014 | 10080 | chat[mesId]['.swipes'][0] = chat[mesId]['.mes']; //assign swipe array with last chat[mesId] from chat |
| 10015 | 10081 | chat[mesId]['.swipe_info'][0] = { |
| 10016 | 10082 | 'send_date': chat[mesId]['.send_date'], |
| 10017 | 10083 | 'gen_started': chat[mesId]['.gen_started'], |
| 10018 | 10084 | 'gen_finished': chat[mesId]['.gen_finished'], |
| 10019 | 10085 | 'extra': structuredClone(chat[mesId]['.extra']), |
| 10020 | 10086 | }; |
| 10021 | 10087 | } |
| 10022 | 10088 | // If the user is holding down the key and we're at the last or first swipe, don't do anything. |
| 10023 | 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 | 10090 | if (source === SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) { |
| 10025 | 10091 | await endSwipe(); |
| 10026 | 10092 | return; |
| @@ -10036,12 +10102,12 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 10036 | 10102 | if (forceSwipeId == null) newSwipeId--; |
| 10037 | 10103 | //Loop to last swipe if negative. |
| 10038 | 10104 | if (newSwipeId < 0) { |
| 10039 | 10105 | newSwipeId = Math.max(0, chat[mesId]['.swipes'].length - 1); |
| 10040 | 10106 | } |
| 10041 | 10107 | //Limit swipe_id to swipes. |
| 10042 | 10108 | if (newSwipeId > chat[mesId]['.swipes'].length - 1) { |
| 10043 | 10109 | toastr.warning(`The swipe_id for message #${mesId} was ${newSwipeId}. It has been reset to ${chat[mesId]['.swipes'].length - 1}.`); |
| 10044 | 10110 | chat[mesId]['.swipe_id'] = chat[mesId]['.swipes'].length - 1; |
| 10045 | 10111 | await endSwipe(); |
| 10046 | 10112 | return; |
| 10047 | 10113 | } |
| @@ -10056,24 +10122,24 @@ export async function swipe(event, direction, { source, repeated, message = chat | ||
| 10056 | 10122 | //Minimum of zero. |
| 10057 | 10123 | if (newSwipeId < 0) { |
| 10058 | 10124 | toastr.warning(`The swipe_id for message #${mesId} was ${newSwipeId}. It has been reset to zero.`); |
| 10059 | 10125 | chat[mesId]['.swipe_id'] = 0; |
| 10060 | 10126 | await endSwipe(); |
| 10061 | 10127 | return; |
| 10062 | 10128 | } |
| 10063 | 10129 | |
| 10064 | 10130 | //If overswiping. |
| 10065 | 10131 | if (newSwipeId >= chat[mesId]['.swipes'].length) { |
| 10066 | 10132 | newSwipeId = chat[mesId]['.swipes'].length; |
| 10067 | 10133 | |
| 10068 | 10134 | //Update the swipe_id. |
| 10069 | 10135 | chat[mesId]['.swipe_id'] = newSwipeId; |
| 10070 | 10136 | |
| 10071 | 10137 | const overswipe = getOverswipeBehavior(mesId); |
| 10072 | 10138 | |
| 10073 | 10139 | //Cancel the generation. |
| 10074 | 10140 | if (overswipe == OVERSWIPE_BEHAVIOR.NONE) { |
| 10075 | 10141 | //Cancel swipe. |
| 10076 | 10142 | chat[mesId]['.swipe_id'] = originalSwipeId; |
| 10077 | 10143 | await endSwipe(); |
| 10078 | 10144 | return; |
| 10079 | 10145 | } |
| @@ -10294,8 +10360,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) { | ||
| 10294 | 10360 | |
| 10295 | 10361 | //Fix it; New chat doesn't create while open create character menu |
| 10296 | 10362 | await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10); |
| 10297 | 10363 | await clearChat({ clearData: true }); |
| 10298 | - chat.length = 0; | |
| 10299 | 10364 | |
| 10300 | 10365 | chat_file_for_del = getCurrentChatDetails()?.sessionName; |
| 10301 | 10366 | |
| @@ -10414,8 +10479,7 @@ export async function renameChat(oldFileName, newName) { | ||
| 10414 | 10479 | export async function closeCurrentChat() { |
| 10415 | 10480 | if (is_send_press == false) { |
| 10416 | 10481 | await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10); |
| 10417 | 10482 | await clearChat({ clearData: true }); |
| 10418 | - chat.length = 0; | |
| 10419 | 10483 | resetSelectedGroup(); |
| 10420 | 10484 | setCharacterId(undefined); |
| 10421 | 10485 | setCharacterName(''); |
| @@ -10946,7 +11010,7 @@ jQuery(async function () { | ||
| 10946 | 11010 | if (group) { |
| 10947 | 11011 | await deleteGroupChat(group, chatFile); |
| 10948 | 11012 | } else { |
| 10949 | 11013 | await delChat(`${chatFile}.jsonl`); |
| 10950 | 11014 | } |
| 10951 | 11015 | |
| 10952 | 11016 | if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view. |
| @@ -10963,18 +11027,18 @@ jQuery(async function () { | ||
| 10963 | 11027 | |
| 10964 | 11028 | $(document).on('click', '.PastChat_cross', async function (e, { fromSlashCommand = false } = {}) { |
| 10965 | 11029 | e.stopPropagation(); |
| 10966 | 11030 | chat_file_for_delconst deleteFileName = $(this).attr('file_name'); |
| 10967 | 11031 | console.debug('detected cross click for' + chat_file_for_deldeleteFileName); |
| 10968 | 11032 | |
| 10969 | 11033 | // Skip confirmation if called from a slash command. |
| 10970 | 11034 | if (fromSlashCommand) { |
| 10971 | 11035 | await handleDeleteChat(chat_file_for_deldeleteFileName, selected_group, true); |
| 10972 | 11036 | return; |
| 10973 | 11037 | } |
| 10974 | 11038 | |
| 10975 | 11039 | const result = await callGenericPopup('<h3>' + t`Delete the Chat File?` + '</h3>', POPUP_TYPE.CONFIRM); |
| 10976 | 11040 | if (result === POPUP_RESULT.AFFIRMATIVE) { |
| 10977 | 11041 | await handleDeleteChat(chat_file_for_deldeleteFileName, selected_group, false); |
| 10978 | 11042 | } |
| 10979 | 11043 | }); |
| 10980 | 11044 | |
| @@ -11008,8 +11072,7 @@ jQuery(async function () { | ||
| 11008 | 11072 | $('#character_popup').css('display', 'none'); |
| 11009 | 11073 | }); |
| 11010 | 11074 | |
| 11011 | 11075 | $('#dialogue_popup_ok').on('click', async function (_e, customData) { |
| 11012 | - const fromSlashCommand = customData?.fromSlashCommand || false; | |
| 11013 | 11076 | dialogueCloseStop = false; |
| 11014 | 11077 | $('#shadow_popup').transition({ |
| 11015 | 11078 | opacity: 0, |
| @@ -11023,10 +11086,6 @@ jQuery(async function () { | ||
| 11023 | 11086 | $('#dialogue_popup').removeClass('wide_dialogue_popup'); |
| 11024 | 11087 | }, animation_duration); |
| 11025 | 11088 | |
| 11026 | - if (popup_type == 'del_chat') { | |
| 11027 | - await handleDeleteChat(chat_file_for_del, selected_group, fromSlashCommand); | |
| 11028 | - } | |
| 11029 | - | |
| 11030 | 11089 | if (dialogueResolve) { |
| 11031 | 11090 | if (popup_type == 'input') { |
| 11032 | 11091 | dialogueResolve($('#dialogue_popup_input').val()); |
| @@ -11139,8 +11198,7 @@ jQuery(async function () { | ||
| 11139 | 11198 | |
| 11140 | 11199 | $(document).on('click', '.renameChatButton', async function (e) { |
| 11141 | 11200 | e.stopPropagation(); |
| 11142 | 11201 | const oldFileNameFulloldFileName = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text(); |
| 11143 | - const oldFileName = oldFileNameFull.replace('.jsonl', ''); | |
| 11144 | 11202 | |
| 11145 | 11203 | const popupText = await renderTemplateAsync('chatRename'); |
| 11146 | 11204 | const newName = await callGenericPopup(popupText, POPUP_TYPE.INPUT, oldFileName); |
| @@ -11161,10 +11219,9 @@ jQuery(async function () { | ||
| 11161 | 11219 | e.stopPropagation(); |
| 11162 | 11220 | const format = $(this).data('format') || 'txt'; |
| 11163 | 11221 | await saveChatConditional(); |
| 11164 | 11222 | const filenamefullfilename = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text(); |
| 11165 | 11223 | console.log(`exporting ${filenamefullfilename} in ${format} format`); |
| 11166 | 11224 | |
| 11167 | - const filename = filenamefull.replace('.jsonl', ''); | |
| 11168 | 11225 | const body = { |
| 11169 | 11226 | is_group: !!selected_group, |
| 11170 | 11227 | avatar_url: characters[this_chid]?.avatar, |
| @@ -11398,10 +11455,13 @@ jQuery(async function () { | ||
| 11398 | 11455 | }); |
| 11399 | 11456 | |
| 11400 | 11457 | if (this_del_mes >= 0) { |
| 11458 | + for (let i = (chat.length - 1); i >= this_del_mes; i--) { | |
| 11459 | + deleteItemizedPromptForMessage(i); | |
| 11460 | + } | |
| 11401 | 11461 | chatElement.find(`.mes[mesid="${this_del_mes}"]`).nextAll('div').remove(); |
| 11402 | 11462 | chatElement.find(`.mes[mesid="${this_del_mes}"]`).remove(); |
| 11403 | 11463 | chat.length = this_del_mes; |
| 11404 | 11464 | chat_metadata['.tainted'] = true; |
| 11405 | 11465 | await saveChatConditional(); |
| 11406 | 11466 | chatElement.scrollTop(chatElement[0].scrollHeight); |
| 11407 | 11467 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); |
| @@ -11488,7 +11548,7 @@ jQuery(async function () { | ||
| 11488 | 11548 | if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) { |
| 11489 | 11549 | try { |
| 11490 | 11550 | const messageId = $(this).closest('.mes').attr('mesid'); |
| 11491 | 11551 | const text = chat[messageId]['.mes']; |
| 11492 | 11552 | await copyText(text); |
| 11493 | 11553 | toastr.info('Copied!', '', { timeOut: 2000 }); |
| 11494 | 11554 | } catch (err) { |
| @@ -11515,8 +11575,8 @@ jQuery(async function () { | ||
| 11515 | 11575 | let mes_edited = chatElement.find(`[mesid="${this_edit_mes_id}"]`).find('.mes_edit_done'); |
| 11516 | 11576 | if (Number(edit_mes_id) == chat.length - 1) { //if the generating swipe (...) |
| 11517 | 11577 | let run_edit = true; |
| 11518 | 11578 | if (chat[edit_mes_id]['.swipe_id'] !== undefined) { |
| 11519 | 11579 | if (chat[edit_mes_id]['.swipes'].length === chat[edit_mes_id]['.swipe_id']) { |
| 11520 | 11580 | run_edit = false; |
| 11521 | 11581 | } |
| 11522 | 11582 | } |
| @@ -11637,14 +11697,16 @@ jQuery(async function () { | ||
| 11637 | 11697 | const oldScroll = chatElement[0].scrollTop; |
| 11638 | 11698 | const clone = structuredClone(chat[this_edit_mes_id]); |
| 11639 | 11699 | clone.send_date = Date.now(); |
| 11640 | 11700 | clone.mesconst this_edit_mes_element = $(this).closest('.mes').find('.edit_textarea').val().toString(); |
| 11701 | + clone.mes = this_edit_mes_element.find('.edit_textarea').val().toString(); | |
| 11641 | 11702 | |
| 11642 | 11703 | if (power_user.trim_spaces) { |
| 11643 | 11704 | clone.mes = clone.mes.trim(); |
| 11644 | 11705 | } |
| 11645 | 11706 | |
| 11646 | 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); | |
| 11648 | 11710 | |
| 11649 | 11711 | updateViewMessageIds(); |
| 11650 | 11712 | await saveChatConditional(); |
| @@ -11655,8 +11717,8 @@ jQuery(async function () { | ||
| 11655 | 11717 | $(document).on('click', '.mes_edit_delete', async function (event, customData) { |
| 11656 | 11718 | const fromSlashCommand = customData?.fromSlashCommand || false; |
| 11657 | 11719 | const message = chat[this_edit_mes_id]; |
| 11658 | 11720 | const selectedSwipe = message['.swipe_id'] ?? undefined; |
| 11659 | 11721 | const swipesArray = Array.isArray(message['.swipes']) ? message['.swipes'] : []; |
| 11660 | 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 | 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 | 12075 | if (this_edit_mes_id === undefined && $('#mes_stop').is(':visible')) { |
| 12014 | 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 | 12080 | $('.last_mes .swipe_left').trigger('click'); |
| 12017 | 12081 | } |
| 12018 | 12082 | } |
| @@ -12071,7 +12135,7 @@ jQuery(async function () { | ||
| 12071 | 12135 | }); |
| 12072 | 12136 | |
| 12073 | 12137 | // Remember the chat currently selected, so we can reload it after the replacement |
| 12074 | 12138 | const currentChatFile = characters[this_chid]['.chat']; |
| 12075 | 12139 | async function postReplace() { |
| 12076 | 12140 | await openCharacterChat(currentChatFile); |
| 12077 | 12141 | } |
| @@ -765,7 +765,7 @@ class PromptManager { | ||
| 765 | 765 | eventSource.on(event_types.CHATCOMPLETION_MODEL_CHANGED, () => this.renderDebounced()); |
| 766 | 766 | |
| 767 | 767 | // Re-render when the character changes. |
| 768 | 768 | eventSource.on('chatLoaded'event_types.CHAT_LOADED, (event) => { |
| 769 | 769 | this.handleCharacterSelected(event); |
| 770 | 770 | this.saveServiceSettings().then(() => this.renderDebounced()); |
| 771 | 771 | }); |
| @@ -408,7 +408,7 @@ function RA_autoconnect(PrevApi) { | ||
| 408 | 408 | || (secret_state[SECRET_KEYS.FIREWORKS] && oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS) |
| 409 | 409 | || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) |
| 410 | 410 | || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI) |
| 411 | 411 | || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) |
| 412 | 412 | || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) |
| 413 | 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 | } |
| 997 | 997 | |
| 998 | 998 | //Enter to send when send_textarea in focus |
| 999 | 999 | if (document.activeElement == hotkeyTargets['.send_textarea']) { |
| 1000 | 1000 | const sendOnEnter = shouldSendOnEnter(); |
| 1001 | 1001 | if (!event.isComposing && !event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) { |
| 1002 | 1002 | event.preventDefault(); |
| @@ -1004,7 +1004,7 @@ export function initRossMods() { | ||
| 1004 | 1004 | return; |
| 1005 | 1005 | } |
| 1006 | 1006 | } |
| 1007 | 1007 | if (document.activeElement == hotkeyTargets['.dialogue_popup_input'] && !isMobile()) { |
| 1008 | 1008 | if (!event.shiftKey && !event.ctrlKey && event.key == 'Enter') { |
| 1009 | 1009 | event.preventDefault(); |
| 1010 | 1010 | $('#dialogue_popup_ok').trigger('click'); |
| @@ -1139,7 +1139,7 @@ export function initRossMods() { | ||
| 1139 | 1139 | |
| 1140 | 1140 | if (event.ctrlKey && event.key == 'ArrowUp') { //edits last USER message if chatbar is empty and focused |
| 1141 | 1141 | if ( |
| 1142 | 1142 | hotkeyTargets['.send_textarea'].value === '' && |
| 1143 | 1143 | chatbarInFocus === true && |
| 1144 | 1144 | ($('.swipe_right:last').css('display') === 'flex' || $('.last_mes').attr('is_system') === 'true') && |
| 1145 | 1145 | $('#character_popup').css('display') === 'none' && |
| @@ -1158,7 +1158,7 @@ export function initRossMods() { | ||
| 1158 | 1158 | if (event.key == 'ArrowUp') { //edits last message if chatbar is empty and focused |
| 1159 | 1159 | console.log('got uparrow input'); |
| 1160 | 1160 | if ( |
| 1161 | 1161 | hotkeyTargets['.send_textarea'].value === '' && |
| 1162 | 1162 | chatbarInFocus === true && |
| 1163 | 1163 | //$('.swipe_right:last').css('display') === 'flex' && |
| 1164 | 1164 | $('.last_mes .mes_buttons').is(':visible') && |
| @@ -594,7 +594,7 @@ function registerAuthorsNoteMacros() { | ||
| 594 | 594 | handler: () => chat_metadata[metadata_keys.prompt] ?? '', |
| 595 | 595 | }); |
| 596 | 596 | macros.register('charAuthorsNote', { |
| 597 | 597 | category: MacroCategory.CHARACTERPROMPTS, |
| 598 | 598 | description: t`The contents of the Character Author's Note`, |
| 599 | 599 | handler: () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '', |
| 600 | 600 | }); |
| @@ -155,6 +155,10 @@ export class AutoComplete { | ||
| 155 | 155 | */ |
| 156 | 156 | updateName(item) { |
| 157 | 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 | 162 | switch (this.matchType) { |
| 159 | 163 | case 'strict': { |
| 160 | 164 | chars.forEach((it, idx) => { |
| @@ -275,6 +279,7 @@ export class AutoComplete { | ||
| 275 | 279 | //TODO check if isInput and isForced are both required |
| 276 | 280 | this.text = this.textarea.value; |
| 277 | 281 | this.isReplaceable = false; |
| 282 | + this.isShowForced = isForced; // Store forced state for checkIfActivate to access | |
| 278 | 283 | |
| 279 | 284 | if (document.activeElement != this.textarea) { |
| 280 | 285 | // only show with textarea in focus |
| @@ -311,8 +316,8 @@ export class AutoComplete { | ||
| 311 | 316 | this.name = this.parserResult.name.toLowerCase() ?? ''; |
| 312 | 317 | |
| 313 | 318 | const isCursorInNamePart = this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0); |
| 314 | 319 | if (isForced || isInput || isSelect) { |
| 315 | 320 | // if forced (ctrl+space) or user input or just selected an option... |
| 316 | 321 | if (isCursorInNamePart) { |
| 317 | 322 | // ...and cursor is somewhere in the name part (including right behind the final char) |
| 318 | 323 | // -> show autocomplete for the (partial if cursor in the middle) name |
| @@ -393,8 +398,20 @@ export class AutoComplete { | ||
| 393 | 398 | this.updateName(option); |
| 394 | 399 | return option; |
| 395 | 400 | }) |
| 396 | 401 | // 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 | + }); | |
| 398 | 415 | |
| 399 | 416 | |
| 400 | 417 | |
| @@ -430,7 +447,7 @@ export class AutoComplete { | ||
| 430 | 447 | } else if (!this.isReplaceable && this.result.length > 1) { |
| 431 | 448 | return this.hide(); |
| 432 | 449 | } |
| 433 | 450 | this.selectedItem = this.selectDefaultItem(this.result[0]); |
| 434 | 451 | this.isActive = true; |
| 435 | 452 | this.wasForced = isForced; |
| 436 | 453 | this.renderDebounced(); |
| @@ -588,7 +605,22 @@ export class AutoComplete { | ||
| 588 | 605 | if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) { |
| 589 | 606 | return this.hide(); |
| 590 | 607 | } |
| 591 | 608 | constlet 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 | 624 | this.detailsWrap.style.setProperty('--targetOffset', `${left}`); |
| 593 | 625 | if (this.isReplaceable) { |
| 594 | 626 | this.detailsWrap.classList.remove('full'); |
| @@ -680,8 +712,10 @@ export class AutoComplete { | ||
| 680 | 712 | */ |
| 681 | 713 | async select() { |
| 682 | 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 | 716 | this.textarea.selectionStartconst effectiveStart = this.effectiveParserResult.start + (this.selectedItem.replacer.lengthreplacementStartOffset ?? 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 | 719 | this.textarea.selectionEnd = this.textarea.selectionStart; |
| 686 | 720 | this.show(false, false, true); |
| 687 | 721 | } else { |
| @@ -697,6 +731,24 @@ export class AutoComplete { | ||
| 697 | 731 | |
| 698 | 732 | |
| 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 | 752 | * Mark the item at newIdx in the autocomplete list as selected. |
| 701 | 753 | * @param {number} newIdx |
| 702 | 754 | */ |
| @@ -24,7 +24,7 @@ export class AutoCompleteNameResultBase { | ||
| 24 | 24 | this.start = start; |
| 25 | 25 | this.optionList = optionList; |
| 26 | 26 | this.canBeQuoted = canBeQuoted; |
| 27 | 27 | this.noMatchText =if (makeNoMatchText ??) this.makeNoMatchText = makeNoMatchText; |
| 28 | 28 | this.noOptionstext =if (makeNoOptionsText ??) this.makeNoOptionsText = makeNoOptionsText; |
| 29 | 29 | } |
| 30 | 30 | } |
| @@ -13,6 +13,22 @@ export class AutoCompleteOption { | ||
| 13 | 13 | /** @type {(input:string)=>boolean} */ matchProvider; |
| 14 | 14 | /** @type {(input:string)=>string} */ valueProvider; |
| 15 | 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; | |
| 16 | 32 | |
| 17 | 33 | |
| 18 | 34 | /** |
| @@ -9,8 +9,11 @@ import { | ||
| 9 | 9 | createSourceIndicator, |
| 10 | 10 | createAliasIndicator, |
| 11 | 11 | renderMacroDetails, |
| 12 | 12 | } from '../macros/engine/MacroBrowser.js'; |
| 13 | 13 | import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | +import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js'; | |
| 15 | +import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js'; | |
| 16 | +import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js'; | |
| 14 | 17 | |
| 15 | 18 | /** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */ |
| 16 | 19 | |
| @@ -19,9 +22,46 @@ import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js' | ||
| 19 | 22 | * @typedef {Object} MacroAutoCompleteContext |
| 20 | 23 | * @property {string} fullText - The full macro text being typed (without {{ }}). |
| 21 | 24 | * @property {number} cursorOffset - Cursor position within the macro text. |
| 25 | + * @property {string} paddingBefore - Padding before the macro identifier/flags. | |
| 22 | 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 | 31 | * @property {string[]} args - Array of arguments typed so far. |
| 24 | 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 | */ |
| 26 | 66 | |
| 27 | 67 | export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { |
| @@ -31,17 +71,62 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | ||
| 31 | 71 | /** @type {MacroAutoCompleteContext|null} */ |
| 32 | 72 | #context = null; |
| 33 | 73 | |
| 74 | + /** @type {EnhancedMacroAutoCompleteOptions|null} */ | |
| 75 | + #options = null; | |
| 76 | + | |
| 77 | + /** @type {boolean} */ | |
| 78 | + #noBraces = false; | |
| 79 | + | |
| 80 | + /** @type {string} */ | |
| 81 | + #paddingAfter = ''; | |
| 82 | + | |
| 34 | 83 | /** |
| 35 | 84 | * @param {MacroDefinition} macro - The macro definition from MacroRegistry. |
| 36 | 85 | * @param {MacroAutoCompleteContext|EnhancedMacroAutoCompleteOptions|null} [contextcontextOrOptions] - Optional contextContext for argument hints, or options object. |
| 37 | 86 | */ |
| 38 | 87 | constructor(macro, contextcontextOrOptions = null) { |
| 39 | 88 | // Use the macro name as the autocomplete key |
| 40 | 89 | super(macro.name, enumIcons.macro); |
| 41 | 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 | 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 | } |
| 46 | 131 | |
| 47 | 132 | /** @returns {MacroDefinition} */ |
| @@ -74,8 +159,9 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | ||
| 74 | 159 | const nameEl = document.createElement('span'); |
| 75 | 160 | nameEl.classList.add('name', 'monospace'); |
| 76 | 161 | |
| 77 | 162 | // Build signature with individual character spans (includes {{ }}) |
| 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 | 165 | for (const char of sigText) { |
| 80 | 166 | const span = document.createElement('span'); |
| 81 | 167 | span.textContent = char; |
| @@ -121,17 +207,36 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | ||
| 121 | 207 | renderDetails() { |
| 122 | 208 | const frag = document.createDocumentFragment(); |
| 123 | 209 | |
| 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 | 223 | // Determine current argument index for highlighting |
| 125 | 224 | const currentArgIndex = this.#context?.currentArgIndex ?? -1; |
| 126 | 225 | |
| 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 | 232 | // Render argument hint banner if we're typing an argument |
| 128 | 233 | if (hightlightArgsHint && currentArgIndex >= 0) { |
| 129 | 234 | const hint = this.#renderArgumentHint(); |
| 130 | 235 | if (hint) frag.append(hint); |
| 131 | 236 | } |
| 132 | 237 | |
| 133 | 238 | // Reuse MacroBrowser's renderMacroDetails with options |
| 134 | 239 | const details = renderMacroDetails(this.#macro, { currentArgIndex: hightlightArgsHint ? currentArgIndex : -1 }); |
| 135 | 240 | |
| 136 | 241 | // Add class for autocomplete-specific styling overrides |
| 137 | 242 | details.classList.add('macro-ac-details'); |
| @@ -141,6 +246,113 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | ||
| 141 | 246 | } |
| 142 | 247 | |
| 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 | 356 | * Renders the current argument hint banner. |
| 145 | 357 | * @returns {HTMLElement|null} |
| 146 | 358 | */ |
| @@ -163,8 +375,23 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | ||
| 163 | 375 | if (isListArg) { |
| 164 | 376 | // List argument hint |
| 165 | 377 | const listIndex = argIndex - this.#macro.maxArgs + 1; |
| 378 | + const totalListItems = this.#context.args.length - this.#macro.maxArgs; | |
| 379 | + | |
| 166 | 380 | const text = document.createElement('span'); |
| 167 | 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 | 395 | hint.append(text); |
| 169 | 396 | } else { |
| 170 | 397 | // Unnamed argument hint (required or optional) |
| @@ -210,51 +437,1436 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | ||
| 210 | 437 | } |
| 211 | 438 | |
| 212 | 439 | /** |
| 440 | + * Autocomplete option for macro execution flags. | |
| 441 | + * Shows flag symbol, name, and description. | |
| 442 | + * Uses default AutoCompleteOption rendering for consistent styling. | |
| 443 | + */ | |
| 444 | +export class MacroFlagAutoCompleteOption extends AutoCompleteOption { | |
| 445 | + /** @type {import('../macros/engine/MacroFlags.js').MacroFlagDefinition} */ | |
| 446 | + #flagDef; | |
| 447 | + | |
| 448 | + /** | |
| 449 | + * @param {import('../macros/engine/MacroFlags.js').MacroFlagDefinition} flagDef - The flag definition. | |
| 450 | + */ | |
| 451 | + constructor(flagDef) { | |
| 452 | + // Use the flag symbol as the name, with a flag icon | |
| 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 | + */ | |
| 530 | +export 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 | + */ | |
| 549 | +export 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 | + */ | |
| 568 | +export 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 | + */ | |
| 577 | +const 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 | + */ | |
| 584 | +export 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 | + */ | |
| 594 | +export 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 | + */ | |
| 709 | +export 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`; | |
| 768 | + } else { | |
| 769 | + description = `${scopeLabel} variable`; | |
| 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'); | |
| 788 | + } | |
| 789 | + return li; | |
| 790 | + } | |
| 791 | + | |
| 792 | + /** | |
| 793 | + * Renders the details panel for this variable. | |
| 794 | + * @returns {DocumentFragment} | |
| 795 | + */ | |
| 796 | + renderDetails() { | |
| 797 | + const frag = document.createDocumentFragment(); | |
| 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; | |
| 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}`; | |
| 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); | |
| 854 | + | |
| 855 | + const usageList = document.createElement('ul'); | |
| 856 | + const examples = [ | |
| 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); | |
| 878 | + } | |
| 879 | + details.append(usageList); | |
| 880 | + | |
| 881 | + frag.append(details); | |
| 882 | + return frag; | |
| 883 | + } | |
| 884 | +} | |
| 885 | + | |
| 886 | +/** | |
| 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 | + */ | |
| 892 | +function 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 | + */ | |
| 902 | +export 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 | + */ | |
| 999 | +export 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 | + */ | |
| 1071 | +export 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 | + */ | |
| 1155 | +export 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 | +/** | |
| 213 | 1321 | * Parses the macro text to determine current argument context. |
| 214 | - * @param {string} macroText - The text inside {{ }}, e.g., "roll::1d20" or "random::a::b". | |
| 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 ". | |
| 215 | 1325 | * @param {number} cursorOffset - Cursor position within macroText. |
| 216 | 1326 | * @returns {MacroAutoCompleteContext} |
| 217 | 1327 | */ |
| 218 | 1328 | export function parseMacroContext(macroText, cursorOffset) { |
| 219 | - const parts = []; | |
| 220 | - let currentPart = ''; | |
| 221 | - let partStart = 0; | |
| 222 | 1329 | let i = 0; |
| 223 | 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) | |
| 224 | 1341 | while (i < macroText.length) { |
| 225 | - if (macroText[i] === ':' && macroText[i + 1] === ':') { | |
| 1342 | + const char = macroText[i]; | |
| 226 | - parts.push({ text: currentPart, start: partStart, end: i }); | |
| 1343 | + // Check if this looks like a closing tag: `/` followed by an identifier character | |
| 227 | - currentPart = ''; | |
| 1344 | + if (char === '/' && i + 1 < macroText.length && /[a-zA-Z/]/.test(macroText[i + 1])) { | |
| 228 | - i += 2; | |
| 1345 | + // This is a closing tag identifier, not a flag - stop parsing flags | |
| 229 | - partStart = i; | |
| 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 | + } | |
| 230 | 1356 | } else { |
| 231 | - currentPart += macroText[i]; | |
| 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])) { | |
| 232 | 1408 | i++; |
| 233 | 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 | + }; | |
| 234 | 1584 | } |
| 235 | - // Push the last part | |
| 1585 | + | |
| 236 | - parts.push({ text: currentPart, start: partStart, end: macroText.length }); | |
| 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; | |
| 237 | 1688 | |
| 238 | 1689 | // Determine which part the cursor is in |
| 239 | 1690 | let currentArgIndex = -1; |
| 240 | - for (let idx = 0; idx < parts.length; idx++) { | |
| 1691 | + | |
| 241 | - const part = parts[idx]; | |
| 1692 | + // Only consider being in an argument if we've passed a separator | |
| 242 | - if (cursorOffset >= part.start && cursorOffset <= part.end) { | |
| 1693 | + if (separatorPositions.length > 0) { | |
| 243 | - currentArgIndex = idx - 1; // -1 because first part is identifier | |
| 1694 | + // Find which argument we're in based on separator positions | |
| 244 | - break; | |
| 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 | + } | |
| 245 | 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; | |
| 246 | 1726 | } |
| 247 | 1727 | |
| 248 | - // If cursor is after all parts (at the end), we're in the last arg | |
| 1728 | + // Build args array - include space-separated arg if present | |
| 249 | - if (currentArgIndex === -1 && cursorOffset >= parts[parts.length - 1].end) { | |
| 1729 | + // Trim args like the macro engine does | |
| 250 | - currentArgIndex = parts.length - 1; | |
| 1730 | + let args = parts.slice(1).map(p => p.text.trim()); | |
| 1731 | + if (spaceArgText.length > 0) { | |
| 1732 | + args = [spaceArgText, ...args]; | |
| 251 | 1733 | } |
| 252 | 1734 | |
| 253 | 1735 | return { |
| 254 | 1736 | fullText: macroText, |
| 255 | 1737 | cursorOffset, |
| 256 | - identifier: parts[0]?.text.trim() || '', | |
| 1738 | + paddingBefore: leftPadding, | |
| 257 | - args: parts.slice(1).map(p => p.text), | |
| 1739 | + identifier: cleanIdentifier, | |
| 1740 | + identifierStart: identifierStartPos, | |
| 1741 | + isInFlagsArea, | |
| 1742 | + flags, | |
| 1743 | + currentFlag, | |
| 1744 | + args, | |
| 258 | 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, | |
| 259 | 1762 | }; |
| 260 | 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 | + */ | |
| 1771 | +export 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 | + } | |
| 1872 | +} | |
| @@ -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 | + | |
| 14 | +import { power_user } from '../power-user.js'; | |
| 15 | +import { AutoComplete, AUTOCOMPLETE_STATE } from './AutoComplete.js'; | |
| 16 | +import { findMacroAtCursor, findUnclosedScopes, getMacroAutoCompleteAt } from './MacroAutoCompleteHelper.js'; | |
| 17 | + | |
| 18 | +/** Custom attribute name used to mark elements that support macro autocomplete */ | |
| 19 | +export const MACRO_AUTOCOMPLETE_ATTRIBUTE = 'data-macros'; | |
| 20 | + | |
| 21 | +/** Attribute to control autocomplete visibility: 'always' (force show) or 'hide' (never show) */ | |
| 22 | +export const MACRO_AUTOCOMPLETE_MODE_ATTRIBUTE = 'data-macros-autocomplete'; | |
| 23 | + | |
| 24 | +/** Generic attribute to control autocomplete popup style/size (used by AutoComplete) */ | |
| 25 | +export const MACRO_AUTOCOMPLETE_STYLE_ATTRIBUTE = 'data-macros-autocomplete-style'; | |
| 26 | + | |
| 27 | +/** | |
| 28 | + * @readonly | |
| 29 | + * @enum {string} | |
| 30 | + */ | |
| 31 | +export 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 | + */ | |
| 44 | +export 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 */ | |
| 52 | +const initializedElements = new WeakSet(); | |
| 53 | + | |
| 54 | +/** @type {WeakMap<HTMLElement, AutoComplete>} Map elements to their autocomplete instances */ | |
| 55 | +const 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 | + */ | |
| 71 | +function 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 | + */ | |
| 119 | +export 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 | + */ | |
| 141 | +function 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 | + */ | |
| 158 | +function 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 | + */ | |
| 175 | +function 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 | + */ | |
| 197 | +function 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 | + */ | |
| 211 | +function 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 | + */ | |
| 236 | +const 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 | + */ | |
| 262 | +export 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 | + */ | |
| 295 | +export 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 | +} | |
| @@ -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 | + | |
| 9 | +import { AutoCompleteNameResult } from './AutoCompleteNameResult.js'; | |
| 10 | +import { | |
| 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'; | |
| 24 | +import { macros as macroSystem } from '../macros/macro-system.js'; | |
| 25 | +import { MacroFlagDefinitions, MacroFlagType } from '../macros/engine/MacroFlags.js'; | |
| 26 | +import { MacroParser } from '../macros/engine/MacroParser.js'; | |
| 27 | +import { MacroCstWalker } from '../macros/engine/MacroCstWalker.js'; | |
| 28 | +import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js'; | |
| 29 | +import { chat_metadata } from '/script.js'; | |
| 30 | +import { 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 | + */ | |
| 70 | +export 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 | + */ | |
| 93 | +export 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 | + */ | |
| 148 | +function 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 | + */ | |
| 194 | +function 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 | + */ | |
| 212 | +export 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 | + */ | |
| 461 | +export 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 | + */ | |
| 679 | +export 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 | + */ | |
| 796 | +export 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 | + */ | |
| 881 | +export 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 | + */ | |
| 913 | +export 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 | + */ | |
| 1214 | +export async function getMacroAutoCompleteAt(text, cursorPos, { isForced = false } = {}) { | |
| 1215 | + const macro = findMacroAtCursor(text, cursorPos); | |
| 1216 | + return buildMacroAutoCompleteResult(text, cursorPos, { macro, isForced }); | |
| 1217 | +} | |
| @@ -3,7 +3,7 @@ import { characters, chat_metadata, eventSource, event_types, generateQuietPromp | ||
| 3 | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension, sortIgnoreCaseAndAccents } from './utils.js'; |
| 7 | 7 | import { debounce_timeout } from './constants.js'; |
| 8 | 8 | import { t } from './i18n.js'; |
| 9 | 9 | import { Popup } from './popup.js'; |
| @@ -42,6 +42,12 @@ const THUMBNAIL_CONFIG = { | ||
| 42 | 42 | }; |
| 43 | 43 | |
| 44 | 44 | /** |
| 45 | + * Cache for image metadata. | |
| 46 | + * @type {Map<string, import('../../src/endpoints/image-metadata.js').ImageMetadata>} | |
| 47 | + */ | |
| 48 | +const METADATA_CACHE = new Map(); | |
| 49 | + | |
| 50 | +/** | |
| 45 | 51 | * Background source types. |
| 46 | 52 | * @readonly |
| 47 | 53 | * @enum {number} |
| @@ -52,6 +58,18 @@ const BG_SOURCES = { | ||
| 52 | 58 | }; |
| 53 | 59 | |
| 54 | 60 | /** |
| 61 | + * Background sorting options. | |
| 62 | + * @readonly | |
| 63 | + * @enum {string} | |
| 64 | + */ | |
| 65 | +const BG_SORT_OPTIONS = { | |
| 66 | + AZ: 'az', | |
| 67 | + ZA: 'za', | |
| 68 | + NEWEST: 'newest', | |
| 69 | + OLDEST: 'oldest', | |
| 70 | +}; | |
| 71 | + | |
| 72 | +/** | |
| 55 | 73 | * Mapping of background sources to their corresponding tab IDs. |
| 56 | 74 | * @readonly |
| 57 | 75 | * @type {Record<string, string>} |
| @@ -67,14 +85,56 @@ const BG_TABS = Object.freeze({ | ||
| 67 | 85 | */ |
| 68 | 86 | let lazyLoadObserver = null; |
| 69 | 87 | |
| 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 | + */ | |
| 93 | +let cachedSystemBackgrounds = []; | |
| 94 | + | |
| 70 | 95 | export let background_settings = { |
| 71 | 96 | name: '__transparent.png', |
| 72 | 97 | url: generateUrlParameter('__transparent.png', false), |
| 73 | 98 | fitting: 'classic', |
| 74 | 99 | animation: false, |
| 100 | + sortOrder: BG_SORT_OPTIONS.AZ, | |
| 75 | 101 | }; |
| 76 | 102 | |
| 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 | + */ | |
| 109 | +function 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 | 138 | * Creates a single thumbnail DOM element. The CSS now handles all sizing. |
| 79 | 139 | * @param {object} imageData - Data for the image (filename, isCustom). |
| 80 | 140 | * @returns {HTMLElement} The created thumbnail element. |
| @@ -89,6 +149,18 @@ function createThumbnailElement(imageData) { | ||
| 89 | 149 | clipper.className = 'thumbnail-clipper lazy-load-background'; |
| 90 | 150 | clipper.style.backgroundImage = PLACEHOLDER_IMAGE; |
| 91 | 151 | |
| 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 | 164 | const titleElement = thumbnail.find('.BGSampleTitle'); |
| 93 | 165 | clipper.appendChild(titleElement.get(0)); |
| 94 | 166 | thumbnail.append(clipper); |
| @@ -132,6 +204,9 @@ export function loadBackgroundSettings(settings) { | ||
| 132 | 204 | if (!Object.hasOwn(backgroundSettings, 'animation')) { |
| 133 | 205 | backgroundSettings.animation = false; |
| 134 | 206 | } |
| 207 | + if (!backgroundSettings.sortOrder) { | |
| 208 | + backgroundSettings.sortOrder = BG_SORT_OPTIONS.AZ; | |
| 209 | + } | |
| 135 | 210 | |
| 136 | 211 | // If a value is already saved, use it. Otherwise, determine default based on screen size. |
| 137 | 212 | let columns = backgroundSettings.thumbnailColumns; |
| @@ -140,12 +215,14 @@ export function loadBackgroundSettings(settings) { | ||
| 140 | 215 | columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP; |
| 141 | 216 | } |
| 142 | 217 | background_settings.thumbnailColumns = columns; |
| 218 | + background_settings.sortOrder = backgroundSettings.sortOrder; | |
| 143 | 219 | applyThumbnailColumns(background_settings.thumbnailColumns); |
| 144 | 220 | |
| 145 | 221 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 146 | 222 | setFittingClass(backgroundSettings.fitting); |
| 147 | 223 | $('#background_fitting').val(backgroundSettings.fitting); |
| 148 | 224 | $('#background_thumbnails_animation').prop('checked', background_settings.animation); |
| 225 | + $('#bg-sort').val(background_settings.sortOrder); | |
| 149 | 226 | highlightSelectedBackground(); |
| 150 | 227 | } |
| 151 | 228 | |
| @@ -429,6 +506,11 @@ async function onDeleteBackgroundClick(e) { | ||
| 429 | 506 | // If it's not custom, it's a built-in background. Delete it from the server |
| 430 | 507 | if (!isCustom) { |
| 431 | 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 | 514 | } else { |
| 433 | 515 | const list = chat_metadata[LIST_METADATA_KEY] || []; |
| 434 | 516 | const index = list.indexOf(bg); |
| @@ -517,7 +599,8 @@ function renderSystemBackgrounds(backgrounds) { | ||
| 517 | 599 | |
| 518 | 600 | if (sourceList.length === 0) return; |
| 519 | 601 | |
| 520 | - sourceList.forEach(bg => { | |
| 602 | + const sortedList = sortBackgrounds(sourceList, false); | |
| 603 | + sortedList.forEach(bg => { | |
| 521 | 604 | const imageData = { filename: bg, isCustom: false }; |
| 522 | 605 | const thumbnail = createThumbnailElement(imageData); |
| 523 | 606 | container.append(thumbnail); |
| @@ -538,7 +621,8 @@ function renderChatBackgrounds(backgrounds) { | ||
| 538 | 621 | |
| 539 | 622 | if (sourceList.length === 0) return; |
| 540 | 623 | |
| 541 | - sourceList.forEach(bg => { | |
| 624 | + const sortedList = sortBackgrounds(sourceList, true); | |
| 625 | + sortedList.forEach(bg => { | |
| 542 | 626 | const imageData = { filename: bg, isCustom: true }; |
| 543 | 627 | const thumbnail = createThumbnailElement(imageData); |
| 544 | 628 | container.append(thumbnail); |
| @@ -548,6 +632,8 @@ function renderChatBackgrounds(backgrounds) { | ||
| 548 | 632 | } |
| 549 | 633 | |
| 550 | 634 | export async function getBackgrounds() { |
| 635 | + const metadataPromise = preloadImageMetadata(); | |
| 636 | + | |
| 551 | 637 | const response = await fetch('/api/backgrounds/all', { |
| 552 | 638 | method: 'POST', |
| 553 | 639 | headers: getRequestHeaders(), |
| @@ -557,11 +643,40 @@ export async function getBackgrounds() { | ||
| 557 | 643 | const { images, config } = await response.json(); |
| 558 | 644 | Object.assign(THUMBNAIL_CONFIG, config); |
| 559 | 645 | |
| 646 | + cachedSystemBackgrounds = images; | |
| 647 | + | |
| 648 | + await metadataPromise; | |
| 649 | + | |
| 560 | 650 | renderSystemBackgrounds(images); |
| 561 | 651 | highlightSelectedBackground(); |
| 562 | 652 | } |
| 563 | 653 | } |
| 564 | 654 | |
| 655 | +/** | |
| 656 | + * Preloads all image metadata to use dominant colors as placeholders. | |
| 657 | + * @return {Promise<void>} | |
| 658 | + */ | |
| 659 | +async 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 | + | |
| 565 | 680 | function activateLazyLoader() { |
| 566 | 681 | // Disconnect previous observer to prevent memory leaks |
| 567 | 682 | if (lazyLoadObserver) { |
| @@ -921,6 +1036,17 @@ export function initBackgrounds() { | ||
| 921 | 1036 | $('#auto_background').on('click', autoBackgroundCommand); |
| 922 | 1037 | $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent)); |
| 923 | 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 | 1050 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 925 | 1051 | name: 'lockbg', |
| 926 | 1052 | callback: () => { |
| @@ -12,6 +12,7 @@ import { | ||
| 12 | 12 | saveChatConditional, |
| 13 | 13 | saveItemizedPrompts, |
| 14 | 14 | setActiveGroup, |
| 15 | + getCurrentChatDetails, | |
| 15 | 16 | } from '../script.js'; |
| 16 | 17 | import { humanizedDateTime } from './RossAscends-mods.js'; |
| 17 | 18 | import { |
| @@ -81,30 +82,35 @@ async function getExistingChatNames() { | ||
| 81 | 82 | } |
| 82 | 83 | |
| 83 | 84 | async function getBookmarkName({ isReplace = false, forceName = null } = {}) { |
| 84 | 85 | const chatNamesmainChatName = await getExistingChatNames(getCurrentChatDetails()).sessionName; |
| 86 | + | |
| 87 | + function buildCheckpointName(name, i) { | |
| 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 }); | |
| 85 | 96 | |
| 86 | 97 | const body = await renderTemplateAsync('createCheckpoint', { isReplace: isReplace, suggestedName: suggestedName }); |
| 87 | 98 | let name = forceName ?? await Popup.show.input('Create Checkpoint', body, suggestedName); |
| 88 | 99 | // Special handling for confirmed empty input (=> auto-generate name) |
| 89 | 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 | 103 | if (!name) { |
| 98 | 104 | return null; |
| 99 | 105 | } |
| 100 | 106 | |
| 101 | - return `${name} - ${humanizedDateTime()}`; | |
| 107 | + return name; | |
| 102 | 108 | } |
| 103 | 109 | |
| 104 | 110 | function getMainChatName() { |
| 105 | 111 | if (chat_metadata) { |
| 106 | 112 | if (chat_metadata['.main_chat']) { |
| 107 | 113 | return chat_metadata['.main_chat']; |
| 108 | 114 | } |
| 109 | 115 | // groups didn't support bookmarks before chat metadata was introduced |
| 110 | 116 | else if (selected_group) { |
| @@ -112,8 +118,8 @@ function getMainChatName() { | ||
| 112 | 118 | } |
| 113 | 119 | else if (characters[this_chid].chat && characters[this_chid].chat.includes(bookmarkNameToken)) { |
| 114 | 120 | const tokenIndex = characters[this_chid].chat.lastIndexOf(bookmarkNameToken); |
| 115 | 121 | chat_metadata['.main_chat'] = characters[this_chid].chat.substring(0, tokenIndex).trim(); |
| 116 | 122 | return chat_metadata['.main_chat']; |
| 117 | 123 | } |
| 118 | 124 | } |
| 119 | 125 | return null; |
| @@ -127,7 +133,7 @@ export function showBookmarksButtons() { | ||
| 127 | 133 | $('#option_convert_to_group').show(); |
| 128 | 134 | } |
| 129 | 135 | |
| 130 | 136 | if (chat_metadata['.main_chat']) { |
| 131 | 137 | // In bookmark chat |
| 132 | 138 | $('#option_back_to_main').show(); |
| 133 | 139 | $('#option_new_bookmark').show(); |
| @@ -170,9 +176,23 @@ export async function createBranch(mesId) { | ||
| 170 | 176 | } |
| 171 | 177 | |
| 172 | 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 | 180 | const newMetadata = { main_chat: mainChatmainChatName }; |
| 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 | + } | |
| 176 | 196 | |
| 177 | 197 | if (selected_group) { |
| 178 | 198 | await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId); |
| @@ -184,10 +204,10 @@ export async function createBranch(mesId) { | ||
| 184 | 204 | if (typeof lastMes.extra !== 'object') { |
| 185 | 205 | lastMes.extra = {}; |
| 186 | 206 | } |
| 187 | 207 | if (typeof lastMes.extra['.branches'] !== 'object') { |
| 188 | 208 | lastMes.extra['.branches'] = []; |
| 189 | 209 | } |
| 190 | 210 | lastMes.extra['.branches'].push(name); |
| 191 | 211 | return name; |
| 192 | 212 | } |
| 193 | 213 | |
| @@ -236,7 +256,7 @@ export async function createNewBookmark(mesId, { forceName = null } = {}) { | ||
| 236 | 256 | await saveChat({ chatName: name, withMetadata: newMetadata, mesId }); |
| 237 | 257 | } |
| 238 | 258 | |
| 239 | 259 | lastMes.extra['.bookmark_link'] = name; |
| 240 | 260 | |
| 241 | 261 | const mes = $(`.mes[mesid="${mesId}"]`); |
| 242 | 262 | updateBookmarkDisplay(mes, name); |
| @@ -636,7 +656,7 @@ export function initBookmarks() { | ||
| 636 | 656 | |
| 637 | 657 | const fileName = $(this).hasClass('mes_bookmark') |
| 638 | 658 | ? $(this).closest('.mes').attr('bookmark_link') |
| 639 | 659 | : $(this).attr('file_name').replace('.jsonl', ''); |
| 640 | 660 | |
| 641 | 661 | if (!fileName) { |
| 642 | 662 | return; |
| @@ -42,13 +42,13 @@ function setCharCfg(tempValue, setting) { | ||
| 42 | 42 | |
| 43 | 43 | switch (setting) { |
| 44 | 44 | case settingType.guidance_scale: |
| 45 | 45 | tempCharaCfg['.guidance_scale'] = Number(tempValue); |
| 46 | 46 | break; |
| 47 | 47 | case settingType.negative_prompt: |
| 48 | 48 | tempCharaCfg['.negative_prompt'] = tempValue; |
| 49 | 49 | break; |
| 50 | 50 | case settingType.positive_prompt: |
| 51 | 51 | tempCharaCfg['.positive_prompt'] = tempValue; |
| 52 | 52 | break; |
| 53 | 53 | default: |
| 54 | 54 | return false; |
| @@ -239,31 +239,31 @@ function migrateSettings() { | ||
| 239 | 239 | |
| 240 | 240 | if (power_user.guidance_scale) { |
| 241 | 241 | extension_settings.cfg.global.guidance_scale = power_user.guidance_scale; |
| 242 | 242 | delete power_user['.guidance_scale']; |
| 243 | 243 | performSettingsSave = true; |
| 244 | 244 | } |
| 245 | 245 | |
| 246 | 246 | if (power_user.negative_prompt) { |
| 247 | 247 | extension_settings.cfg.global.negative_prompt = power_user.negative_prompt; |
| 248 | 248 | delete power_user['.negative_prompt']; |
| 249 | 249 | performSettingsSave = true; |
| 250 | 250 | } |
| 251 | 251 | |
| 252 | 252 | if (chat_metadata['.cfg_negative_combine']) { |
| 253 | 253 | chat_metadata[metadataKeys.prompt_combine] = chat_metadata['.cfg_negative_combine']; |
| 254 | 254 | chat_metadata['.cfg_negative_combine'] = undefined; |
| 255 | 255 | performMetaSave = true; |
| 256 | 256 | } |
| 257 | 257 | |
| 258 | 258 | if (chat_metadata['.cfg_negative_insertion_depth']) { |
| 259 | 259 | chat_metadata[metadataKeys.prompt_insertion_depth] = chat_metadata['.cfg_negative_insertion_depth']; |
| 260 | 260 | chat_metadata['.cfg_negative_insertion_depth'] = undefined; |
| 261 | 261 | performMetaSave = true; |
| 262 | 262 | } |
| 263 | 263 | |
| 264 | 264 | if (chat_metadata['.cfg_negative_separator']) { |
| 265 | 265 | chat_metadata[metadataKeys.prompt_separator] = chat_metadata['.cfg_negative_separator']; |
| 266 | 266 | chat_metadata['.cfg_negative_separator'] = undefined; |
| 267 | 267 | performMetaSave = true; |
| 268 | 268 | } |
| 269 | 269 | |
| @@ -148,8 +148,8 @@ export async function bindModelTemplates(power_user, online_status) { | ||
| 148 | 148 | ?? power_user.model_templates_mappings[chatTemplateHash] |
| 149 | 149 | ?? {}; |
| 150 | 150 | const bindingsMatch = bindModelTemplates |
| 151 | 151 | && power_user.context.preset == bindModelTemplates['.context'] |
| 152 | 152 | && (!power_user.instruct.enabled || power_user.instruct.preset === bindModelTemplates['.instruct']); |
| 153 | 153 | |
| 154 | 154 | const bound = []; |
| 155 | 155 | |
| @@ -160,21 +160,21 @@ export async function bindModelTemplates(power_user, online_status) { | ||
| 160 | 160 | toastr.info(t`Context preset for ${online_status} will use defaults when loaded the next time.`); |
| 161 | 161 | } else { |
| 162 | 162 | if (power_user.context_derived) { |
| 163 | 163 | if (power_user.context.preset !== bindModelTemplates['.context']) { |
| 164 | 164 | bound.push(`${power_user.context.preset} context preset`); |
| 165 | 165 | // toastr.info(`Bound ${power_user.context.preset} preset to currently loaded model and all models that share its chat template.`); |
| 166 | 166 | |
| 167 | 167 | // map current preset to current chat template hash |
| 168 | 168 | bindModelTemplates['.context'] = power_user.context.preset; |
| 169 | 169 | } |
| 170 | 170 | } else { |
| 171 | 171 | toastr.warning(t`Note: Context derivation is disabled. Not including context preset.`); |
| 172 | 172 | } |
| 173 | 173 | if (power_user.instruct.enabled) { |
| 174 | 174 | if (power_user.instruct_derived) { |
| 175 | 175 | if (power_user.instruct.preset !== bindModelTemplates['.instruct']) { |
| 176 | 176 | bound.push(`${power_user.instruct.preset} instruct preset`); |
| 177 | 177 | bindModelTemplates['.instruct'] = power_user.instruct.preset; |
| 178 | 178 | } |
| 179 | 179 | } else { |
| 180 | 180 | toastr.warning(t`Note: Instruct derivation is disabled. Not including instruct preset.`); |
| @@ -685,7 +685,7 @@ export function formatCreatorNotes(text, avatarId) { | ||
| 685 | 685 | const preference = new StylesPreference(avatarId); |
| 686 | 686 | const sanitizeStyles = !preference.get(); |
| 687 | 687 | const decodeStyleParam = { prefix: sanitizeStyles ? '#creator_notes_spoiler ' : '' }; |
| 688 | 688 | /** @type {import('dompurify')DOMPurify.Config & { MESSAGE_SANITIZE: boolean }} */ |
| 689 | 689 | const config = { |
| 690 | 690 | RETURN_DOM: false, |
| 691 | 691 | RETURN_DOM_FRAGMENT: false, |
| @@ -1911,13 +1911,13 @@ export function addDOMPurifyHooks() { | ||
| 1911 | 1911 | }); |
| 1912 | 1912 | |
| 1913 | 1913 | DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => { |
| 1914 | 1914 | if (!config['.MESSAGE_SANITIZE']) { |
| 1915 | 1915 | return; |
| 1916 | 1916 | } |
| 1917 | 1917 | |
| 1918 | 1918 | /* Retain the classes on UI elements of messages that interact with the main UI */ |
| 1919 | 1919 | const permittedNodeTypes = ['BUTTON', 'DIV']; |
| 1920 | 1920 | if (config['.MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) { |
| 1921 | 1921 | return; |
| 1922 | 1922 | } |
| 1923 | 1923 | |
| @@ -1938,7 +1938,7 @@ export function addDOMPurifyHooks() { | ||
| 1938 | 1938 | }); |
| 1939 | 1939 | |
| 1940 | 1940 | DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { |
| 1941 | 1941 | if (!config['.MESSAGE_SANITIZE']) { |
| 1942 | 1942 | return; |
| 1943 | 1943 | } |
| 1944 | 1944 | |
| @@ -2239,6 +2239,11 @@ export function initChatUtilities() { | ||
| 2239 | 2239 | wrapper.classList.add('flexFlowColumn', 'justifyCenter', 'alignitemscenter'); |
| 2240 | 2240 | const textarea = document.createElement('textarea'); |
| 2241 | 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 | 2247 | textarea.value = String(contentEditable ? bro[0].innerText : bro.val()); |
| 2243 | 2248 | textarea.classList.add('height100p', 'wide100p', 'maximized_textarea'); |
| 2244 | 2249 | bro.hasClass('monospace') && textarea.classList.add('monospace'); |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | import { EventEmitter } from '../lib/eventemitter.js'; |
| 2 | 2 | |
| 3 | 3 | export const event_types = { |
| 4 | + APP_INITIALIZED: 'app_initialized', | |
| 4 | 5 | APP_READY: 'app_ready', |
| 5 | 6 | EXTRAS_CONNECTED: 'extras_connected', |
| 6 | 7 | MESSAGE_SWIPED: 'message_swiped', |
| @@ -16,6 +17,8 @@ export const event_types = { | ||
| 16 | 17 | MORE_MESSAGES_LOADED: 'more_messages_loaded', |
| 17 | 18 | IMPERSONATE_READY: 'impersonate_ready', |
| 18 | 19 | CHAT_CHANGED: 'chat_id_changed', |
| 20 | + // TODO: Naming convention is inconsistent with other events | |
| 21 | + CHAT_LOADED: 'chatLoaded', | |
| 19 | 22 | GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS', |
| 20 | 23 | GENERATION_STARTED: 'generation_started', |
| 21 | 24 | GENERATION_STOPPED: 'generation_stopped', |
| @@ -95,4 +98,4 @@ export const event_types = { | ||
| 95 | 98 | MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted', |
| 96 | 99 | }; |
| 97 | 100 | |
| 98 | 101 | export const eventSource = new EventEmitter([event_types.APP_READY, event_types.APP_INITIALIZED]); |
| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | import { disableExtension, enableExtension, extension_settingsextensionNames, extensionNamesfindExtension } from './extensions.js'; |
| 2 | 2 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 3 | 3 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 4 | 4 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| 5 | 5 | import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 6 | 6 | import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; |
| 7 | 7 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 8 | 8 | import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js'; |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | 11 | * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension |
| @@ -22,30 +22,28 @@ function getExtensionActionCallback(action) { | ||
| 22 | 22 | } |
| 23 | 23 | |
| 24 | 24 | const reload = !isFalseBoolean(args?.reload?.toString()); |
| 25 | 25 | const internalExtensionNameextension = findExtension(extensionName); |
| 26 | 26 | if (!internalExtensionNameextension) { |
| 27 | 27 | toastr.warning(`Extension ${extensionName} does not exist.`); |
| 28 | 28 | return ''; |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | - const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName); | |
| 31 | + if (action === 'enable' && extension.enabled) { | |
| 32 | - | |
| 32 | + 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 | } |
| 37 | 35 | |
| 38 | 36 | if (action === 'disable' && !isEnabledextension.enabled) { |
| 39 | 37 | toastr.info(`Extension ${extensionNameextension.name} is already disabled.`); |
| 40 | 38 | return internalExtensionNameextension.name; |
| 41 | 39 | } |
| 42 | 40 | |
| 43 | 41 | if (action === 'toggle') { |
| 44 | 42 | action = isEnabledextension.enabled ? 'disable' : 'enable'; |
| 45 | 43 | } |
| 46 | 44 | |
| 47 | 45 | if (reload) { |
| 48 | 46 | toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionNameextension.name} and reloading...`); |
| 49 | 47 | |
| 50 | 48 | // Clear input, so it doesn't stay because the command didn't "finish", |
| 51 | 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 | } |
| 55 | 53 | |
| 56 | 54 | if (action === 'enable') { |
| 57 | 55 | await enableExtension(internalExtensionNameextension.name, reload); |
| 58 | 56 | } else { |
| 59 | 57 | await disableExtension(internalExtensionNameextension.name, reload); |
| 60 | 58 | } |
| 61 | 59 | |
| 62 | 60 | toastr.success(`Extension ${extensionNameextension.name} ${action}d.`); |
| 63 | 61 | |
| 64 | 62 | |
| 65 | 63 | console.info(`Extension ${action}ed: ${extensionNameextension.name}`); |
| 66 | 64 | if (!reload) { |
| 67 | 65 | console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.'); |
| 68 | 66 | } |
| 69 | 67 | |
| 70 | 68 | return internalExtensionNameextension.name; |
| 71 | 69 | }; |
| 72 | 70 | } |
| 73 | 71 | |
| 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 | - */ | |
| 80 | -function findExtension(name) { | |
| 81 | - return extensionNames.find(extName => { | |
| 82 | - return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`); | |
| 83 | - }); | |
| 84 | -} | |
| 85 | - | |
| 86 | -/** | |
| 87 | 73 | * Provides an array of SlashCommandEnumValue objects based on the extension names. |
| 88 | 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 | 230 | name: 'extension-state', |
| 245 | 231 | callback: async (_, extensionName) => { |
| 246 | 232 | if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.'); |
| 247 | 233 | const internalExtensionNameextension = findExtension(extensionName); |
| 248 | 234 | if (!internalExtensionNameextension) { |
| 249 | 235 | toastr.warning(`Extension ${extensionName} does not exist.`); |
| 250 | 236 | return ''; |
| 251 | 237 | } |
| 252 | 238 | |
| 253 | - const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName); | |
| 239 | + return String(extension.enabled); | |
| 254 | - return String(isEnabled); | |
| 255 | 240 | }, |
| 256 | 241 | returns: 'The state of the extension, whether it is enabled.', |
| 257 | 242 | unnamedArgumentList: [ |
| @@ -282,8 +267,8 @@ export function registerExtensionSlashCommands() { | ||
| 282 | 267 | aliases: ['extension-installed'], |
| 283 | 268 | callback: async (_, extensionName) => { |
| 284 | 269 | if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.'); |
| 285 | 270 | const existsextension = findExtension(extensionName) !== undefined; |
| 286 | 271 | return existsextension !== null ? 'true' : 'false'; |
| 287 | 272 | }, |
| 288 | 273 | returns: 'Whether the extension exists and is installed.', |
| 289 | 274 | unnamedArgumentList: [ |
| @@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque | ||
| 4 | 4 | import { showLoader } from './loader.js'; |
| 5 | 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 6 | 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 7 | 7 | import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js'; |
| 8 | 8 | import { getContext } from './st-context.js'; |
| 9 | 9 | import { isAdmin } from './user.js'; |
| 10 | 10 | import { addLocaleData, getCurrentLocale, t } from './i18n.js'; |
| @@ -300,6 +300,64 @@ function onEnableExtensionClick() { | ||
| 300 | 300 | } |
| 301 | 301 | |
| 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 | + */ | |
| 308 | +function 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 | 361 | * Enables an extension by name. |
| 304 | 362 | * @param {string} name Extension name |
| 305 | 363 | * @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 | } |
| 333 | 391 | |
| 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 | + */ | |
| 398 | +export 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 | 408 | * Loads manifest.json files for extensions. |
| 336 | 409 | * @param {string[]} names Array of extension names |
| 337 | 410 | * @returns {Promise<Record<string, object>>} Object with extension names as keys and their manifests as values |
| @@ -839,8 +912,15 @@ async function showExtensionsDetails() { | ||
| 839 | 912 | await oldPopup.completeCancelled(); |
| 840 | 913 | } |
| 841 | 914 | const htmlErrors = getExtensionLoadErrorsHtml(); |
| 842 | 915 | const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + 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 | 924 | const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5"> |
| 845 | 925 | <i class="fa-solid fa-spinner fa-spin"></i> |
| 846 | 926 | <span>` + t`Loading third-party extensions... Please wait...` + `</span> |
| @@ -852,6 +932,7 @@ async function showExtensionsDetails() { | ||
| 852 | 932 | const sortByName = accountStorage.getItem(sortOrderKey) === 'true'; |
| 853 | 933 | const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder; |
| 854 | 934 | const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData); |
| 935 | + let extensionsToToggle = []; | |
| 855 | 936 | |
| 856 | 937 | extensions.forEach(value => { |
| 857 | 938 | const { isExternal, extensionHtml } = value; |
| @@ -886,6 +967,54 @@ async function showExtensionsDetails() { | ||
| 886 | 967 | updateEnabledOnlyButton.textContent = t`Update enabled`; |
| 887 | 968 | updateEnabledOnlyButton.addEventListener('click', () => updateAction(false)); |
| 888 | 969 | |
| 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 | 1018 | const flexExpander = document.createElement('div'); |
| 890 | 1019 | flexExpander.classList.add('expander'); |
| 891 | 1020 | |
| @@ -899,6 +1028,7 @@ async function showExtensionsDetails() { | ||
| 899 | 1028 | }); |
| 900 | 1029 | |
| 901 | 1030 | toolbar.append(updateAllButton, updateEnabledOnlyButton, flexExpander, sortOrderButton); |
| 1031 | + htmlExternal.find('.third_party_toolbar').append(restoreBulkToggledExtensionsButton, toggleAllExtensionsButton); | |
| 902 | 1032 | html.prepend(toolbar); |
| 903 | 1033 | } |
| 904 | 1034 | |
| @@ -914,6 +1044,24 @@ async function showExtensionsDetails() { | ||
| 914 | 1044 | if (waitingForSave) { |
| 915 | 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 | 1065 | if (stateChanged) { |
| 918 | 1066 | waitingForSave = true; |
| 919 | 1067 | const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`); |
| @@ -922,6 +1070,7 @@ async function showExtensionsDetails() { | ||
| 922 | 1070 | waitingForSave = false; |
| 923 | 1071 | requiresReload = true; |
| 924 | 1072 | } |
| 1073 | + | |
| 925 | 1074 | return true; |
| 926 | 1075 | }, |
| 927 | 1076 | }); |
| @@ -103,10 +103,10 @@ async function downloadAssetsList(url) { | ||
| 103 | 103 | |
| 104 | 104 | for (const i of json) { |
| 105 | 105 | //console.log(DEBUG_PREFIX,i) |
| 106 | 106 | if (availableAssets[i['.type']] === undefined) |
| 107 | 107 | availableAssets[i['.type']] = []; |
| 108 | 108 | |
| 109 | 109 | availableAssets[i['.type']].push(i); |
| 110 | 110 | } |
| 111 | 111 | |
| 112 | 112 | console.debug(DEBUG_PREFIX, 'Updated available assets to', availableAssets); |
| @@ -139,7 +139,7 @@ async function downloadAssetsList(url) { | ||
| 139 | 139 | assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation')); |
| 140 | 140 | } |
| 141 | 141 | |
| 142 | 142 | for (const asset of availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['.name'].localeCompare(b['.name']))) { |
| 143 | 143 | const i = availableAssets[assetType].indexOf(asset); |
| 144 | 144 | const elemId = `assets_install_${assetType}_${i}`; |
| 145 | 145 | let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' }); |
| @@ -149,13 +149,13 @@ async function downloadAssetsList(url) { | ||
| 149 | 149 | //if (DEBUG_TONY_SAMA_FORK_MODE) |
| 150 | 150 | // asset["url"] = asset["url"].replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG |
| 151 | 151 | |
| 152 | 152 | console.debug(DEBUG_PREFIX, 'Checking asset', asset['.id'], asset['.url']); |
| 153 | 153 | |
| 154 | 154 | const assetInstall = async function () { |
| 155 | 155 | element.off('click'); |
| 156 | 156 | label.removeClass('fa-download'); |
| 157 | 157 | this.classList.add('asset-download-button-loading'); |
| 158 | 158 | await installAsset(asset['.url'], assetType, asset['.id']); |
| 159 | 159 | label.addClass('fa-check'); |
| 160 | 160 | this.classList.remove('asset-download-button-loading'); |
| 161 | 161 | element.on('click', assetDelete); |
| @@ -173,11 +173,11 @@ async function downloadAssetsList(url) { | ||
| 173 | 173 | const assetDelete = async function () { |
| 174 | 174 | if (assetType === 'character') { |
| 175 | 175 | toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported'); |
| 176 | 176 | await executeSlashCommandsWithOptions(`/go ${asset['.id']}`); |
| 177 | 177 | return; |
| 178 | 178 | } |
| 179 | 179 | element.off('click'); |
| 180 | 180 | await deleteAsset(assetType, asset['.id']); |
| 181 | 181 | label.removeClass('fa-check'); |
| 182 | 182 | label.removeClass('redOverlayGlow'); |
| 183 | 183 | label.removeClass('fa-trash'); |
| @@ -186,7 +186,7 @@ async function downloadAssetsList(url) { | ||
| 186 | 186 | element.on('click', assetInstall); |
| 187 | 187 | }; |
| 188 | 188 | |
| 189 | 189 | if (isAssetInstalled(assetType, asset['.id'])) { |
| 190 | 190 | console.debug(DEBUG_PREFIX, 'installed, checked'); |
| 191 | 191 | label.toggleClass('fa-download'); |
| 192 | 192 | label.toggleClass('fa-check'); |
| @@ -207,14 +207,14 @@ async function downloadAssetsList(url) { | ||
| 207 | 207 | element.on('click', assetInstall); |
| 208 | 208 | } |
| 209 | 209 | |
| 210 | 210 | console.debug(DEBUG_PREFIX, 'Created element for ', asset['.id']); |
| 211 | 211 | |
| 212 | 212 | const displayName = DOMPurify.sanitize(asset['.name'] || asset['.id']); |
| 213 | 213 | const description = DOMPurify.sanitize(asset['.description'] || ''); |
| 214 | 214 | const url = isValidUrl(asset['.url']) ? asset['.url'] : ''; |
| 215 | 215 | const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`; |
| 216 | 216 | const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; |
| 217 | 217 | const toolTag = assetType === 'extension' && asset['.tool']; |
| 218 | 218 | const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR; |
| 219 | 219 | |
| 220 | 220 | const assetBlock = $('<i></i>') |
| @@ -246,7 +246,7 @@ async function downloadAssetsList(url) { | ||
| 246 | 246 | if (asset.highlight) { |
| 247 | 247 | assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>'); |
| 248 | 248 | } |
| 249 | 249 | assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset['.url']}" alt="${displayName}"></div>`); |
| 250 | 250 | } |
| 251 | 251 | |
| 252 | 252 | assetBlock.addClass('asset-block'); |
| @@ -204,7 +204,7 @@ async function sendCaptionedMessage(caption, image, mimeType) { | ||
| 204 | 204 | inline_image: !!extension_settings.caption.show_in_chat, |
| 205 | 205 | }, |
| 206 | 206 | }; |
| 207 | 207 | chat_metadata['.tainted'] = true; |
| 208 | 208 | context.chat.push(message); |
| 209 | 209 | const messageId = context.chat.length - 1; |
| 210 | 210 | await eventSource.emit(event_types.MESSAGE_SENT, messageId); |
| @@ -489,6 +489,8 @@ jQuery(async function () { | ||
| 489 | 489 | 'vertexai': SECRET_KEYS.VERTEXAI, |
| 490 | 490 | 'anthropic': SECRET_KEYS.CLAUDE, |
| 491 | 491 | 'xai': SECRET_KEYS.XAI, |
| 492 | + 'zai': SECRET_KEYS.ZAI, | |
| 493 | + 'moonshot': SECRET_KEYS.MOONSHOT, | |
| 492 | 494 | }; |
| 493 | 495 | |
| 494 | 496 | if (reverseProxyApis[api]) { |
| @@ -502,11 +504,10 @@ jQuery(async function () { | ||
| 502 | 504 | 'groq': SECRET_KEYS.GROQ, |
| 503 | 505 | 'cohere': SECRET_KEYS.COHERE, |
| 504 | 506 | 'aimlapi': SECRET_KEYS.AIMLAPI, |
| 505 | - 'moonshot': SECRET_KEYS.MOONSHOT, | |
| 506 | 507 | 'nanogpt': SECRET_KEYS.NANOGPT, |
| 507 | 508 | 'chutes': SECRET_KEYS.CHUTES, |
| 508 | 509 | 'electronhub': SECRET_KEYS.ELECTRONHUB, |
| 509 | 510 | 'zaipollinations': SECRET_KEYS.ZAIPOLLINATIONS, |
| 510 | 511 | }; |
| 511 | 512 | |
| 512 | 513 | if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) { |
| @@ -530,7 +531,7 @@ jQuery(async function () { | ||
| 530 | 531 | } |
| 531 | 532 | |
| 532 | 533 | // Custom API doesn't need additional checks |
| 533 | 534 | if (api === 'custom' || api === 'pollinations') { |
| 534 | 535 | return true; |
| 535 | 536 | } |
| 536 | 537 | } |
| @@ -602,7 +603,7 @@ jQuery(async function () { | ||
| 602 | 603 | const modelIds = await response.json(); |
| 603 | 604 | if (Array.isArray(modelIds) && modelIds.length > 0) { |
| 604 | 605 | modelIds.sort().forEach((modelId) => { |
| 605 | 606 | if (!modelId || typeof modelId !== 'string' || options.some(o => o.value === modelId && o.dataset.type === api)) { |
| 606 | 607 | return; |
| 607 | 608 | } |
| 608 | 609 | const option = document.createElement('option'); |
| @@ -622,6 +623,7 @@ jQuery(async function () { | ||
| 622 | 623 | await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub'); |
| 623 | 624 | await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral'); |
| 624 | 625 | await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai'); |
| 626 | + await processEndpoint('moonshot', '/api/backends/chat-completions/multimodal-models/moonshot'); | |
| 625 | 627 | } |
| 626 | 628 | |
| 627 | 629 | await addSettings(); |
| @@ -699,6 +701,10 @@ jQuery(async function () { | ||
| 699 | 701 | extension_settings.caption.ollama_custom_model = String($('#caption_ollama_custom_model').val()).trim(); |
| 700 | 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 | 708 | $('#caption_refresh_models').on('click', async () => { |
| 703 | 709 | extension_settings.caption.multimodal_model = ''; |
| 704 | 710 | await switchMultimodalBlocks(); |
| @@ -49,13 +49,10 @@ | ||
| 49 | 49 | </div> |
| 50 | 50 | </label> |
| 51 | 51 | <select id="caption_multimodal_model" class="flex1 text_pole"> |
| 52 | 52 | <!-- AI/ML API, OpenRouter, Pollinations, NanoGPT, Mistral, xAI, Moonshot are added externally by JavaScript --> |
| 53 | 53 | <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option> |
| 54 | 54 | <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option> |
| 55 | 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 | 56 | <option data-type="openai" value="gpt-5.2">gpt-5.2</option> |
| 60 | 57 | <option data-type="openai" value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option> |
| 61 | 58 | <option data-type="openai" value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option> |
| @@ -89,6 +86,7 @@ | ||
| 89 | 86 | <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option> |
| 90 | 87 | <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option> |
| 91 | 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 | 90 | <option data-type="anthropic" value="claude-opus-4-5">claude-opus-4-5</option> |
| 93 | 91 | <option data-type="anthropic" value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option> |
| 94 | 92 | <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option> |
| @@ -177,8 +175,16 @@ | ||
| 177 | 175 | <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option> |
| 178 | 176 | <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option> |
| 179 | 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 | 179 | </select> |
| 181 | 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 | 188 | <div data-type="ollama"> |
| 183 | 189 | <div> |
| 184 | 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 | 197 | <input id="caption_ollama_custom_model" class="text_pole" type="text" placeholder="e.g. gemma3:latest" /> |
| 192 | 198 | </div> |
| 193 | 199 | </div> |
| 194 | 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 | 201 | <input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox"> |
| 196 | 202 | <span data-i18n="Allow reverse proxy">Allow reverse proxy</span> |
| 197 | 203 | </label> |
| @@ -437,9 +437,13 @@ async function onChatEvent() { | ||
| 437 | 437 | |
| 438 | 438 | const context = getContext(); |
| 439 | 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]; | |
| 440 | 444 | |
| 441 | 445 | // No new messages - do nothing |
| 442 | 446 | if (chat.length === 0 || (lastMessageId === chat.length && getStringHash(chat[chat.length - 1]lastMessage.mes) === lastMessageHash)) { |
| 443 | 447 | return; |
| 444 | 448 | } |
| 445 | 449 | |
| @@ -451,18 +455,18 @@ async function onChatEvent() { | ||
| 451 | 455 | |
| 452 | 456 | // Message has been edited / regenerated - delete the saved memory |
| 453 | 457 | if (chat.length |
| 454 | 458 | && chat[chat.length - 1]lastMessage.extra |
| 455 | 459 | && chat[chat.length - 1]lastMessage.extra.memory |
| 456 | 460 | && lastMessageId === chat.length |
| 457 | 461 | && getStringHash(chat[chat.length - 1]lastMessage.mes) !== lastMessageHash) { |
| 458 | 462 | delete chat[chat.length - 1]lastMessage.extra.memory; |
| 459 | 463 | } |
| 460 | 464 | |
| 461 | 465 | summarizeChat(context) |
| 462 | 466 | .catch(console.error) |
| 463 | 467 | .finally(() => { |
| 464 | 468 | lastMessageId = context.chat?.length ?? null; |
| 465 | 469 | lastMessageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1]['.mes']) ?? ''); |
| 466 | 470 | }); |
| 467 | 471 | } |
| 468 | 472 | |
| @@ -185,7 +185,7 @@ const init = async () => { | ||
| 185 | 185 | buttons.show(); |
| 186 | 186 | settings.onSave = ()=>buttons.refresh(); |
| 187 | 187 | |
| 188 | 188 | window['globalThis.executeQuickReplyByName'] = async(name, args = {}, options = {}) => { |
| 189 | 189 | let qr = [ |
| 190 | 190 | ...settings.config.setList, |
| 191 | 191 | ...(settings.chatConfig?.setList ?? []), |
| @@ -77,7 +77,7 @@ export class SlashCommandHandler { | ||
| 77 | 77 | }, |
| 78 | 78 | }; |
| 79 | 79 | |
| 80 | 80 | window['globalThis.qrEnumProviderExecutables'] = localEnumProviders.qrExecutables; |
| 81 | 81 | |
| 82 | 82 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr', |
| 83 | 83 | callback: (_, value) => this.executeQuickReplyByIndex(Number(value)), |
| @@ -15,7 +15,7 @@ import { createThumbnail, isValidUrl } from '../utils.js'; | ||
| 15 | 15 | */ |
| 16 | 16 | export async function getMultimodalCaption(base64Img, prompt) { |
| 17 | 17 | const useReverseProxy = |
| 18 | 18 | (['openai', 'anthropic', 'google', 'mistral', 'vertexai', 'xai', 'zai', 'moonshot'].includes(extension_settings.caption.multimodal_api)) |
| 19 | 19 | && extension_settings.caption.allow_reverse_proxy |
| 20 | 20 | && oai_settings.reverse_proxy |
| 21 | 21 | && isValidUrl(oai_settings.reverse_proxy); |
| @@ -108,8 +108,15 @@ export async function getMultimodalCaption(base64Img, prompt) { | ||
| 108 | 108 | } |
| 109 | 109 | |
| 110 | 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 | 119 | requestBody.server_url = oai_settings.custom_url; |
| 112 | - requestBody.model = oai_settings.custom_model || 'gpt-4-turbo'; | |
| 113 | 120 | requestBody.custom_include_headers = oai_settings.custom_include_headers; |
| 114 | 121 | requestBody.custom_include_body = oai_settings.custom_include_body; |
| 115 | 122 | requestBody.custom_exclude_body = oai_settings.custom_exclude_body; |
| @@ -245,6 +252,10 @@ function throwIfInvalidModel(useReverseProxy) { | ||
| 245 | 252 | throw new Error('Custom API URL is not set.'); |
| 246 | 253 | } |
| 247 | 254 | |
| 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 | 259 | if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) { |
| 249 | 260 | throw new Error('AI/ML API key is not set.'); |
| 250 | 261 | } |
| @@ -268,6 +279,10 @@ function throwIfInvalidModel(useReverseProxy) { | ||
| 268 | 279 | if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) { |
| 269 | 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 | } |
| 272 | 287 | |
| 273 | 288 | /** |
| @@ -69,10 +69,17 @@ const MODULE_NAME = 'sd'; | ||
| 69 | 69 | // This is a 1x1 transparent PNG |
| 70 | 70 | const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; |
| 71 | 71 | const CUSTOM_STOP_EVENT = 'sd_stop_generation'; |
| 72 | + | |
| 73 | +// Generation tracking for status indicator | |
| 74 | +let activeGenerations = 0; | |
| 75 | +/** @type {JQuery<HTMLElement>|null} */ | |
| 76 | +let generationToast = null; | |
| 77 | + | |
| 72 | 78 | const sources = { |
| 73 | 79 | extras: 'extras', |
| 74 | 80 | horde: 'horde', |
| 75 | 81 | auto: 'auto', |
| 82 | + sdcpp: 'sdcpp', | |
| 76 | 83 | novel: 'novel', |
| 77 | 84 | vlad: 'vlad', |
| 78 | 85 | openai: 'openai', |
| @@ -277,6 +284,7 @@ const defaultSettings = { | ||
| 277 | 284 | snap: false, |
| 278 | 285 | free_extend: false, |
| 279 | 286 | function_tool: false, |
| 287 | + minimal_prompt_processing: false, | |
| 280 | 288 | |
| 281 | 289 | prompts: promptTemplates, |
| 282 | 290 | |
| @@ -284,6 +292,9 @@ const defaultSettings = { | ||
| 284 | 292 | auto_url: 'http://localhost:7860', |
| 285 | 293 | auto_auth: '', |
| 286 | 294 | |
| 295 | + // stable-diffusion.cpp settings | |
| 296 | + sdcpp_url: 'http://127.0.0.1:1234', | |
| 297 | + | |
| 287 | 298 | vlad_url: 'http://localhost:7860', |
| 288 | 299 | vlad_auth: '', |
| 289 | 300 | |
| @@ -320,6 +331,7 @@ const defaultSettings = { | ||
| 320 | 331 | // OpenAI settings |
| 321 | 332 | openai_style: 'vivid', |
| 322 | 333 | openai_quality: 'standard', |
| 334 | + openai_quality_gpt: 'auto', | |
| 323 | 335 | openai_duration: '8', |
| 324 | 336 | |
| 325 | 337 | style: 'Default', |
| @@ -425,7 +437,7 @@ function processTriggers(chat, _, abort, type) { | ||
| 425 | 437 | } |
| 426 | 438 | } |
| 427 | 439 | |
| 428 | 440 | window['globalThis.SD_ProcessTriggers'] = processTriggers; |
| 429 | 441 | |
| 430 | 442 | function getSdRequestBody() { |
| 431 | 443 | switch (extension_settings.sd.source) { |
| @@ -521,6 +533,7 @@ async function loadSettings() { | ||
| 521 | 533 | $('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning); |
| 522 | 534 | $('#sd_auto_url').val(extension_settings.sd.auto_url); |
| 523 | 535 | $('#sd_auto_auth').val(extension_settings.sd.auto_auth); |
| 536 | + $('#sd_sdcpp_url').val(extension_settings.sd.sdcpp_url); | |
| 524 | 537 | $('#sd_vlad_url').val(extension_settings.sd.vlad_url); |
| 525 | 538 | $('#sd_vlad_auth').val(extension_settings.sd.vlad_auth); |
| 526 | 539 | $('#sd_drawthings_url').val(extension_settings.sd.drawthings_url); |
| @@ -528,12 +541,14 @@ async function loadSettings() { | ||
| 528 | 541 | $('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode); |
| 529 | 542 | $('#sd_openai_style').val(extension_settings.sd.openai_style); |
| 530 | 543 | $('#sd_openai_quality').val(extension_settings.sd.openai_quality); |
| 544 | + $('#sd_openai_quality_gpt').val(extension_settings.sd.openai_quality_gpt); | |
| 531 | 545 | $('#sd_openai_duration').val(extension_settings.sd.openai_duration); |
| 532 | 546 | $('#sd_comfy_type').val(extension_settings.sd.comfy_type); |
| 533 | 547 | $('#sd_comfy_url').val(extension_settings.sd.comfy_url); |
| 534 | 548 | $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt); |
| 535 | 549 | $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url); |
| 536 | 550 | $('#sd_snap').prop('checked', extension_settings.sd.snap); |
| 551 | + $('#sd_minimal_prompt_processing').prop('checked', extension_settings.sd.minimal_prompt_processing); | |
| 537 | 552 | $('#sd_clip_skip').val(extension_settings.sd.clip_skip); |
| 538 | 553 | $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip); |
| 539 | 554 | $('#sd_seed').val(extension_settings.sd.seed); |
| @@ -656,6 +671,11 @@ function onSnapInput() { | ||
| 656 | 671 | saveSettingsDebounced(); |
| 657 | 672 | } |
| 658 | 673 | |
| 674 | +function onMinimalPromptProcessing() { | |
| 675 | + extension_settings.sd.minimal_prompt_processing = !!$(this).prop('checked'); | |
| 676 | + saveSettingsDebounced(); | |
| 677 | +} | |
| 678 | + | |
| 659 | 679 | function onStyleSelect() { |
| 660 | 680 | const selectedStyle = String($('#sd_style').find(':selected').val()); |
| 661 | 681 | const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle); |
| @@ -708,7 +728,8 @@ async function onDeleteStyleClick() { | ||
| 708 | 728 | } |
| 709 | 729 | |
| 710 | 730 | async 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); | |
| 712 | 733 | |
| 713 | 734 | if (!userInput) { |
| 714 | 735 | return; |
| @@ -744,6 +765,48 @@ async function onSaveStyleClick() { | ||
| 744 | 765 | saveSettingsDebounced(); |
| 745 | 766 | } |
| 746 | 767 | |
| 768 | +async 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 | 811 | * Modifies prompt based on user inputs. |
| 749 | 812 | * @param {string} prompt Prompt to refine |
| @@ -977,6 +1040,13 @@ const resolutionOptions = { | ||
| 977 | 1040 | sd_res_1024x1536: { width: 1024, height: 1536, name: '1024x1536 (2:3, ChatGPT)' }, |
| 978 | 1041 | sd_res_1024x1792: { width: 1024, height: 1792, name: '1024x1792 (4:7, DALL-E)' }, |
| 979 | 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 | }; |
| 981 | 1051 | |
| 982 | 1052 | function onResolutionChange() { |
| @@ -1141,6 +1211,11 @@ function onAutoAuthInput() { | ||
| 1141 | 1211 | saveSettingsDebounced(); |
| 1142 | 1212 | } |
| 1143 | 1213 | |
| 1214 | +function onSdcppUrlInput() { | |
| 1215 | + extension_settings.sd.sdcpp_url = $('#sd_sdcpp_url').val(); | |
| 1216 | + saveSettingsDebounced(); | |
| 1217 | +} | |
| 1218 | + | |
| 1144 | 1219 | function onVladUrlInput() { |
| 1145 | 1220 | extension_settings.sd.vlad_url = $('#sd_vlad_url').val(); |
| 1146 | 1221 | saveSettingsDebounced(); |
| @@ -1249,6 +1324,29 @@ async function validateAutoUrl() { | ||
| 1249 | 1324 | } |
| 1250 | 1325 | } |
| 1251 | 1326 | |
| 1327 | +async 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 | + | |
| 1252 | 1350 | async function validateDrawthingsUrl() { |
| 1253 | 1351 | try { |
| 1254 | 1352 | if (!extension_settings.sd.drawthings_url) { |
| @@ -1542,6 +1640,9 @@ async function loadSamplers() { | ||
| 1542 | 1640 | case sources.auto: |
| 1543 | 1641 | samplers = await loadAutoSamplers(); |
| 1544 | 1642 | break; |
| 1643 | + case sources.sdcpp: | |
| 1644 | + samplers = await loadSdcppSamplers(); | |
| 1645 | + break; | |
| 1545 | 1646 | case sources.drawthings: |
| 1546 | 1647 | samplers = await loadDrawthingsSamplers(); |
| 1547 | 1648 | break; |
| @@ -1667,6 +1768,11 @@ async function loadAutoSamplers() { | ||
| 1667 | 1768 | } |
| 1668 | 1769 | } |
| 1669 | 1770 | |
| 1771 | +async 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 | + | |
| 1670 | 1776 | async function loadDrawthingsSamplers() { |
| 1671 | 1777 | // The app developer doesn't provide an API to get these yet |
| 1672 | 1778 | return [ |
| @@ -1756,6 +1862,9 @@ async function loadModels() { | ||
| 1756 | 1862 | case sources.auto: |
| 1757 | 1863 | models = await loadAutoModels(); |
| 1758 | 1864 | break; |
| 1865 | + case sources.sdcpp: | |
| 1866 | + models = [{ value: '', text: 'N/A' }]; | |
| 1867 | + break; | |
| 1759 | 1868 | case sources.drawthings: |
| 1760 | 1869 | models = await loadDrawthingsModels(); |
| 1761 | 1870 | break; |
| @@ -1850,7 +1959,7 @@ function switchModelSpecificControls(modelId) { | ||
| 1850 | 1959 | |
| 1851 | 1960 | modelControls.each(function () { |
| 1852 | 1961 | const models = String($(this).attr('data-sd-model') || '').split(',').map(m => m.trim()); |
| 1853 | 1962 | $(this).toggle(models.includessome(m => modelId.includes(m))); |
| 1854 | 1963 | }); |
| 1855 | 1964 | } |
| 1856 | 1965 | |
| @@ -1940,6 +2049,8 @@ async function loadXAIModels() { | ||
| 1940 | 2049 | } |
| 1941 | 2050 | |
| 1942 | 2051 | async function loadPollinationsModels() { |
| 2052 | + $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]); | |
| 2053 | + | |
| 1943 | 2054 | const result = await fetch('/api/sd/pollinations/models', { |
| 1944 | 2055 | method: 'POST', |
| 1945 | 2056 | headers: getRequestHeaders({ omitContentType: true }), |
| @@ -2169,6 +2280,7 @@ async function loadOpenAiModels() { | ||
| 2169 | 2280 | { value: 'gpt-image-1.5', text: 'gpt-image-1.5' }, |
| 2170 | 2281 | { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' }, |
| 2171 | 2282 | { value: 'gpt-image-1', text: 'gpt-image-1' }, |
| 2283 | + { value: 'chatgpt-image-latest', text: 'chatgpt-image-latest' }, | |
| 2172 | 2284 | { value: 'dall-e-3', text: 'dall-e-3' }, |
| 2173 | 2285 | { value: 'dall-e-2', text: 'dall-e-2' }, |
| 2174 | 2286 | { value: 'sora-2', text: 'sora-2' }, |
| @@ -2294,7 +2406,12 @@ async function loadGoogleModels() { | ||
| 2294 | 2406 | } |
| 2295 | 2407 | |
| 2296 | 2408 | async 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 | } |
| 2299 | 2416 | |
| 2300 | 2417 | async function loadOpenRouterModels() { |
| @@ -2356,6 +2473,9 @@ async function loadSchedulers() { | ||
| 2356 | 2473 | case sources.auto: |
| 2357 | 2474 | schedulers = await getAutoRemoteSchedulers(); |
| 2358 | 2475 | break; |
| 2476 | + case sources.sdcpp: | |
| 2477 | + schedulers = await loadSdcppSchedulers(); | |
| 2478 | + break; | |
| 2359 | 2479 | case sources.novel: |
| 2360 | 2480 | schedulers = loadNovelSchedulers(); |
| 2361 | 2481 | break; |
| @@ -2454,6 +2574,11 @@ async function loadComfySchedulers() { | ||
| 2454 | 2574 | } |
| 2455 | 2575 | } |
| 2456 | 2576 | |
| 2577 | +async 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 | + | |
| 2457 | 2582 | async function loadVaes() { |
| 2458 | 2583 | $('#sd_vae').empty(); |
| 2459 | 2584 | let vaes = []; |
| @@ -2468,6 +2593,9 @@ async function loadVaes() { | ||
| 2468 | 2593 | case sources.auto: |
| 2469 | 2594 | vaes = await loadAutoVaes(); |
| 2470 | 2595 | break; |
| 2596 | + case sources.sdcpp: | |
| 2597 | + vaes = ['N/A']; | |
| 2598 | + break; | |
| 2471 | 2599 | case sources.novel: |
| 2472 | 2600 | vaes = ['N/A']; |
| 2473 | 2601 | break; |
| @@ -2657,6 +2785,15 @@ function processReply(str) { | ||
| 2657 | 2785 | return ''; |
| 2658 | 2786 | } |
| 2659 | 2787 | |
| 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 | 2797 | str = str.replaceAll('"', ''); |
| 2661 | 2798 | str = str.replaceAll('“', ''); |
| 2662 | 2799 | str = str.replaceAll('\n', ', '); |
| @@ -2728,6 +2865,62 @@ function ensureSelectionExists(setting, selector) { | ||
| 2728 | 2865 | } |
| 2729 | 2866 | |
| 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 | + */ | |
| 2871 | +function 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 | + */ | |
| 2910 | +function startGenerationTracking() { | |
| 2911 | + activeGenerations++; | |
| 2912 | + updateGenerationIndicator(); | |
| 2913 | +} | |
| 2914 | + | |
| 2915 | +/** | |
| 2916 | + * Decrements the active generation counter and updates indicators. | |
| 2917 | + */ | |
| 2918 | +function endGenerationTracking() { | |
| 2919 | + activeGenerations = Math.max(0, activeGenerations - 1); | |
| 2920 | + updateGenerationIndicator(); | |
| 2921 | +} | |
| 2922 | + | |
| 2923 | +/** | |
| 2731 | 2924 | * Generates an image based on the given trigger word. |
| 2732 | 2925 | * @param {string} initiator The initiator of the image generation |
| 2733 | 2926 | * @param {Record<string, object>} args Command arguments |
| @@ -2801,6 +2994,9 @@ async function generatePicture(initiator, args, trigger, message, callback) { | ||
| 2801 | 2994 | await eventSource.emit(event_types.SD_PROMPT_PROCESSING, eventData); |
| 2802 | 2995 | prompt = eventData.prompt; // Allow extensions to modify the prompt |
| 2803 | 2996 | |
| 2997 | + // Track this generation for status indicator | |
| 2998 | + startGenerationTracking(); | |
| 2999 | + // Show stop button after prompt is ready (prompt generation uses separate abort mechanism) | |
| 2804 | 3000 | $(stopButton).show(); |
| 2805 | 3001 | eventSource.once(CUSTOM_STOP_EVENT, stopListener); |
| 2806 | 3002 | |
| @@ -2811,6 +3007,13 @@ async function generatePicture(initiator, args, trigger, message, callback) { | ||
| 2811 | 3007 | // generate the image |
| 2812 | 3008 | imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal); |
| 2813 | 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 | 3017 | console.trace(err); |
| 2815 | 3018 | // errors here are most likely due to text generation failure |
| 2816 | 3019 | // sendGenerationRequest mostly deals with its own errors |
| @@ -2823,6 +3026,7 @@ async function generatePicture(initiator, args, trigger, message, callback) { | ||
| 2823 | 3026 | $(stopButton).hide(); |
| 2824 | 3027 | restoreOriginalDimensions(dimensions); |
| 2825 | 3028 | eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener); |
| 3029 | + endGenerationTracking(); | |
| 2826 | 3030 | } |
| 2827 | 3031 | |
| 2828 | 3032 | return imagePath; |
| @@ -3014,8 +3218,10 @@ function getUserAvatarUrl() { | ||
| 3014 | 3218 | * @returns {Promise<string>} - A promise that resolves when the prompt generation completes. |
| 3015 | 3219 | */ |
| 3016 | 3220 | async function generatePrompt(quietPrompt) { |
| 3221 | + const toast = toastr.info('Generating image prompt with an LLM...', 'Image Generation'); | |
| 3017 | 3222 | const reply = await generateQuietPrompt({ quietPrompt }); |
| 3018 | 3223 | const processedReply = processReply(reply); |
| 3224 | + toastr.clear(toast); | |
| 3019 | 3225 | |
| 3020 | 3226 | if (!processedReply) { |
| 3021 | 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 | 3280 | case sources.auto: |
| 3075 | 3281 | result = await generateAutoImage(prefixedPrompt, negativePrompt, signal); |
| 3076 | 3282 | break; |
| 3283 | + case sources.sdcpp: | |
| 3284 | + result = await generateSdcppImage(prefixedPrompt, negativePrompt, signal); | |
| 3285 | + break; | |
| 3077 | 3286 | case sources.novel: |
| 3078 | 3287 | result = await generateNovelImage(prefixedPrompt, negativePrompt, signal); |
| 3079 | 3288 | break; |
| @@ -3140,6 +3349,13 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 3140 | 3349 | throw new Error('Endpoint did not return image data.'); |
| 3141 | 3350 | } |
| 3142 | 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 | 3359 | console.error('Image generation request error: ', err); |
| 3144 | 3360 | toastr.error('Image generation failed. Please try again.' + '\n\n' + String(err), 'Image Generation'); |
| 3145 | 3361 | return; |
| @@ -3215,7 +3431,7 @@ async function generatePollinationsImage(prompt, negativePrompt, signal) { | ||
| 3215 | 3431 | |
| 3216 | 3432 | if (result.ok) { |
| 3217 | 3433 | const data = await result.json(); |
| 3218 | 3434 | return { format: 'jpg'data?.format, data: data?.image }; |
| 3219 | 3435 | } else { |
| 3220 | 3436 | const text = await result.text(); |
| 3221 | 3437 | throw new Error(text); |
| @@ -3271,7 +3487,7 @@ async function generateExtrasImage(prompt, negativePrompt, signal) { | ||
| 3271 | 3487 | * Gets an aspect ratio for Stability that is the closest to the given width and height. |
| 3272 | 3488 | * @param {number} width Target width |
| 3273 | 3489 | * @param {number} height Target height |
| 3274 | 3490 | * @param {'google'|'stability'|'zai'} source Source of the request, used to determine aspect ratio |
| 3275 | 3491 | * @returns {string} Closest aspect ratio as a string |
| 3276 | 3492 | */ |
| 3277 | 3493 | function getClosestAspectRatio(width, height, source) { |
| @@ -3297,6 +3513,12 @@ function getClosestAspectRatio(width, height, source) { | ||
| 3297 | 3513 | '4:3': 4 / 3, |
| 3298 | 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 | 3522 | default: |
| 3301 | 3523 | console.warn(`Unknown source "${source}" for aspect ratio calculation.`); |
| 3302 | 3524 | return null; |
| @@ -3325,22 +3547,41 @@ function getClosestAspectRatio(width, height, source) { | ||
| 3325 | 3547 | * Get closest size for Electron Hub |
| 3326 | 3548 | * @param {number} width - The width of the image |
| 3327 | 3549 | * @param {number} height - The height of the image |
| 3550 | + * @param {string[]} sizes - Available sizes | |
| 3328 | 3551 | * @returns {Promise<string>} - The closest size |
| 3329 | 3552 | */ |
| 3330 | 3553 | async function getClosestSize(width, height, sizes = []) { |
| 3331 | - const response = await fetch('/api/sd/electronhub/sizes', { | |
| 3554 | + const sizesData = []; | |
| 3332 | - method: 'POST', | |
| 3555 | + | |
| 3333 | - headers: getRequestHeaders(), | |
| 3556 | + if (Array.isArray(sizes) && sizes.length > 0) { | |
| 3334 | - body: JSON.stringify({ | |
| 3557 | + sizesData.push(...sizes); | |
| 3335 | - model: extension_settings.sd.model, | |
| 3558 | + } else if (extension_settings.sd.source === sources.electronhub) { | |
| 3336 | - }), | |
| 3559 | + const response = await fetch('/api/sd/electronhub/sizes', { | |
| 3337 | - }); | |
| 3560 | + method: 'POST', | |
| 3338 | - if (!response.ok) { | |
| 3561 | + headers: getRequestHeaders(), | |
| 3339 | - const text = await response.text(); | |
| 3562 | + body: JSON.stringify({ | |
| 3340 | - throw new Error(text); | |
| 3563 | + model: extension_settings.sd.model, | |
| 3564 | + }), | |
| 3565 | + }); | |
| 3566 | + if (!response.ok) { | |
| 3567 | + const text = await response.text(); | |
| 3568 | + throw new Error(text); | |
| 3569 | + } | |
| 3570 | + const result = await response.json(); | |
| 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; | |
| 3341 | 3581 | } |
| 3342 | - const result = await response.json(); | |
| 3582 | + | |
| 3343 | 3583 | const sizesDatatargetAspect = result.sizestargetWidth / targetHeight; |
| 3584 | + const targetResolution = targetWidth * targetHeight; | |
| 3344 | 3585 | |
| 3345 | 3586 | const closestSize = sizesData.reduce((closest, size) => { |
| 3346 | 3587 | if (!size || typeof size !== 'string') { |
| @@ -3353,16 +3594,14 @@ async function getClosestSize(width, height) { | ||
| 3353 | 3594 | |
| 3354 | 3595 | const sizeWidth = Number(sizeParts[0]); |
| 3355 | 3596 | const sizeHeight = Number(sizeParts[1]); |
| 3356 | - const targetWidth = Number(width); | |
| 3357 | - const targetHeight = Number(height); | |
| 3358 | 3597 | |
| 3359 | 3598 | if (isNaN(sizeWidth) || isNaN(sizeHeight) || isNaN(targetWidth) || isNaN(targetHeight)) { |
| 3360 | 3599 | return closest; |
| 3361 | 3600 | } |
| 3362 | 3601 | |
| 3363 | 3602 | const sizeAreaaspectDiff = Math.abs((sizeWidth */ sizeHeight) - targetAspect) / targetAspect; |
| 3364 | 3603 | const targetArearesolutionDiff = targetWidthMath.abs(sizeWidth * targetHeightsizeHeight - targetResolution) / targetResolution; |
| 3365 | 3604 | const diff = Math.abs(sizeAreaaspectDiff -+ targetArea)resolutionDiff; |
| 3366 | 3605 | |
| 3367 | 3606 | return diff < closest.diff ? { size, diff } : closest; |
| 3368 | 3607 | }, { size: null, diff: Infinity }); |
| @@ -3532,6 +3771,55 @@ async function generateAutoImage(prompt, negativePrompt, signal) { | ||
| 3532 | 3771 | } |
| 3533 | 3772 | |
| 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 | + */ | |
| 3781 | +async 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 | 3823 | * Generates an image in Drawthings API using the provided prompt and configuration settings. |
| 3536 | 3824 | * |
| 3537 | 3825 | * @param {string} prompt - The main instruction used to guide the image generation. |
| @@ -3697,7 +3985,7 @@ async function generateOpenAiImage(prompt, signal) { | ||
| 3697 | 3985 | |
| 3698 | 3986 | const isDalle2 = /dall-e-2/.test(extension_settings.sd.model); |
| 3699 | 3987 | const isDalle3 = /dall-e-3/.test(extension_settings.sd.model); |
| 3700 | 3988 | const isGptImg = /gpt-image-(1|latest)/.test(extension_settings.sd.model); |
| 3701 | 3989 | const isSora2 = /sora-2/.test(extension_settings.sd.model); |
| 3702 | 3990 | |
| 3703 | 3991 | if (isDalle2 && prompt.length > dalle2PromptLimit) { |
| @@ -3770,7 +4058,7 @@ async function generateOpenAiImage(prompt, signal) { | ||
| 3770 | 4058 | model: extension_settings.sd.model, |
| 3771 | 4059 | size: `${width}x${height}`, |
| 3772 | 4060 | n: 1, |
| 3773 | 4061 | quality: isDalle3 ? extension_settings.sd.openai_quality : (isGptImg ? extension_settings.sd.openai_quality_gpt : undefined), |
| 3774 | 4062 | style: isDalle3 ? extension_settings.sd.openai_style : undefined, |
| 3775 | 4063 | response_format: isDalle2 || isDalle3 ? 'b64_json' : undefined, |
| 3776 | 4064 | moderation: isGptImg ? 'low' : undefined, |
| @@ -4242,38 +4530,77 @@ async function generateGoogleImage(prompt, negativePrompt, signal) { | ||
| 4242 | 4530 | * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete. |
| 4243 | 4531 | */ |
| 4244 | 4532 | async function generateZaiImage(prompt, signal) { |
| 4245 | - // Round width and height to nearest multiple of 16, and clamp to 512-2048 range | |
| 4533 | + // 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 = {}; | |
| 4248 | - | |
| 4536 | + if (/cogvideox/.test(extension_settings.sd.model)) { | |
| 4249 | - // Make sure the pixel count does not exceed 2^21px | |
| 4537 | + const cogVideoSizes = ['1280x720', '720x1280', '1024x1024', '1080x1920', '2048x1080', '3840x2160']; | |
| 4250 | - while ((width * height) > Math.pow(2, 21)) { | |
| 4538 | + videoParams.quality = extension_settings.sd.openai_quality === 'hd' ? 'quality' : 'speed'; | |
| 4251 | - if (width >= height) { | |
| 4539 | + videoParams.size = await getClosestSize(extension_settings.sd.width, extension_settings.sd.height, cogVideoSizes); | |
| 4252 | - width -= 16; | |
| 4540 | + } | |
| 4253 | - } else { | |
| 4541 | + if (/vidu/.test(extension_settings.sd.model)) { | |
| 4254 | - height -= 16; | |
| 4542 | + videoParams.aspect_ratio = getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'zai'); | |
| 4255 | 4543 | } |
| 4256 | - } | |
| 4257 | 4544 | |
| 4258 | 4545 | const resultvideoResult = await fetch('/api/sd/zai/generate-video', { |
| 4259 | 4546 | method: 'POST', |
| 4260 | 4547 | headers: getRequestHeaders(), |
| 4261 | 4548 | signal: signal, |
| 4262 | 4549 | body: JSON.stringify({ |
| 4263 | 4550 | prompt: prompt, |
| 4264 | 4551 | model: extension_settings.sd.model, |
| 4265 | - quality: extension_settings.sd.openai_quality, | |
| 4552 | + ...videoParams, | |
| 4266 | - size: `${width}x${height}`, | |
| 4553 | + }), | |
| 4267 | 4554 | }),; |
| 4268 | - }); | |
| 4269 | 4555 | |
| 4270 | 4556 | if (resultvideoResult.ok) { |
| 4271 | 4557 | const data = await resultvideoResult.json(); |
| 4272 | 4558 | return { format: data.format, data: data.imagevideo }; |
| 4273 | 4559 | } |
| 4274 | 4560 | |
| 4275 | 4561 | const text = await resultvideoResult.text(); |
| 4276 | 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) { | |
| 4575 | + while ((width * height) > Math.pow(2, 21)) { | |
| 4576 | + if (width >= height) { | |
| 4577 | + width -= multiple; | |
| 4578 | + } else { | |
| 4579 | + height -= multiple; | |
| 4580 | + } | |
| 4581 | + } | |
| 4582 | + } | |
| 4583 | + | |
| 4584 | + const result = await fetch('/api/sd/zai/generate', { | |
| 4585 | + method: 'POST', | |
| 4586 | + headers: getRequestHeaders(), | |
| 4587 | + signal: signal, | |
| 4588 | + body: JSON.stringify({ | |
| 4589 | + prompt: prompt, | |
| 4590 | + model: extension_settings.sd.model, | |
| 4591 | + quality: extension_settings.sd.openai_quality, | |
| 4592 | + size: `${width}x${height}`, | |
| 4593 | + }), | |
| 4594 | + }); | |
| 4595 | + | |
| 4596 | + if (result.ok) { | |
| 4597 | + const data = await result.json(); | |
| 4598 | + return { format: data.format, data: data.image }; | |
| 4599 | + } | |
| 4600 | + | |
| 4601 | + const text = await result.text(); | |
| 4602 | + throw new Error(text); | |
| 4603 | + } | |
| 4277 | 4604 | } |
| 4278 | 4605 | |
| 4279 | 4606 | /** |
| @@ -4443,6 +4770,58 @@ async function onComfyDeleteWorkflowClick() { | ||
| 4443 | 4770 | onComfyWorkflowChange(); |
| 4444 | 4771 | } |
| 4445 | 4772 | |
| 4773 | +async 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 | 4826 | * Sends a chat message with the generated image. |
| 4448 | 4827 | * @param {string} prompt Prompt used for the image generation |
| @@ -4575,6 +4954,8 @@ function isValidState() { | ||
| 4575 | 4954 | return true; |
| 4576 | 4955 | case sources.auto: |
| 4577 | 4956 | return !!extension_settings.sd.auto_url; |
| 4957 | + case sources.sdcpp: | |
| 4958 | + return !!extension_settings.sd.sdcpp_url; | |
| 4578 | 4959 | case sources.drawthings: |
| 4579 | 4960 | return !!extension_settings.sd.drawthings_url; |
| 4580 | 4961 | case sources.vlad: |
| @@ -4598,7 +4979,7 @@ function isValidState() { | ||
| 4598 | 4979 | case sources.togetherai: |
| 4599 | 4980 | return secret_state[SECRET_KEYS.TOGETHERAI]; |
| 4600 | 4981 | case sources.pollinations: |
| 4601 | 4982 | return truesecret_state[SECRET_KEYS.POLLINATIONS]; |
| 4602 | 4983 | case sources.stability: |
| 4603 | 4984 | return secret_state[SECRET_KEYS.STABILITY]; |
| 4604 | 4985 | case sources.huggingface: |
| @@ -4626,7 +5007,8 @@ function isValidState() { | ||
| 4626 | 5007 | } |
| 4627 | 5008 | } |
| 4628 | 5009 | |
| 4629 | -let buttonAbortController = null; | |
| 5010 | +/** @type {WeakMap<HTMLElement, AbortController>} */ | |
| 5011 | +const buttonAbortControllers = new WeakMap(); | |
| 4630 | 5012 | |
| 4631 | 5013 | /** |
| 4632 | 5014 | * "Paintbrush" button handler to generate a new image for a message. |
| @@ -4644,16 +5026,30 @@ async function sdMessageButton($icon, { animate } = {}) { | ||
| 4644 | 5026 | $icon.toggleClass(classes.idle, !isBusy); |
| 4645 | 5027 | $icon.toggleClass(classes.busy, isBusy); |
| 4646 | 5028 | $media.toggleClass(classes.animation, isBusy); |
| 5029 | + | |
| 5030 | + // Update generation counter toast | |
| 5031 | + const trackingFunction = isBusy ? startGenerationTracking : endGenerationTracking; | |
| 5032 | + trackingFunction(); | |
| 4647 | 5033 | } |
| 4648 | 5034 | |
| 4649 | 5035 | let $media = jQuery(); |
| 4650 | 5036 | |
| 4651 | 5037 | const classes = { busy: 'fa-hourglass', idle: 'fa-paintbrush', animation: 'fa-fade' }; |
| 4652 | 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 | + })(); | |
| 4653 | 5049 | |
| 4654 | 5050 | if ($icon.hasClass(classes.busy)) { |
| 4655 | 5051 | buttonAbortController?abortController.abort('Aborted by user'); |
| 4656 | 5052 | console.log('PreviousSD: imageImage isgeneration stillaborted beingby generated...user'); |
| 4657 | 5053 | return; |
| 4658 | 5054 | } |
| 4659 | 5055 | |
| @@ -4690,13 +5086,12 @@ async function sdMessageButton($icon, { animate } = {}) { | ||
| 4690 | 5086 | $media = messageElement.find(`.mes_media_container[data-index="${index}"]`).find('.mes_img, .mes_video'); |
| 4691 | 5087 | } |
| 4692 | 5088 | |
| 4693 | - buttonAbortController = new AbortController(); | |
| 4694 | 5089 | const newMediaAttachment = await generateMediaSwipe( |
| 4695 | 5090 | selectedMedia, |
| 4696 | 5091 | message, |
| 4697 | 5092 | () => setBusyIcon(true), |
| 4698 | 5093 | () => setBusyIcon(false), |
| 4699 | 5094 | buttonAbortControllerabortController, |
| 4700 | 5095 | ); |
| 4701 | 5096 | |
| 4702 | 5097 | if (!newMediaAttachment) { |
| @@ -4869,6 +5264,17 @@ function applyCommandArguments(args) { | ||
| 4869 | 5264 | 'denoise': 'denoising_strength', |
| 4870 | 5265 | '2ndpass': 'hr_second_pass_steps', |
| 4871 | 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 | }; |
| 4873 | 5279 | |
| 4874 | 5280 | for (const [param, setting] of Object.entries(settingMap)) { |
| @@ -4877,6 +5283,14 @@ function applyCommandArguments(args) { | ||
| 4877 | 5283 | } |
| 4878 | 5284 | currentSettings[setting] = extension_settings.sd[setting]; |
| 4879 | 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 | 5294 | const type = typeof defaultSettings[setting]; |
| 4881 | 5295 | switch (type) { |
| 4882 | 5296 | case 'boolean': |
| @@ -4999,6 +5413,17 @@ jQuery(async () => { | ||
| 4999 | 5413 | acceptsMultiple: false, |
| 5000 | 5414 | }), |
| 5001 | 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 | 5427 | name: 'seed', |
| 5003 | 5428 | description: 'random seed', |
| 5004 | 5429 | isRequired: false, |
| @@ -5243,6 +5668,8 @@ jQuery(async () => { | ||
| 5243 | 5668 | $('#sd_auto_validate').on('click', validateAutoUrl); |
| 5244 | 5669 | $('#sd_auto_url').on('input', onAutoUrlInput); |
| 5245 | 5670 | $('#sd_auto_auth').on('input', onAutoAuthInput); |
| 5671 | + $('#sd_sdcpp_validate').on('click', validateSdcppUrl); | |
| 5672 | + $('#sd_sdcpp_url').on('input', onSdcppUrlInput); | |
| 5246 | 5673 | $('#sd_drawthings_validate').on('click', validateDrawthingsUrl); |
| 5247 | 5674 | $('#sd_drawthings_url').on('input', onDrawthingsUrlInput); |
| 5248 | 5675 | $('#sd_drawthings_auth').on('input', onDrawthingsAuthInput); |
| @@ -5268,9 +5695,11 @@ jQuery(async () => { | ||
| 5268 | 5695 | $('#sd_comfy_workflow').on('change', onComfyWorkflowChange); |
| 5269 | 5696 | $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick); |
| 5270 | 5697 | $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick); |
| 5698 | + $('#sd_comfy_rename_workflow').on('click', onComfyRenameWorkflowClick); | |
| 5271 | 5699 | $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick); |
| 5272 | 5700 | $('#sd_style').on('change', onStyleSelect); |
| 5273 | 5701 | $('#sd_save_style').on('click', onSaveStyleClick); |
| 5702 | + $('#sd_rename_style').on('click', onRenameStyleClick); | |
| 5274 | 5703 | $('#sd_delete_style').on('click', onDeleteStyleClick); |
| 5275 | 5704 | $('#sd_character_prompt_block').hide(); |
| 5276 | 5705 | $('#sd_interactive_mode').on('input', onInteractiveModeInput); |
| @@ -5279,6 +5708,7 @@ jQuery(async () => { | ||
| 5279 | 5708 | $('#sd_openai_duration').on('input', onOpenAiDurationSelect); |
| 5280 | 5709 | $('#sd_multimodal_captioning').on('input', onMultimodalCaptioningInput); |
| 5281 | 5710 | $('#sd_snap').on('input', onSnapInput); |
| 5711 | + $('#sd_minimal_prompt_processing').on('input', onMinimalPromptProcessing); | |
| 5282 | 5712 | $('#sd_clip_skip').on('input', onClipSkipInput); |
| 5283 | 5713 | $('#sd_seed').on('input', onSeedInput); |
| 5284 | 5714 | $('#sd_character_prompt_share').on('input', onCharacterPromptShareInput); |
| @@ -5309,6 +5739,10 @@ jQuery(async () => { | ||
| 5309 | 5739 | extension_settings.sd.electronhub_quality = String($(this).val()); |
| 5310 | 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 | + }); | |
| 5312 | 5746 | |
| 5313 | 5747 | if (!CSS.supports('field-sizing', 'content')) { |
| 5314 | 5748 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| @@ -5337,15 +5771,19 @@ jQuery(async () => { | ||
| 5337 | 5771 | |
| 5338 | 5772 | [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { |
| 5339 | 5773 | eventSource.on(event, async (/** @type {string} */ key) => { |
| 5340 | 5774 | switchconst (key)keySourceMap = { |
| 5341 | 5775 | case[sources.bfl]: SECRET_KEYS.BFL:, |
| 5342 | 5776 | case[sources.falai]: SECRET_KEYS.FALAI:, |
| 5343 | 5777 | case[sources.stability]: SECRET_KEYS.STABILITY:, |
| 5344 | 5778 | case[sources.aimlapi]: SECRET_KEYS.AIMLAPI:, |
| 5345 | 5779 | case[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 | }); |
| 5351 | 5789 | |
| @@ -35,6 +35,10 @@ | ||
| 35 | 35 | <input id="sd_snap" type="checkbox" /> |
| 36 | 36 | <span data-i18n="sd_snap_txt">Snap auto-adjusted resolutions</span> |
| 37 | 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 | 42 | <label for="sd_source" data-i18n="Source">Source</label> |
| 39 | 43 | <select id="sd_source"> |
| 40 | 44 | <option value="aimlapi">AI/ML API</option> |
| @@ -55,10 +59,11 @@ | ||
| 55 | 59 | <option value="vlad">SD.Next (vladmandic)</option> |
| 56 | 60 | <option value="stability">Stability AI</option> |
| 57 | 61 | <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option> |
| 62 | + <option value="sdcpp">stable-diffusion.cpp server</option> | |
| 58 | 63 | <option value="horde">Stable Horde</option> |
| 59 | 64 | <option value="togetherai">TogetherAI</option> |
| 60 | 65 | <option value="xai">xAI (Grok)</option> |
| 61 | 66 | <option value="zai">Z.AI (CogView)</option> |
| 62 | 67 | </select> |
| 63 | 68 | <div data-sd-source="auto"> |
| 64 | 69 | <label for="sd_auto_url">SD Web UI URL</label> |
| @@ -76,6 +81,19 @@ | ||
| 76 | 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 | 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 | 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 | 97 | <div data-sd-source="drawthings"> |
| 80 | 98 | <label for="sd_drawthings_url">DrawThings API URL</label> |
| 81 | 99 | <div class="flex-container flexnowrap"> |
| @@ -178,7 +196,16 @@ | ||
| 178 | 196 | <option value="natural">Natural</option> |
| 179 | 197 | </select> |
| 180 | 198 | </div> |
| 181 | 199 | <div data-sd-model="dall-e-3,cogview-4gpt-250304image" 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 | 209 | <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> |
| 183 | 210 | <select id="sd_openai_quality"> |
| 184 | 211 | <option value="standard" data-i18n="Standard">Standard</option> |
| @@ -248,15 +275,23 @@ | ||
| 248 | 275 | <div id="sd_comfy_new_workflow" class="menu_button menu_button_icon" data-i18n="[title]Create new workflow" title="Create new workflow"> |
| 249 | 276 | <i class="fa-solid fa-plus"></i> |
| 250 | 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 | 281 | <div id="sd_comfy_delete_workflow" class="menu_button menu_button_icon" data-i18n="[title]Delete workflow" title="Delete workflow"> |
| 252 | 282 | <i class="fa-solid fa-trash-can"></i> |
| 253 | 283 | </div> |
| 254 | 284 | </div> |
| 255 | 285 | </div> |
| 256 | 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 | 295 | <div class="flex-container"> |
| 261 | 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 | 297 | <input id="sd_pollinations_enhance" type="checkbox" /> |
| @@ -381,12 +416,12 @@ | ||
| 381 | 416 | </div> |
| 382 | 417 | |
| 383 | 418 | <div class="flex-container"> |
| 384 | 419 | <div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy,sdcpp"> |
| 385 | 420 | <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label> |
| 386 | 421 | <select id="sd_sampler"></select> |
| 387 | 422 | </div> |
| 388 | 423 | |
| 389 | 424 | <div class="flex1" data-sd-source="comfy,auto,novel,sdcpp"> |
| 390 | 425 | <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label> |
| 391 | 426 | <select id="sd_scheduler"></select> |
| 392 | 427 | </div> |
| @@ -469,7 +504,7 @@ | ||
| 469 | 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 | 505 | </div> |
| 471 | 506 | |
| 472 | 507 | <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras,sdcpp"> |
| 473 | 508 | <small> |
| 474 | 509 | <span data-i18n="CLIP Skip">CLIP Skip</span> |
| 475 | 510 | </small> |
| @@ -523,7 +558,7 @@ | ||
| 523 | 558 | </label> |
| 524 | 559 | </div> |
| 525 | 560 | |
| 526 | 561 | <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,bfl,sdcpp" class="marginTop5"> |
| 527 | 562 | <label for="sd_seed"> |
| 528 | 563 | <span data-i18n="Seed">Seed</span> |
| 529 | 564 | <small data-i18n="(-1 for random)">(-1 for random)</small> |
| @@ -540,6 +575,9 @@ | ||
| 540 | 575 | <div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button"> |
| 541 | 576 | <i class="fa-solid fa-save"></i> |
| 542 | 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 | 581 | <div id="sd_delete_style" data-i18n="[title]Delete style" title="Delete style" class="menu_button"> |
| 544 | 582 | <i class="fa-solid fa-trash-can"></i> |
| 545 | 583 | </div> |
| @@ -207,13 +207,13 @@ class CoquiTtsProvider { | ||
| 207 | 207 | this.settings.customVoices = {}; |
| 208 | 208 | for (let voiceName in this.settings.voiceMapDict) { |
| 209 | 209 | const voiceId = this.settings.voiceMapDict[voiceName]; |
| 210 | 210 | this.settings.customVoices[voiceName] = voiceId['.model_id']; |
| 211 | 211 | |
| 212 | 212 | if (voiceId['.model_language'] != null) |
| 213 | 213 | this.settings.customVoices[voiceName] += '[' + voiceId['.model_language'] + ']'; |
| 214 | 214 | |
| 215 | 215 | if (voiceId['.model_speaker'] != null) |
| 216 | 216 | this.settings.customVoices[voiceName] += '[' + voiceId['.model_speaker'] + ']'; |
| 217 | 217 | } |
| 218 | 218 | |
| 219 | 219 | // Update UI select list with voices |
| @@ -493,8 +493,8 @@ class CoquiTtsProvider { | ||
| 493 | 493 | .append('<option value="none">Select language</option>') |
| 494 | 494 | .val('none'); |
| 495 | 495 | |
| 496 | 496 | for (let i = 0; i < model_settings['.languages'].length; i++) { |
| 497 | 497 | const language_label = JSON.stringify(model_settings['.languages'][i]).replaceAll('"', ''); |
| 498 | 498 | $('#coqui_api_model_settings_language').append(new Option(language_label, i)); |
| 499 | 499 | } |
| 500 | 500 | } |
| @@ -512,8 +512,8 @@ class CoquiTtsProvider { | ||
| 512 | 512 | .append('<option value="none">Select speaker</option>') |
| 513 | 513 | .val('none'); |
| 514 | 514 | |
| 515 | - for (let i = 0; i < model_settings['speakers'].length; i++) { | |
| 516 | - const speaker_label = JSON.stringify(model_settings['speakers'][i]).replaceAll('"', ''); | |
Diff truncated