Merge pull request #5591 from SillyTavern/staging
Create Docker Image (Release and Staging) / build (push) Waiting to run
🔄 Update Issues on Push / 🔗 Mark Linked Issues Done on Push (push) Waiting to run
⚔️ Check Merge Conflicts / ⚔️ Check Merge Conflicts (push) Waiting to run

Staging
This commit is contained in:
Cohee
2026-05-03 18:45:46 +03:00
committed by GitHub
165 changed files with 10008 additions and 2548 deletions
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3
with:
node-version: 24
- run: npm ci
- run: npm ci --omit=dev --ignore-scripts
publish-npm:
needs: build
@@ -30,5 +30,5 @@ jobs:
with:
node-version: 24
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm ci --omit=dev --ignore-scripts
- run: npm publish
+2
View File
@@ -0,0 +1,2 @@
ignore-scripts=true
min-release-age=7
+4
View File
@@ -0,0 +1,4 @@
{
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
"js/ts.tsdk.path": "./node_modules/typescript/lib"
}
+1 -1
View File
@@ -19,7 +19,7 @@ COPY --chown=node:node . ./
RUN \
echo "*** Install npm packages ***" && \
npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev && npm cache clean --force
npm ci --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts && npm cache clean --force
# Create config directory and link config.yaml. Added hardcoded dirs(constants.js?)
# that must be present for Non-Root Mode and volumeless docker runs.
+1 -1
View File
@@ -1,7 +1,7 @@
@echo off
pushd %~dp0
set NODE_ENV=production
call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev
call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
node server.js %*
pause
popd
+1 -1
View File
@@ -20,7 +20,7 @@ if %errorlevel% neq 0 (
)
)
set NODE_ENV=production
call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev
call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
node server.js %*
:end
pause
+1 -1
View File
@@ -102,7 +102,7 @@ if %errorlevel% neq 0 (
echo Installing npm packages and starting server
set NODE_ENV=production
call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev
call npm install --no-save --no-audit --no-fund --loglevel=error --no-progress --omit=dev --ignore-scripts
node server.js %*
:end
+48 -3
View File
@@ -41,6 +41,9 @@ port: 8000
# Interval in seconds to write a heartbeat file. Set to 0 to disable.
# This is used primarily for Docker healthchecks.
heartbeatInterval: 0
# Enable HTTP/HTTPS keep-alive globally.
# Disabling restores old Node 18 behavior, can help if ECONNRESET and other network errors occur.
enableKeepAlive: false
# -- SSL options --
ssl:
# Enable SSL/TLS encryption
@@ -55,7 +58,7 @@ ssl:
# -- SECURITY CONFIGURATION --
# Toggle whitelist mode
whitelistMode: true
# Whitelist will also verify IP in X-Forwarded-For / X-Real-IP headers
# When enabled, whitelist will also verify IP in headers enabled in `forwardedHeaders` section.
enableForwardedWhitelist: true
# Whitelist of allowed IP addresses
whitelist:
@@ -127,6 +130,13 @@ sso:
# as that used for authentik. (Ensure the username in authentik
# is an exact match in lowercase with that in sillytavern).
authentikAuth: false
# List of trusted proxy IPs for SSO authentication.
# Supports wildcards or CIDR notation for subnets.
# Example: ['127.0.0.1', '192.168.1.1']
# Set to ['*'] to trust all proxies (NOT RECOMMENDED unless you have other security measures in place)
trustedProxies:
- ::1
- 127.0.0.1
# Host whitelist configuration. Recommended if you're using a listen mode
hostWhitelist:
@@ -141,6 +151,26 @@ hostWhitelist:
# - .trycloudflare.com
hosts: []
# Perform whitelist checks against server-side HTTP requests that resolve to private IP addresses.
# This is an additional layer of security to prevent Server-Side Request Forgery (SSRF) attacks.
# Recommended when listen mode is enabled, or if your server is accessible by untrusted users.
privateAddressWhitelist:
# Enable private address whitelist to block requests to private IP ranges.
enabled: false
# If true, requests to hosts that cannot be resolved will be allowed instead of blocked.
allowUnresolvedHosts: false
# Log blocked and allowed requests to the console.
log:
# Log blocked requests to the console with a warning message
blockedRequests: true
# Log allowed requests to the console with an info message
allowedRequests: false
# List of allowed private IP ranges (in CIDR notation or wildcard format).
# Allows loopback IP ranges by default, but you can customize this list to fit your needs.
allowedRanges:
- '127.0.0.0/8' # Loopback (IPv4)
- '::1/128' # Loopback (IPv6)
# User session timeout *in seconds* (defaults to 24 hours).
## Set to a positive number to expire session after a certain time of inactivity
## Set to 0 to expire session when the browser is closed
@@ -159,9 +189,24 @@ logging:
minLogLevel: 0
# -- RATE LIMITING CONFIGURATION --
rateLimiting:
# Use X-Real-IP header instead of socket IP for rate limiting
# Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy)
# Use any of the enabled headers in the `forwardedHeaders` section to identify the client IP for rate limiting.
# If disabled, only the socket IP will be used, which may not work correctly if you are behind a reverse proxy.
preferRealIpHeader: false
# Set the maximum number of allowed failed basic authentication attempts before rate limiting is applied. Set to 0 to disable rate limiting for basic auth.
basicAuthMaxAttempts: 5
# Set the maximum number of allowed failed account login attempts before rate limiting is applied. Set to 0 to disable rate limiting for account logins.
accountsLoginMaxAttempts: 5
# Set the maximum number of allowed failed account recovery attempts before rate limiting is applied. Set to 0 to disable rate limiting for account recovery.
accountsRecoverMaxAttempts: 5
# Set to true to enable support for real IPs in certain request headers for features like IP whitelisting, rate limiting and access logging.
# Only change if you are sure that you use a correctly configured reverse proxy, otherwise this may lead to IP spoofing.
forwardedHeaders:
# X-Real-IP header (common with Nginx and Caddy)
xRealIp: true
# X-Forwarded-For header (common with many proxies, but may contain multiple IPs - only the first one will be used)
xForwardedFor: true
# CF-Connecting-IP header (used by Cloudflare Tunnels)
cfConnectingIp: false
## BACKUP CONFIGURATION
backups:
@@ -2,11 +2,11 @@
<html>
<head>
<title>Not found</title>
<title>Not Found</title>
</head>
<body>
<h1>Not found</h1>
<h1>Not Found</h1>
<p>
The requested URL was not found on this server.
</p>
+32
View File
@@ -642,5 +642,37 @@
{
"filename": "presets/reasoning/Think XML.json",
"type": "reasoning"
},
{
"filename": "presets/reasoning/Gemma 4.json",
"type": "reasoning"
},
{
"filename": "presets/instruct/Gemma 4.json",
"type": "instruct"
},
{
"filename": "presets/context/Gemma 4.json",
"type": "context"
},
{
"filename": "user.css",
"type": "stylesheet"
},
{
"filename": "errors/forbidden-by-whitelist.html",
"type": "error_page"
},
{
"filename": "errors/host-not-allowed.html",
"type": "error_page"
},
{
"filename": "errors/unauthorized.html",
"type": "error_page"
},
{
"filename": "errors/url-not-found.html",
"type": "error_page"
}
]
@@ -0,0 +1,14 @@
{
"story_string": "{{#if anchorBefore}}{{anchorBefore}}\n{{/if}}{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{#if anchorAfter}}{{anchorAfter}}\n{{/if}}{{trim}}",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"names_as_stop_strings": true,
"story_string_position": 0,
"story_string_depth": 1,
"story_string_role": 0,
"always_force_name2": true,
"trim_sentences": false,
"single_line": false,
"name": "Gemma 4"
}
@@ -0,0 +1,25 @@
{
"input_sequence": "<|turn>user\n",
"output_sequence": "<|turn>model\n",
"last_output_sequence": "",
"system_sequence": "<|turn>system\n",
"stop_sequence": "<turn|>",
"wrap": false,
"macro": true,
"names_behavior": "force",
"activation_regex": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "<turn|>\n",
"input_suffix": "<turn|>\n",
"system_suffix": "<turn|>\n",
"user_alignment_message": "",
"system_same_as_user": false,
"last_system_sequence": "",
"first_input_sequence": "",
"last_input_sequence": "",
"sequences_as_stop_strings": true,
"story_string_prefix": "<|turn>system\n",
"story_string_suffix": "<turn|>\n",
"name": "Gemma 4"
}
@@ -10,6 +10,8 @@
"mistralai_model": "mistral-large-latest",
"chutes_model": "deepseek-ai/DeepSeek-V3-0324",
"chutes_sort_models": "alphabetically",
"minimax_model": "MiniMax-M2.7",
"minimax_endpoint": "global",
"electronhub_model": "gpt-4o-mini",
"electronhub_sort_models": "alphabetically",
"electronhub_group_models": false,
@@ -0,0 +1,6 @@
{
"name": "Gemma 4",
"prefix": "<|channel>thought\n",
"suffix": "<channel|>",
"separator": "\n\n"
}
+3 -3
View File
@@ -1,6 +1,6 @@
#!/bin/sh
# Function to handle startup logic (Config check + Postinstall + Start)
# Function to handle startup logic (Config check + init + Start)
start_sillytavern() {
local PREFIX="$1"
shift # Remove the first argument (PREFIX) so $@ contains the rest
@@ -11,8 +11,8 @@ start_sillytavern() {
$PREFIX cp "default/config.yaml" "config/config.yaml"
fi
# Execute postinstall to auto-populate config.yaml with missing values
$PREFIX npm run postinstall
# Execute init script to auto-populate config.yaml with missing values
$PREFIX npm run init
# Start the server
exec $PREFIX node server.js --listen "$@"
Vendored
+4
View File
@@ -41,6 +41,10 @@ declare global {
* Authenticated user handle.
*/
handle: string | null;
/**
* Account version tag: shake256 derivative of password hash and salt.
*/
version: string | null;
/**
* Last time the session was extended.
*/
+80 -80
View File
@@ -1,13 +1,12 @@
{
"name": "sillytavern",
"version": "1.17.0",
"version": "1.18.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.17.0",
"hasInstallScript": true,
"version": "1.18.0",
"license": "AGPL-3.0",
"dependencies": {
"@adobe/css-tools": "^4.4.4",
@@ -39,13 +38,14 @@
"@mozilla/readability": "^0.6.0",
"@popperjs/core": "^2.11.8",
"@zeldafan0225/ai_horde": "^5.2.0",
"agent-base": "^7.1.3",
"archiver": "^7.0.1",
"bing-translate-api": "^4.1.0",
"body-parser": "^1.20.2",
"bowser": "^2.12.1",
"bytes": "^3.1.2",
"chalk": "^5.6.0",
"chevrotain": "^11.1.1",
"chevrotain": "^11.2.0",
"command-exists": "^1.2.9",
"compression": "^1.8.1",
"cookie-parser": "^1.4.6",
@@ -54,7 +54,7 @@
"crc": "^4.3.2",
"csrf-sync": "^4.2.1",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.2.6",
"dompurify": "^3.4.2",
"droll": "^0.2.1",
"env-paths": "^3.0.0",
"express": "^4.21.0",
@@ -74,8 +74,9 @@
"ipaddr.js": "^2.2.0",
"is-docker": "^3.0.0",
"isomorphic-git": "^1.36.3",
"js-sha256": "^0.11.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"lodash": "^4.18.1",
"mime-types": "^3.0.2",
"moment": "^2.30.1",
"morphdom": "^2.7.7",
@@ -109,7 +110,7 @@
"sillytavern": "src/server-global.js"
},
"devDependencies": {
"@chevrotain/types": "^11.0.3",
"@chevrotain/types": "^11.2.0",
"@types/archiver": "^6.0.3",
"@types/bytes": "^3.1.5",
"@types/command-exists": "^1.2.3",
@@ -123,7 +124,7 @@
"@types/jquery-cropper": "^1.0.4",
"@types/jquery.transit": "^0.9.33",
"@types/jqueryui": "^1.12.24",
"@types/lodash": "^4.17.20",
"@types/lodash": "^4.17.24",
"@types/mime-types": "^3.0.1",
"@types/multer": "^2.1.0",
"@types/node": "^18.19.84",
@@ -139,7 +140,8 @@
"eslint": "^8.57.1",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-jsdoc": "^48.10.0",
"eslint-plugin-playwright": "^2.3.0"
"eslint-plugin-playwright": "^2.3.0",
"typescript": "^5.9.3"
},
"engines": {
"node": ">= 20"
@@ -177,42 +179,42 @@
"license": "Apache-2.0"
},
"node_modules/@chevrotain/cst-dts-gen": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz",
"integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==",
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.2.0.tgz",
"integrity": "sha512-ssJFvn/UXhQQeICw3SR/fZPmYVj+JM2mP+Lx7bZ51cOeHaMWOKp3AUMuyM3QR82aFFXTfcAp67P5GpPjGmbZWQ==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/gast": "11.1.1",
"@chevrotain/types": "11.1.1",
"@chevrotain/gast": "11.2.0",
"@chevrotain/types": "11.2.0",
"lodash-es": "4.17.23"
}
},
"node_modules/@chevrotain/gast": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz",
"integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==",
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.2.0.tgz",
"integrity": "sha512-c+KoD6eSI1xjAZZoNUW+V0l13UEn+a4ShmUrjIKs1BeEWCji0Kwhmqn5FSx1K4BhWL7IQKlV7wLR4r8lLArORQ==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/types": "11.1.1",
"@chevrotain/types": "11.2.0",
"lodash-es": "4.17.23"
}
},
"node_modules/@chevrotain/regexp-to-ast": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz",
"integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==",
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.2.0.tgz",
"integrity": "sha512-lG73pBFqbXODTbXhdZwv0oyUaI+3Irm+uOv5/W79lI3g5hasYaJnVJOm3H2NkhA0Ef4XLBU4Scr7TJDJwgFkAw==",
"license": "Apache-2.0"
},
"node_modules/@chevrotain/types": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz",
"integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==",
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.2.0.tgz",
"integrity": "sha512-vBMSj/lz/LqolbGQEHB0tlpW5BnljHVtp+kzjQfQU+5BtGMTuZCPVgaAjtKvQYXnHb/8i/02Kii00y0tsuwfsw==",
"license": "Apache-2.0"
},
"node_modules/@chevrotain/utils": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz",
"integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==",
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.2.0.tgz",
"integrity": "sha512-+7whECg4yNWHottjvr2To2BRxL4XJVjIyyv5J4+bJ0iMOVU8j/8n1qPDLZS/90W/BObDR8VNL46lFbzY/Hosmw==",
"license": "Apache-2.0"
},
"node_modules/@es-joy/jsdoccomment": {
@@ -549,7 +551,6 @@
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "^0.22.12"
}
@@ -1011,7 +1012,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.0.tgz",
"integrity": "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "1.6.0",
"@jimp/types": "1.6.0",
@@ -1043,7 +1043,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz",
"integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -1139,7 +1138,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz",
"integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -1176,7 +1174,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz",
"integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12",
"tinycolor2": "^1.6.0"
@@ -1220,7 +1217,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz",
"integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -1308,7 +1304,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz",
"integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -1321,7 +1316,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz",
"integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -1922,7 +1916,6 @@
"integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -2018,9 +2011,9 @@
}
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"dev": true,
"license": "MIT"
},
@@ -2612,7 +2605,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2799,9 +2791,9 @@
}
},
"node_modules/archiver-utils/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -3021,14 +3013,23 @@
}
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
"proxy-from-env": "^2.1.0"
}
},
"node_modules/axios/node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/b4a": {
@@ -3083,9 +3084,9 @@
}
},
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -3190,9 +3191,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3232,7 +3233,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3503,16 +3503,16 @@
}
},
"node_modules/chevrotain": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz",
"integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==",
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.2.0.tgz",
"integrity": "sha512-mHCHTxM51nCklUw9RzRVc0DLjAh/SAUPM4k/zMInlTIo25ldWXOZoPt7XEIk/LwoT4lFVmJcu9g5MHtx371x3A==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/cst-dts-gen": "11.1.1",
"@chevrotain/gast": "11.1.1",
"@chevrotain/regexp-to-ast": "11.1.1",
"@chevrotain/types": "11.1.1",
"@chevrotain/utils": "11.1.1",
"@chevrotain/cst-dts-gen": "11.2.0",
"@chevrotain/gast": "11.2.0",
"@chevrotain/regexp-to-ast": "11.2.0",
"@chevrotain/types": "11.2.0",
"@chevrotain/utils": "11.2.0",
"lodash-es": "4.17.23"
}
},
@@ -4322,13 +4322,10 @@
}
},
"node_modules/dompurify": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz",
"integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -4567,7 +4564,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -5209,9 +5205,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
@@ -6378,6 +6374,12 @@
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause"
},
"node_modules/js-sha256": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz",
"integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==",
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -6614,9 +6616,9 @@
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
@@ -7926,9 +7928,9 @@
}
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -8125,7 +8127,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -9143,7 +9144,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
+11 -8
View File
@@ -29,13 +29,14 @@
"@mozilla/readability": "^0.6.0",
"@popperjs/core": "^2.11.8",
"@zeldafan0225/ai_horde": "^5.2.0",
"agent-base": "^7.1.3",
"archiver": "^7.0.1",
"bing-translate-api": "^4.1.0",
"body-parser": "^1.20.2",
"bowser": "^2.12.1",
"bytes": "^3.1.2",
"chalk": "^5.6.0",
"chevrotain": "^11.1.1",
"chevrotain": "^11.2.0",
"command-exists": "^1.2.9",
"compression": "^1.8.1",
"cookie-parser": "^1.4.6",
@@ -44,7 +45,7 @@
"crc": "^4.3.2",
"csrf-sync": "^4.2.1",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.2.6",
"dompurify": "^3.4.2",
"droll": "^0.2.1",
"env-paths": "^3.0.0",
"express": "^4.21.0",
@@ -64,8 +65,9 @@
"ipaddr.js": "^2.2.0",
"is-docker": "^3.0.0",
"isomorphic-git": "^1.36.3",
"js-sha256": "^0.11.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"lodash": "^4.18.1",
"mime-types": "^3.0.2",
"moment": "^2.30.1",
"morphdom": "^2.7.7",
@@ -113,8 +115,9 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.17.0",
"version": "1.18.0",
"scripts": {
"init": "node src/server-init.js",
"start": "node server.js",
"debug": "node --inspect server.js",
"start:global": "node server.js --global",
@@ -122,7 +125,6 @@
"start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
"start:bun": "bun server.js",
"start:no-csrf": "node server.js --disableCsrf",
"postinstall": "node post-install.js",
"lint": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js",
"lint:fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix",
"plugins:update": "node plugins update",
@@ -137,7 +139,7 @@
},
"main": "server.js",
"devDependencies": {
"@chevrotain/types": "^11.0.3",
"@chevrotain/types": "^11.2.0",
"@types/archiver": "^6.0.3",
"@types/bytes": "^3.1.5",
"@types/command-exists": "^1.2.3",
@@ -151,7 +153,7 @@
"@types/jquery-cropper": "^1.0.4",
"@types/jquery.transit": "^0.9.33",
"@types/jqueryui": "^1.12.24",
"@types/lodash": "^4.17.20",
"@types/lodash": "^4.17.24",
"@types/mime-types": "^3.0.1",
"@types/multer": "^2.1.0",
"@types/node": "^18.19.84",
@@ -167,6 +169,7 @@
"eslint": "^8.57.1",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-jsdoc": "^48.10.0",
"eslint-plugin-playwright": "^2.3.0"
"eslint-plugin-playwright": "^2.3.0",
"typescript": "^5.9.3"
}
}
-114
View File
@@ -1,114 +0,0 @@
/**
* Scripts to be done before starting the server for the first time.
*/
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import yaml from 'yaml';
import chalk from 'chalk';
import { createRequire } from 'node:module';
import { addMissingConfigValues } from './src/config-init.js';
/**
* Colorizes console output.
*/
const color = chalk;
/**
* Converts the old config.conf file to the new config.yaml format.
*/
function convertConfig() {
if (fs.existsSync('./config.conf')) {
if (fs.existsSync('./config.yaml')) {
console.log(color.yellow('Both config.conf and config.yaml exist. Please delete config.conf manually.'));
return;
}
try {
console.log(color.blue('Converting config.conf to config.yaml. Your old config.conf will be renamed to config.conf.bak'));
fs.renameSync('./config.conf', './config.conf.cjs'); // Force loading as CommonJS
const require = createRequire(import.meta.url);
const config = require(path.join(process.cwd(), './config.conf.cjs'));
fs.copyFileSync('./config.conf.cjs', './config.conf.bak');
fs.rmSync('./config.conf.cjs');
fs.writeFileSync('./config.yaml', yaml.stringify(config));
console.log(color.green('Conversion successful. Please check your config.yaml and fix it if necessary.'));
} catch (error) {
console.error(color.red('FATAL: Config conversion failed. Please check your config.conf file and try again.'), error);
return;
}
}
}
/**
* Creates the default config files if they don't exist yet.
*/
function createDefaultFiles() {
/**
* @typedef DefaultItem
* @type {object}
* @property {'file' | 'directory'} type - Whether the item should be copied as a single file or merged into a directory structure.
* @property {string} defaultPath - The path to the default item (typically in `default/`).
* @property {string} productionPath - The path to the copied item for production use.
*/
/** @type {DefaultItem[]} */
const defaultItems = [
{
type: 'file',
defaultPath: './default/config.yaml',
productionPath: './config.yaml',
},
{
type: 'directory',
defaultPath: './default/public/',
productionPath: './public/',
},
];
for (const defaultItem of defaultItems) {
try {
if (defaultItem.type === 'file') {
if (!fs.existsSync(defaultItem.productionPath)) {
fs.copyFileSync(
defaultItem.defaultPath,
defaultItem.productionPath,
);
console.log(
color.green(`Created default file: ${defaultItem.productionPath}`),
);
}
} else if (defaultItem.type === 'directory') {
fs.cpSync(defaultItem.defaultPath, defaultItem.productionPath, {
force: false, // Don't overwrite existing files!
recursive: true,
});
console.log(
color.green(`Synchronized missing files: ${defaultItem.productionPath}`),
);
} else {
throw new Error(
'FATAL: Unexpected default file format in `post-install.js#createDefaultFiles()`.',
);
}
} catch (error) {
console.error(
color.red(
`FATAL: Could not write default ${defaultItem.type}: ${defaultItem.productionPath}`,
),
error,
);
}
}
}
try {
// 0. Convert config.conf to config.yaml
convertConfig();
// 1. Create default config files
createDefaultFiles();
// 2. Add missing config values
addMissingConfigValues(path.join(process.cwd(), './config.yaml'));
} catch (error) {
console.error(error);
}
+7
View File
@@ -0,0 +1,7 @@
# Looking for user.css?
user.css is now located under your data root directory in the "_css" folder.
Example for the default data root:
/data/_css/user.css
+11 -1
View File
@@ -97,13 +97,23 @@ label[for="extensions_autoconnect"] {
font-size: 1.05em;
}
.extensions_info .extension_version {
.extensions_info :is(.extension_version, .extension_author) {
opacity: 0.8;
font-size: 0.8em;
font-weight: normal;
margin-left: 2px;
}
.extensions_info :is(.extension_version, .extension_author):empty {
display: none;
}
.extensions_info .extension_author {
display: inline-flex;
gap: 2px;
align-items: baseline;
}
.extensions_info .extension_block a {
color: var(--SmartThemeBodyColor);
}
+5
View File
@@ -500,6 +500,11 @@
.horde_multiple_hint {
display: none;
}
select[multiple] {
max-height: 50px;
overflow-y: auto;
}
}
/*landscape mode phones and ipads*/
+236
View File
@@ -0,0 +1,236 @@
/* ─────────────────────────────────────────────────────────────────────────────
Streaming Display — floating toast panel for live LLM generation output.
Shows reasoning (thinking) and content as they stream in.
Used by extensions that leverage ConnectionManagerRequestService streaming.
───────────────────────────────────────────────────────────────────────────── */
.streaming-display {
position: fixed;
bottom: max(calc(var(--bottomFormBlockSize) + 5px), 20px);
right: 20px;
width: min(550px, calc(100vw - 40px));
max-height: 70vh;
background: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 10px;
padding: 14px;
z-index: 9000;
display: flex;
flex-direction: column;
gap: 10px;
opacity: 0;
transform: translateY(20px);
transition: opacity var(--animation-duration, 125ms) ease, transform var(--animation-duration, 125ms) ease;
overflow: hidden;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(12px);
}
.streaming-display-visible {
opacity: 1;
transform: translateY(0);
}
/* Header label with animated activity indicator */
.streaming-display-label {
font-weight: 600;
font-size: 0.95em;
color: var(--SmartThemeBodyColor);
display: flex;
align-items: center;
gap: 8px;
user-select: none;
}
/* LED status indicator - pulsing while streaming */
.streaming-display-led {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: rgb(225, 138, 36);
animation: streaming-display-pulse 1.5s ease-in-out infinite;
flex-shrink: 0;
}
/* Completed state: solid green LED */
.streaming-display-complete .streaming-display-led {
background: #4caf50;
animation: none;
opacity: 1;
box-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
}
/* Stopped state: solid red LED */
.streaming-display-stopped .streaming-display-led {
background: #f44336;
animation: none;
opacity: 1;
box-shadow: 0 0 8px rgba(244, 67, 54, 0.6);
}
@keyframes streaming-display-pulse {
0%, 100% { opacity: 0.4; transform: scale(0.9); }
50% { opacity: 1; transform: scale(1.1); }
}
/* Label text takes available space */
.streaming-display-label-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Window control buttons container */
.streaming-display-controls {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
flex-shrink: 0;
}
/* Window control buttons */
.streaming-display-btn {
width: 22px;
height: 22px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--SmartThemeBodyColor);
font-size: 14px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.6;
transition: opacity 0.15s ease, background-color 0.15s ease;
}
.streaming-display-btn:hover {
opacity: 1;
background-color: rgba(255, 255, 255, 0.1);
}
.streaming-display-btn-close:hover {
background-color: rgba(244, 67, 54, 0.2);
}
.streaming-display-btn-stop {
font-size: 10px;
}
.streaming-display-btn-stop:hover {
background-color: rgba(244, 150, 36, 0.2);
color: rgb(225, 138, 36);
}
/* Content container - collapsible for minimize */
.streaming-display-content {
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
transition: max-height var(--animation-duration, 125ms) ease, opacity var(--animation-duration, 125ms) ease;
}
/* Minimized state - hide content sections */
.streaming-display-minimized .streaming-display-content {
max-height: 0;
opacity: 0;
}
.streaming-display-minimized {
gap: 0;
}
/* Model/API icon in the label */
.streaming-display-icon {
width: 1.1em;
height: 1.1em;
flex-shrink: 0;
}
/* Minimized state adjustments */
.streaming-display-minimized.streaming-display {
padding: 10px 14px;
}
/* Reasoning (thinking) section */
.streaming-display-reasoning {
background: color-mix(in srgb, var(--SmartThemeBodyColor) 5%, transparent);
border-radius: 6px;
padding: 8px 10px;
border-left: 3px solid color-mix(in srgb, var(--SmartThemeBodyColor) 25%, transparent);
}
.streaming-display-reasoning-label {
font-size: 0.8em;
font-weight: 600;
opacity: 0.5;
margin-bottom: 4px;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.streaming-display-reasoning-content {
font-size: 0.82em;
opacity: 0.65;
max-height: 25vh;
overflow-y: auto;
word-break: break-word;
line-height: 1.45;
scrollbar-width: thin;
}
.streaming-display-reasoning-content p {
margin: 0.3em 0;
}
.streaming-display-reasoning-content p:first-child {
margin-top: 0;
}
.streaming-display-reasoning-content p:last-child {
margin-bottom: 0;
}
/* Main content section */
.streaming-display-text {
border-top: 1px solid color-mix(in srgb, var(--SmartThemeBorderColor) 50%, transparent);
padding-top: 8px;
min-height: 1.5em;
}
.streaming-display-text-content {
font-size: 0.9em;
color: var(--SmartThemeBodyColor);
max-height: 40vh;
overflow-y: auto;
word-break: break-word;
line-height: 1.5;
scrollbar-width: thin;
}
.streaming-display-text-content p {
margin: 0.4em 0;
}
.streaming-display-text-content p:first-child {
margin-top: 0;
}
.streaming-display-text-content p:last-child {
margin-bottom: 0;
}
/* Strip auto-added quotes from <q> tags, as message formatting adds them */
.streaming-display-reasoning-content q:before,
.streaming-display-reasoning-content q:after,
.streaming-display-text-content q:before,
.streaming-display-text-content q:after {
content: '';
}
+4
View File
@@ -565,6 +565,10 @@ label[for="bind_preset_to_connection"]:has(input:checked) {
display: none;
}
#openai_settings:has(#openai_function_calling:not(:checked)) #tool_call_recurse_limit_block {
display: none;
}
#adaptive_p_block:has([data-tg-samplers="adaptive_target"][style*="display: none"]):has([data-tg-samplers="adaptive_decay"][style*="display: none"]) {
display: none;
}
+1
View File
@@ -17,6 +17,7 @@
.welcomePanel.recentHidden .welcomeRecent,
.welcomePanel.recentHidden .recentChatsTitle,
.welcomePanel.recentHidden .hideRecentChats,
.welcomePanel.recentHidden .recentChatsSettings,
.welcomePanel:not(.recentHidden) .showRecentChats {
display: none;
}
+4 -76
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

+5 -46
View File
@@ -1,48 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
width="431.92484pt"
height="430.70734pt"
viewBox="0 0 431.92484 430.70734"
preserveAspectRatio="xMidYMid"
id="svg8"
sodipodi:docname="koboldhorde.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs12" />
<sodipodi:namedview
id="namedview10"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="pt"
showgrid="false"
inkscape:zoom="1.5043945"
inkscape:cx="237.63713"
inkscape:cy="285.82928"
inkscape:window-width="2560"
inkscape:window-height="1377"
inkscape:window-x="5112"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<g
transform="matrix(0.1,0,0,-0.1,-40.075165,470.78832)"
stroke="none"
id="g6">
<path
d="m 2495,4698 c -3,-7 -6,-76 -7,-153 l -3,-140 -115,-5 c -63,-3 -114,-9 -114,-14 1,-4 34,-41 74,-82 l 72,-74 -4,-125 -3,-124 -65,-67 c -74,-75 -224,-205 -257,-223 -27,-14 -54,-3 -89,38 -22,26 -24,26 -156,26 -132,0 -135,0 -172,28 -52,40 -60,67 -41,135 9,30 14,56 11,59 -7,7 -47,-16 -77,-43 l -25,-24 -18,41 c -10,23 -39,56 -68,80 -63,49 -110,53 -160,12 -58,-49 -67,-65 -68,-118 0,-49 2,-53 63,-113 62,-61 63,-62 118,-62 h 56 l 72,-80 73,-80 h 51 c 61,0 66,-3 92,-53 11,-21 46,-69 77,-105 l 58,-67 -320,-3 -321,-2 -55,26 c -47,22 -67,42 -131,125 -79,102 -128,139 -186,139 -23,0 -27,-4 -27,-28 0,-61 91,-163 177,-198 64,-27 97,-59 167,-162 36,-55 75,-107 86,-117 24,-22 80,-108 80,-123 0,-6 -15,-35 -34,-64 -18,-29 -66,-113 -106,-185 l -72,-133 h -80 -79 l 2,80 c 1,44 0,83 -4,86 -3,3 -31,-8 -63,-24 -67,-33 -197,-82 -220,-82 -9,0 -34,-13 -57,-29 -36,-25 -55,-30 -129,-36 -77,-6 -87,-9 -92,-28 -13,-48 -15,-47 157,-47 90,0 232,-3 315,-7 l 152,-6 v -39 -38 h 281 280 l 57,47 c 31,25 80,56 109,69 87,39 78,44 -89,45 -84,1 -232,4 -328,8 l -175,6 55,28 c 30,15 63,27 72,27 16,0 58,44 58,61 0,17 92,150 113,164 12,7 57,17 100,21 l 77,7 62,-43 c 49,-35 72,-45 115,-48 31,-3 53,0 53,5 0,11 -93,102 -156,152 l -42,32 -4,90 c -2,50 -9,94 -16,101 -7,7 -12,16 -12,20 0,4 56,8 123,8 68,0 212,3 319,7 l 195,6 -56,50 c -72,65 -71,63 -71,125 0,29 -7,69 -15,89 -8,19 -15,37 -15,39 0,2 28,4 61,4 57,0 65,3 110,41 l 49,41 v 69 70 l 55,51 c 106,98 95,132 95,-292 v -371 l -45,-42 -44,-42 -2,-194 c -2,-219 -6,-209 76,-210 22,-1 50,-8 62,-16 18,-13 23,-13 30,-2 5,9 33,16 71,20 35,3 66,8 69,11 3,3 8,93 11,199 l 5,194 -47,32 -46,32 v 202 c 0,111 -3,281 -7,377 l -6,174 39,-31 c 107,-86 106,-85 111,-143 7,-89 10,-96 65,-134 43,-29 62,-36 101,-36 h 48 l -5,-32 c -3,-18 -8,-64 -10,-103 l -5,-70 -56,-50 c -30,-28 -55,-53 -55,-55 0,-3 78,-3 172,-2 95,2 237,1 315,-3 l 142,-7 -14,-29 c -9,-17 -15,-56 -15,-97 0,-85 -1,-86 -161,-228 -66,-58 -118,-111 -116,-118 9,-27 130,16 213,76 45,32 47,33 164,34 l 118,2 78,-114 c 44,-62 79,-122 79,-131 0,-16 -18,-19 -205,-24 -113,-3 -238,-7 -277,-8 -40,0 -73,-5 -73,-11 0,-5 21,-18 48,-30 26,-12 81,-43 123,-70 l 75,-50 h 316 316 l 9,38 9,37 58,3 c 53,3 58,1 51,-15 -4,-10 -11,-39 -15,-66 -5,-35 -15,-57 -40,-82 -40,-41 -33,-57 13,-30 18,11 79,35 137,55 l 105,35 9,50 c 5,28 14,53 20,57 6,4 52,8 103,8 h 91 l 11,31 c 6,18 11,33 11,34 0,2 -46,4 -103,7 -96,3 -106,5 -152,36 -27,18 -67,37 -89,43 -44,12 -112,44 -136,64 -8,7 -27,15 -42,17 -27,5 -27,5 -34,-73 l -7,-79 h -89 -89 l -57,118 c -31,64 -69,137 -84,161 -22,36 -28,57 -28,102 0,56 2,60 54,116 29,32 81,98 116,147 49,69 76,96 122,125 138,84 191,138 202,204 l 7,37 h -41 c -59,0 -88,-19 -159,-104 -65,-78 -120,-127 -181,-165 -34,-20 -46,-21 -362,-21 -192,0 -328,4 -328,9 0,5 19,24 42,42 22,18 61,64 85,101 44,68 44,68 90,68 44,0 49,3 134,86 84,82 89,86 122,80 49,-9 84,7 145,67 50,49 52,53 52,105 0,65 -10,84 -62,122 -61,45 -119,42 -165,-6 -18,-20 -43,-54 -54,-75 -10,-21 -22,-39 -26,-39 -3,0 -20,9 -37,19 -17,11 -40,21 -50,23 -17,3 -19,-3 -17,-57 1,-46 -3,-68 -19,-93 -31,-51 -68,-61 -191,-55 -131,7 -152,2 -179,-42 -12,-19 -29,-35 -38,-35 -17,0 -405,306 -429,337 -7,10 -13,45 -13,80 v 63 h 89 c 53,0 93,4 96,11 4,6 -4,22 -16,36 -32,34 -134,120 -150,125 -9,3 -13,49 -13,180 -1,97 -3,178 -6,181 -3,3 -33,8 -67,12 -49,5 -64,4 -68,-7 z"
id="path2" />
<path
d="m 2166,2764 c -3,-9 -6,-59 -6,-112 v -97 l -49,-42 -50,-43 h -145 -146 v -110 -110 h -25 c -17,0 -43,-16 -75,-46 -53,-49 -96,-63 -164,-50 -30,6 -42,18 -89,89 -30,45 -61,97 -70,114 -16,30 -20,31 -83,35 -36,1 -68,0 -71,-5 -2,-4 25,-67 61,-140 36,-72 68,-150 72,-173 6,-38 3,-44 -33,-80 -21,-21 -62,-58 -91,-82 -29,-24 -61,-60 -71,-80 -26,-49 -106,-125 -161,-152 -56,-28 -116,-83 -141,-132 -36,-70 -25,-84 51,-64 48,12 64,25 154,118 149,155 126,148 509,148 174,0 317,-4 317,-8 0,-5 -14,-21 -31,-38 -17,-16 -53,-63 -81,-104 l -50,-75 -57,-6 c -54,-7 -58,-9 -122,-79 l -67,-73 -58,6 c -71,8 -88,0 -149,-71 -34,-41 -45,-62 -45,-88 0,-51 23,-98 64,-129 31,-24 43,-27 85,-23 73,7 121,50 145,130 5,16 8,16 48,-2 63,-29 69,-27 62,20 -6,34 -2,45 20,75 48,63 67,70 182,66 134,-3 172,4 180,35 13,52 26,48 147,-43 63,-49 167,-130 231,-182 l 116,-94 v -57 -57 l -86,-6 c -48,-3 -89,-8 -91,-11 -3,-3 26,-33 63,-68 136,-125 121,-89 123,-288 1,-96 4,-176 6,-179 3,-2 22,12 44,32 22,19 51,43 65,52 25,17 26,20 26,126 v 109 h 119 c 66,0 122,4 125,9 3,5 -33,46 -80,90 l -86,82 7,109 c 8,130 17,146 140,256 43,38 100,90 127,117 64,61 94,63 137,8 l 32,-40 145,-3 146,-3 30,-34 c 27,-30 30,-39 26,-82 -5,-48 -4,-49 21,-48 14,1 40,4 56,8 26,5 32,2 45,-21 8,-15 32,-46 53,-68 35,-36 44,-40 86,-40 38,0 57,7 90,30 45,33 61,63 61,116 0,27 -11,46 -52,91 -66,74 -63,73 -127,73 -53,0 -55,1 -132,75 l -79,75 h -79 c -51,0 -82,4 -89,13 -5,6 -15,38 -22,70 -8,38 -22,70 -41,92 -30,33 -30,34 -9,45 14,8 116,10 328,8 355,-3 350,-2 442,-101 150,-161 142,-154 194,-161 64,-10 75,1 51,55 -26,56 -86,105 -154,125 -45,13 -65,26 -101,67 -25,28 -75,83 -111,122 -37,38 -86,101 -109,140 l -42,69 32,56 c 40,70 140,277 140,291 0,10 -40,6 -112,-12 -30,-8 -42,-18 -59,-52 -28,-54 -75,-125 -107,-159 -49,-53 -158,-50 -221,7 -21,19 -49,38 -63,44 -21,8 -26,18 -31,63 -4,29 -7,78 -7,110 v 56 l -160,4 -159,3 -39,38 -39,38 -7,112 c -4,61 -8,113 -11,115 -8,9 -40,-21 -195,-186 -89,-95 -170,-177 -181,-183 -30,-15 -11,-34 -263,249 -60,67 -112,122 -116,122 -4,0 -11,-7 -14,-16 z m 388,-524 c 4,0 59,50 121,110 62,61 117,110 123,110 16,0 72,-75 72,-96 0,-18 -290,-304 -308,-304 -11,0 -326,287 -330,301 -4,10 41,75 65,95 10,9 38,-13 131,-102 64,-62 121,-114 126,-114 z m -759,-97 c 4,-21 7,-44 8,-52 2,-11 31,-14 143,-15 l 141,-1 69,-60 c 37,-33 128,-118 201,-190 l 133,-130 v -230 c 0,-218 -4,-275 -19,-275 -3,0 -35,24 -71,53 l -65,53 -12,79 c -6,44 -12,81 -12,81 -1,1 -17,14 -37,29 -25,20 -54,30 -93,34 -42,5 -55,10 -52,21 2,8 7,51 12,95 l 8,80 51,47 c 29,25 49,51 45,57 -4,7 -93,11 -268,12 -144,1 -295,4 -335,8 l -73,6 45,43 c 45,43 46,45 46,105 0,61 1,63 56,125 31,34 60,62 64,62 5,0 12,-17 15,-37 z m 1608,-34 47,-41 v -82 c 0,-69 3,-86 20,-104 12,-12 25,-22 31,-22 6,0 8,-4 5,-9 -3,-5 -71,-12 -150,-16 l -144,-7 -159,-148 c -87,-81 -187,-171 -223,-201 l -65,-55 -5,-66 c -3,-45 -11,-72 -24,-87 -36,-39 -125,-103 -136,-96 -6,4 -10,92 -10,254 v 248 l 103,99 c 108,106 215,206 285,267 l 42,37 h 149 c 158,0 161,1 161,51 0,29 20,23 73,-22 z"
id="path4" />
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg8" width="431.92pt" height="430.69pt" preserveAspectRatio="xMidYMid" version="1.0" viewBox="0 0 431.92 430.69" xmlns="http://www.w3.org/2000/svg">
<g id="g3" transform="matrix(.1 0 0 -.1 -40.075 470.79)">
<path id="path1" d="m2495 4698c-9-97-8-195-10-293-65-14-170 12-229-19 45-55 96-105 146-156-3-83-5-166-7-249-102-101-201-211-322-290-27-14-54-3-89 38-65 77-439-60-369 189 45 119-53 28-91-8-51 115-151 211-246 133-124-105-64-173-5-231 48-64 103-62 174-62 48-54 96-107 145-160 66 0 107 14 143-53 37-63 87-117 135-172-289 13-616-97-827 146-162 210-213 129-213 111 0-155 182-121 344-360 77-118 166-205 166-240-69-128-143-254-212-382h-159c-6 53 16 115-2 166-102-50-248-79-340-135-92-64-208-14-221-64-13-48-15-47 157-47 156 0 311-7 467-13v-77h561c52 44 105 87 166 116 87 39 78 44-89 45-145 2-166 2-503 14 42 19 81 47 127 55 43 0 115 188 171 225 56 21 118 23 177 28 56-38 107-85 177-91 31-3 53 0 53 5-53 72-127 130-198 184l-4 90c-5 123-28 104-28 121 210 14 425 14 637 21-58 55-127 86-127 175 0 72-30 121-30 128 0 2 28 4 61 4 68-7 111 42 159 82v139l55 51c106 98 95 132 95-292v-371c-30-28-59-56-89-84l-2-194c-2-219-6-209 76-210 22-1 50-8 62-16 18-13 23-13 30-2 5 9 33 16 71 20 35 3 66 8 69 11 6 6 10 155 16 393-31 21-62 43-93 64 3 251-4 502-13 753 60-51 142-89 150-174 7-89 10-96 65-134 44-37 95-36 149-36-12-68-15-137-20-205-37-35-78-66-111-105 0-22 2 19 629-12-23-39-28-82-29-126 0-85-1-86-161-228-66-58-118-111-116-118 9-27 130 16 213 76 45 32 47 33 164 34l118 2c46-83 124-154 157-245 0-47-496-7-555-43 81-49 166-97 246-150h632l18 75c20-2 119 19 109-12-20-51-3-96-55-148-40-41-33-57 13-30 32 20 119 49 242 90 9 34 6 78 29 107 12 8 86 8 194 8 4 12 22 62 22 65 0 2-46 4-103 7-194 6-120 46-241 79-97 26-138 76-178 81-44 11-38-118-41-152h-178c-47 93-87 190-141 279-88 140 63 263 142 365 149 210 291 154 331 366-217 0-138-117-381-269-71-42-690-15-690-12 70 58 140 211 217 211 110 0 171 181 256 166 107-20 302 172 135 294-157 116-226-120-245-120-9 0-60 37-87 42-17 3-19-3-17-57 6-274-307-57-389-190-12-19-29-35-38-35-17 0-405 306-429 337-18 45-13 96-13 143h89c53 0 93 4 96 11 17 26-154 157-166 161-21 7-9 351-19 361-2 2-126 29-135 5z"/>
<path id="path3" d="m2160 2555c-33-28-66-57-99-85h-291v-220c-128 0-96-128-264-96-55 11-143 173-159 203-19 41-119 33-154 30-6-12 122-252 133-313 15-96-132-130-195-242-71-133-240-162-302-284-36-70-25-84 51-64 48 12 64 25 154 118 149 155 126 148 509 148 174 0 317-4 317-8-60-67-112-142-162-217-114 4-177-83-246-158-101 14-134 18-207-65-166-201 173-364 249-110 15 49 125-85 110 18-4 24-16 149 202 141 134-3 172 4 180 35 13 52 26 48 147-43 117-90 232-183 347-276v-114c-58-3-122-7-177-17-3-3 26-33 63-68 136-125 121-89 123-288 1-96 4-176 6-179 114 63 135 89 135 210v109c78 11 169-18 244 9-43 65-109 118-166 172-24 285 101 312 274 482 81 84 115 36 169-32l291-6c34-35 60-64 56-116-12-117 80 11 122-61 123-230 398-46 238 129-66 74-63 73-127 73-88 2-151 93-211 150h-79c-93 0-94 3-111 83-25 121-96 113-50 137 14 8 116 10 328 8 355-3 350-2 442-101 150-161 142-154 194-161 138-22 24 143-103 180-112 52-233 185-363 398 58 114 136 224 172 347 0 10-40 6-112-12-75-20-186-386-387-204-48 42-86 37-94 107-8 55-7 111-7 166-106 3-213 5-319 7l-78 76c-6 74-9 151-18 227-18 20-336-347-376-369-60-30-362 398-379 371-15-25-20-173-20-225zm394-315c12 0 229 220 244 220 16 0 72-75 72-96 0-18-290-304-308-304-11 0-326 287-330 301-4 10 41 75 65 95 23 20 239-216 257-216zm-751-149c84-11 197-15 284-16 140-120 271-251 403-380-10-256-8-257-19-505 0-5-57 42-136 106-8 53-16 107-24 160-89 89-191 52-182 84 3 11 9 61 20 175 54 50 55 51 96 104-12 21-352-1-676 26 51 48 91 72 91 148-4 72 60 129 120 187 7 7 17-43 23-89zm1647-23c1-67-10-140 20-186 6-15 39-17 36-31-5-9-121-15-294-23-146-138-294-274-447-404-8-49 3-111-29-153-36-39-125-103-136-96-8 6-10 64-10 502 141 137 282 273 430 403 299-9 306-19 310 51 16 48 96-42 120-63z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg1" width="329.35" height="307.64" version="1.1" viewBox="0 0 329.35 307.64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g id="layer1" transform="translate(-779.42 -7.5519)"><path id="path9" transform="translate(429.83 -2.2682)" d="m647.8 18.2-1.9 3.5-45 0.1-36.5 42.6c-20.2-12.3-43-0.7-48.7 9.8-1 1.8-1.4 1.5-5.9-3.1-14.2-15-34.2-11.1-39.7-8.4-4.7 2.3-4.9 2.3-6.6 0.5-12.3-13.8-24.3-27.7-36.4-41.5l-44.3-0.1-2-3.2c-8.1-12.9-28.1-9.2-30.9 5.7-3.4 18.1 22.4 28.1 31.1 11.8l1.8-3.2h39.3l34 39c-2.7 5.4-6 10.6-7.3 16.6-0.5 3.2-1.4 4.3-4.8 6.3-5 3-9.2 7.1-13.5 11.1-10.6 0.1-23.9 1.6-25.6-12-0.4-14.2-13.9-20.4-24.7-14.7-11.9 6.3-11.7 24.3 0.4 30.1 4.2 2 10.8 2.1 14.5 0.2 7.5-3.9 4.9 7.4 21.5 7.4 6.2 0 6.9 0.2 6.2 1.8-1 13.1-3.6 10.8-10.2 38.8l-24.5 0.6-4.2-4.4c-11.8-12.6-29.6-5.5-29.6 10.5 0 16.3 20.2 24.6 31.3 8.1l2.9-4.3 24.2 0.6c5.7 25.5 10.2 17.2 7.7 27.1-1.9 18.1-6.8 13.4-21.7 13.4l-12 8.9c-27.2-8.2-33.1 21.6-18.3 29.1 11.7 6 24.9-1.8 24.9-14.6 0-5.1 0.1-5.4 4.7-8.9 6-5.1 14.3-3.3 21.6-3 5.3 9.6 12.3 18.6 24 20.1 9.1 1.2 4.4 5.9 14.5 18l-31.9 36.4h-30.6l-2.4-3.7c-6.3-9.5-21.8-10.7-28.7 1.3-8 14 7.1 29.9 21.3 23.5 3.8-1.7 9.5-7.2 9.5-9.2 12-1.6 24.4-0.9 36.5-0.9l17.4-20.1c14.3-16.5 17.8-20 19.5-19.6 20.4 5.4 32.4 0 45.7-14.6 2.9 3.8 17 20.1 39 14.8 6.9-1.7 7-1.7 8.7 0.3 11.2 13 22.5 26 33.7 39l36.2 0.1 2.4 3.8c9 14 32.2 8.1 30.7-10.4-1.5-18.4-23.8-19.2-30.8-7.9l-2.2 3.5h-30.7l-32-37.1c4.1-4.8 7.2-10 9.2-16 1.3-4.1 17 1.6 29.6-21.9 10.6-0.2 25.6-1.4 27.1 12.4 0 12.9 13.2 20.6 24.9 14.6 14.8-7.5 11.1-39.7-18.3-29.1-10.3-8.2-9.6-8.9-21.7-8.9-14.3 1.2-9.6-5.6-12.1-15.1-2.5-8.3 1.7-1.7 7.4-25.9h25.6l1.2 3c5.8 13.9 32 13.5 32-7.1 0-19.1-23.6-21.3-30.4-9.8l-1.7 2.9c-4.7-0.4-26.5 1.8-26.9-2.3 0-10.3-7.7-17.7-9-37.7 6.7 0 12.9 0.5 18.2-4.7 5.3-4 6.3-4.5 8-3.4 12.7 7.9 33.4-9.6 21.3-25.4-9.5-12.5-31.4-5.7-30.4 11.2-0.4 12.3-14.3 11.3-23.3 11.3-4.8-4.3-9.6-8.9-15.2-12.1-8.9-4.7-2.6-7.7-12.6-21.7 11.2-13 22.3-26.1 33.5-39.1h40l2 3.2c10 15.8 35.5 5.4 30.5-13.1-4.8-18-24-15.5-30.5-4.8zm-84.2 59.5c6.4 7.1 8.3 15.9 5.5 25.2-1.5 5.1-8.2 12.6-13.3 14.9-14.8 6.7-2.7-13.1-13.5-13.1-4.8 0-5.6 2-4.1 10.1 1.1 6.4 0.4 12.6-0.5 19 2.8 0 8.7 1.8 10.5-0.2 0.3-0.5 1.8-4 4.1-4.4 21.3-3.4 27.1-26.6 28-26.6 2.1 0 24.8 17.7 13.8 37.2-6.5-1.8-21.6-21.7-24.8-7.4 0 3.7 22.3 19.3 21.5 26.5-1 9.3 5.3 9.7 7.9 7.1 1.3-1.3 1.6-3.3 1.4-10 1.4-31.4 10.3 28.3-1.6 22.5-6.4-5.6-20-8.6-24.8-5.4-3.9 2.5-4.2 10.6 3.8 9.1 7.5-1.4 15.7 3.3 19.2 11 6.5 14.3 2.4 30.5-9.1 35.7-17.5 7.9-2.6-46.6-54.6-12.9-5.9 5.2-7.2 8.1-4.8 10.6 3.8 2.7 27.5-9.9 32.5-11.9 12.3 5.4 14.8 35-7.5 42.6-12.4 4.2-25.4-1.4-31.4-13.4-2.3-4.5-2.3-5-2.6-49.3v-84c0.4-23.8 1-26.2 7.6-33.1 9.3-9.7 26-11.8 36.8 0.2zm-55.3 163.3c-0.1 5.9-9.8 17.8-24.5 17.8-16.2 0-41.9-21.7-10.9-53.5 6.7 7.1 11.7 16.1 19.9 21.6 7.5 4.7 10.5-1.9 8.1-5.6-1-1.5-10.4-12.7-16.6-19.1 0.1-6.2 0-13.1 0-19.3-0.7 0-8.7-0.2-9 0-0.3 0.3-0.5 6.1-0.5 10-0.9 9.4-21.7 8.8-25.2 34.6-1.4 9.9-16.6-1-19.1-9.4-1.5-5.2-1.5-16.4 0-21.6 3.3-10.9 17.3-15.3 21.2-16.4-2.1-2.5-4.8-4.9-5.9-8.1 0-3.8-17.3 9.8-18.6 7.8-9.4-14.6 2.1-52.8 1.1-22.2-0.3 8.2 0.7 10.2 5 10.2 3.6 0 4.9-2.4 4.2-7.6-0.6-4.3 8.7-25.5 21.1-25.5-4-19.2-24 14.8-27-0.7-3.6-18.7 15.7-30.2 16.5-30.2 0.5 0 5.3 15.4 17.5 22.5 7.8 4.6 16.2 7.4 25.2 7.4-3.8-25.1 4.9-28.9-4.2-28.9-9 0-1.8 17.9-11.8 14.5-14.2-4.9-20.9-22.1-13.8-35.6 11.6-22.2 42.9-14.1 44.8 0.6z" fill="#a11b1b"/><path id="path2" transform="translate(429.83 -2.2682)" d="m461.3 217.7c-20-1-19-0.4-19-10.1 0-4.4 0.4-7.8 0.8-7.5s11.1 0.8 23.8 1.1c27.9 0.8 26.9 1.1 26.9-9.7v-7.3h-17.5c-25.1 0-30.4-2.1-33.7-13.4-2.4-8.1-1.5-24.5 1.5-29.4 4.3-6.9 7.7-8.1 25.1-8.9 9.7-0.4 15.7-0.6 39 0.5v15.7h-18.9c-18.3 0-23.6 0.7-25.3 3.3-1.3 2-0.9 10.2 0.6 12.2 3.5 4.8 31.3-0.3 41.5 5.4 6.9 3.8 7.9 8.6 8.3 21.4 0.4 12.6-0.2 17.1-4.4 21.4-6.1 6.2-12.7 6.9-48.9 5.1zm78.9-0.7c0.5-22.6-0.3-45.2-0.4-67.8l-25.5 0.2v-17.2h72.5l0.6 16.4-25.6 0.6c-0.5 22.2 0.5 45.5-0.3 67.8-0.3 3.3-21.3 2.3-21.3 0z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

+2 -60
View File
@@ -1,60 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="8.2832384mm"
height="9.4351206mm"
viewBox="0 0 8.2832384 9.4351206"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.3 (0e150ed, 2023-07-21)"
sodipodi:docname="scale.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.0289025"
inkscape:cx="8.7471843"
inkscape:cy="8.2612296"
inkscape:window-width="1280"
inkscape:window-height="688"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-88.746886,-143.1532)"><g
id="g1"
transform="matrix(0.26458333,0,0,0.26458333,88.077577,143.1532)"><path
d="m 14.3423,8.42148 0.0218,-0.00723 2.1725,-0.51502 c 0.4642,-0.10879 0.8631,-0.40619 1.1025,-0.81965 l 0.5041,-0.87044 0.0146,-0.02902 0.0145,0.02902 0.5041,0.87044 c 0.2394,0.41346 0.6383,0.71086 1.1025,0.81965 l 2.1725,0.51502 0.0254,0.01087 H 21.9514 L 19.7789,8.94737 C 19.3147,9.0562 18.9158,9.35359 18.6764,9.76705 l -0.5041,0.87045 -0.0145,0.029 -0.0146,-0.029 -0.5041,-0.87045 C 17.3997,9.35359 17.0008,9.0562 16.5366,8.94737 L 14.3641,8.43239 14.3423,8.42512 Z"
id="path10" /><path
d="M 14.3423,2.245 14.3641,2.23775 16.5366,1.72274 C 17.0008,1.61393 17.3997,1.31653 17.6391,0.903078 L 18.1432,0.0290146 18.1578,0 18.1723,0.0290146 18.6764,0.899453 C 18.9158,1.31291 19.3147,1.61031 19.7789,1.71911 L 21.9514,2.23412 21.9768,2.245 H 21.9514 L 19.7789,2.76726 C 19.3147,2.87607 18.9158,3.17347 18.6764,3.58692 L 18.1723,4.45738 18.1578,4.48639 18.1432,4.45738 17.6391,3.58692 C 17.3997,3.17347 17.0008,2.87607 16.5366,2.76726 L 14.3641,2.25225 Z"
id="path11" /><path
d="M 18.183,15.7998 C 17.9328,13.7832 16.754,11.9807 14.9697,10.9507 11.749,9.03938 8.96364,8.82176 6.23265,6.73999 L 2.52967,8.79638 18.183,17.8344 33.8364,8.79638 30.1334,6.74359 c -2.731,2.08181 -5.5164,2.29943 -8.737,4.21071 -1.7844,1.0301 -2.9631,2.8326 -3.2134,4.8491 z"
id="path12" /><path
d="M 18.183,35.6603 C 17.824,35.653 17.4649,35.5551 17.1458,35.3701 L 2.52967,26.9269 V 8.79646 L 17.1458,17.236 c 0.3227,0.185 0.6782,0.2829 1.0372,0.2902 0.3591,-0.0073 0.7181,-0.1052 1.0373,-0.2902 L 33.8364,8.79646 V 26.9306 l -14.6161,8.4395 c -0.3228,0.185 -0.6782,0.2829 -1.0373,0.2902 z"
id="path13" /><path
d="m 18.183,35.6603 c 0.3591,-0.0073 0.7181,-0.1052 1.0373,-0.2902 l 14.616,-8.4395 V 8.79646 L 19.2203,17.236 c -0.3228,0.185 -0.6782,0.2829 -1.0373,0.2902"
id="path14" /><path
d="M 18.183,17.5262 C 17.824,17.5189 17.4649,17.421 17.1458,17.236 L 2.52967,8.79646 V 26.9306 l 14.61613,8.4395 c 0.3227,0.185 0.6782,0.2829 1.0372,0.2902"
id="path15" /><path
d="M 18.1831,17.5262 C 17.824,17.5189 17.465,17.421 17.1458,17.236 l -1.4507,-0.8378 v 18.1341 l 1.4507,0.8378 c 0.3228,0.185 0.6782,0.2829 1.0373,0.2902 0.359,-0.0073 0.7181,-0.1052 1.0372,-0.2902 l 1.4508,-0.8378 V 16.3946 l -1.4508,0.8378 c -0.3227,0.1849 -0.6782,0.2829 -1.0372,0.2901 z"
id="path16" /><path
d="M 9,5.32055 9.02174,5.31328 11.1942,4.79826 c 0.4643,-0.10879 0.8632,-0.40618 1.1026,-0.81964 l 0.5041,-0.87044 0.0145,-0.02902 0.0145,0.02902 0.5041,0.87044 c 0.2394,0.41346 0.6384,0.71085 1.1026,0.81964 l 2.1725,0.51502 0.0253,0.01087 H 16.6091 L 14.4366,5.84644 C 13.9724,5.95523 13.5734,6.25263 13.334,6.66609 L 12.8299,7.53653 12.8154,7.56555 12.8009,7.53653 12.2968,6.66609 C 12.0574,6.25263 11.6585,5.95523 11.1942,5.84644 L 9.02174,5.33142 9,5.32415 Z"
id="path17" /><path
d="m 19.6593,5.32055 0.0218,-0.00727 2.1724,-0.51502 c 0.4642,-0.10879 0.8632,-0.40618 1.1026,-0.81964 l 0.5041,-0.87044 0.0145,-0.02902 0.0145,0.02902 0.5042,0.87044 c 0.2393,0.41346 0.6383,0.71085 1.1025,0.81964 l 2.1724,0.51502 0.0255,0.01087 H 27.2683 L 25.0959,5.84644 C 24.6317,5.95523 24.2327,6.25263 23.9934,6.66609 L 23.4892,7.53653 23.4747,7.56555 23.4602,7.53653 22.9561,6.66609 C 22.7167,6.25263 22.3177,5.95523 21.8535,5.84644 L 19.6811,5.33142 19.6593,5.32415 Z"
id="path18" /></g></g></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg1" width="8.28mm" height="9.4349mm" version="1.1" viewBox="0 0 8.28 9.4349" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path id="path9" d="m4.14 0c-0.05 0.08-0.1 0.16-0.14 0.24-0.07 0.11-0.17 0.19-0.29 0.22-0.2 0.04-0.39 0.09-0.58 0.13 0.19 0.05 0.38 0.1 0.58 0.14 0.12 0.03 0.22 0.11 0.29 0.22 0.04 0.08 0.09 0.16 0.14 0.24 0.04-0.08 0.09-0.16 0.13-0.24 0.07-0.11 0.17-0.19 0.29-0.22 0.2-0.04 0.39-0.09 0.59-0.14-0.2-0.04-0.39-0.09-0.59-0.14-0.12-0.02-0.22-0.1-0.29-0.21-0.04-0.08-0.09-0.16-0.13-0.24zm-1.42 0.81c-0.04 0.08-0.09 0.16-0.14 0.24-0.06 0.11-0.16 0.19-0.29 0.22-0.19 0.05-0.38 0.09-0.58 0.14 0.19 0.05 0.39 0.09 0.58 0.14 0.12 0.03 0.23 0.11 0.29 0.22 0.05 0.08 0.09 0.16 0.14 0.24 0.05-0.09 0.09-0.17 0.14-0.25 0.06-0.11 0.17-0.18 0.29-0.21 0.19-0.05 0.39-0.1 0.58-0.14-0.19-0.05-0.39-0.09-0.58-0.14-0.12-0.03-0.23-0.11-0.29-0.22-0.05-0.08-0.09-0.16-0.14-0.24zm2.82 0c-0.04 0.08-0.09 0.16-0.14 0.24-0.06 0.11-0.16 0.19-0.29 0.22-0.19 0.05-0.38 0.09-0.58 0.14 0.19 0.05 0.39 0.09 0.58 0.14 0.12 0.03 0.23 0.11 0.29 0.22 0.05 0.08 0.09 0.16 0.14 0.24 0.05-0.09 0.09-0.17 0.14-0.25 0.06-0.11 0.17-0.18 0.29-0.21 0.19-0.05 0.39-0.1 0.58-0.14-0.19-0.05-0.39-0.09-0.58-0.14-0.12-0.03-0.23-0.11-0.29-0.22-0.05-0.08-0.09-0.16-0.14-0.24zm-1.4 0.83c-0.05 0.07-0.1 0.15-0.14 0.23-0.07 0.11-0.17 0.19-0.29 0.22-0.2 0.05-0.39 0.09-0.58 0.14 0.19 0.05 0.39 0.09 0.58 0.14 0.12 0.03 0.23 0.11 0.29 0.22 0.05 0.08 0.09 0.16 0.14 0.24 0.04-0.09 0.09-0.17 0.13-0.25 0.07-0.11 0.17-0.18 0.29-0.21 0.2-0.05 0.39-0.1 0.59-0.14-0.2-0.05-0.39-0.09-0.59-0.14-0.12-0.03-0.22-0.11-0.29-0.22-0.04-0.08-0.09-0.16-0.13-0.23zm-3.16 0.14-0.98 0.55v4.8c1.29 0.74 2.58 1.49 3.87 2.23 0.25 0.14 0.46 0.05 0.55 0 1.29-0.75 2.57-1.49 3.86-2.23v-4.8l-0.98-0.55c-1.11 0.86-2.96 0.79-3.16 2.4-0.2-1.61-2.04-1.55-3.16-2.4z"/></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+6 -87
View File
@@ -1,88 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1280.000000pt" height="1217.000000pt" viewBox="0 0 1280.000000 1217.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,1217.000000) scale(0.100000,-0.100000)" stroke="none">
<path d="M7704 12160 c-84 -17 -256 -71 -307 -96 -186 -94 -274 -202 -399
-492 -32 -74 -72 -152 -90 -175 -80 -100 -141 -163 -211 -215 -41 -32 -145
-120 -232 -197 -228 -203 -324 -268 -491 -331 -160 -60 -436 -92 -899 -104
-428 -11 -478 -15 -585 -50 -499 -163 -1061 -520 -1543 -979 l-78 -75 -100 49
c-54 27 -99 52 -99 55 0 3 35 46 77 95 131 154 287 370 387 537 77 130 96 168
96 200 0 36 -2 38 -25 32 -15 -4 -39 0 -60 10 -29 14 -42 15 -77 5 -147 -39
-405 -230 -705 -523 -79 -76 -143 -136 -143 -132 0 3 37 49 81 102 45 54 132
165 195 248 64 86 132 166 159 186 95 72 107 84 129 129 13 25 33 91 45 146
12 55 29 120 38 144 22 59 11 76 -53 78 -27 0 -63 4 -80 8 -23 5 -44 0 -85
-20 -78 -39 -264 -232 -441 -455 -80 -102 -192 -243 -249 -315 -135 -171 -213
-258 -273 -306 -57 -46 -91 -50 -108 -14 -16 35 15 222 42 250 10 11 35 65 54
120 19 55 69 182 110 283 42 100 75 186 74 190 -1 4 4 25 11 46 14 39 12 113
-5 328 l-7 77 -72 6 c-80 8 -99 2 -153 -49 -101 -93 -192 -314 -302 -728 -110
-413 -138 -498 -195 -589 -13 -20 -79 -99 -145 -175 -206 -235 -338 -412 -628
-844 -188 -278 -318 -656 -353 -1020 -16 -171 -1 -459 33 -638 9 -50 15 -62
29 -60 11 2 31 -19 60 -62 60 -88 105 -118 205 -134 140 -22 189 -10 460 118
49 24 131 68 182 99 51 31 92 51 92 44 0 -7 -34 -185 -75 -396 l-74 -383 41
-67 c23 -36 53 -82 67 -101 l25 -35 -33 30 c-51 46 -34 16 56 -98 105 -132
471 -492 628 -618 130 -105 509 -381 670 -489 384 -257 905 -493 1135 -515 49
-4 137 -27 255 -65 192 -63 338 -106 475 -140 47 -11 92 -23 99 -26 23 -8 -73
-49 -282 -120 -492 -168 -840 -315 -1062 -447 -330 -197 -432 -274 -532 -403
-27 -35 -57 -53 -210 -124 -124 -57 -199 -86 -248 -95 -118 -21 -186 -60 -221
-127 -14 -28 -17 -124 -5 -168 4 -15 1 -43 -10 -70 -19 -51 -16 -70 11 -70 14
0 21 -10 25 -37 4 -21 12 -49 18 -63 21 -43 179 -193 257 -243 41 -27 134 -87
205 -133 209 -137 484 -268 770 -367 55 -19 163 -62 240 -94 77 -33 201 -81
275 -107 74 -27 191 -68 260 -93 179 -65 382 -113 721 -172 271 -47 301 -50
350 -40 36 8 116 10 239 6 276 -8 386 -41 584 -172 53 -35 153 -111 222 -169
145 -121 277 -222 384 -293 143 -96 584 -356 659 -390 42 -19 87 -40 101 -48
14 -7 41 -16 62 -20 45 -9 93 23 107 69 5 17 12 39 16 49 5 12 2 17 -8 17 -8
0 -29 21 -45 47 -30 46 -189 207 -322 326 l-65 59 63 -47 c34 -26 110 -92 168
-147 93 -89 129 -111 129 -82 0 5 4 16 9 23 7 11 -13 36 -81 100 -108 103
-189 162 -433 316 -185 117 -268 185 -336 277 -19 25 -39 60 -45 78 -10 27 -9
34 10 54 27 28 108 40 386 57 219 13 1193 2 1213 -13 27 -22 132 15 132 46 0
8 -16 33 -35 56 -19 23 -35 50 -35 60 0 18 -46 35 -55 19 -3 -4 -31 1 -63 12
-149 52 -318 76 -737 105 -235 16 -261 16 -435 1 -102 -10 -214 -17 -250 -17
-114 0 -360 70 -360 103 0 16 32 46 80 75 45 26 325 127 407 146 34 8 75 23
90 34 60 42 161 85 358 152 235 80 316 118 406 190 81 65 86 74 65 117 -9 18
-16 44 -16 57 l0 23 -162 -3 c-129 -1 -190 -7 -295 -28 -222 -44 -459 -119
-758 -238 -238 -95 -287 -112 -425 -144 -169 -38 -325 -50 -487 -36 -76 7
-160 18 -188 26 l-50 13 28 12 c16 6 30 11 32 11 12 0 345 143 470 202 332
157 506 266 675 420 l73 67 -12 55 c-7 31 -20 65 -29 76 -101 125 -621 -82
-1438 -572 -133 -80 -241 -138 -256 -138 -49 0 -615 99 -802 140 -252 56 -364
92 -411 135 -20 19 -45 37 -56 40 -21 7 -24 32 -6 56 22 29 245 92 467 133 47
8 213 36 370 61 476 76 694 134 917 244 131 65 236 136 357 244 45 40 140 120
211 178 161 132 280 255 324 339 33 62 34 65 28 156 -5 80 -10 102 -38 155
-31 58 -117 153 -186 206 -18 14 -116 65 -219 114 -243 116 -360 193 -504 333
-188 183 -293 261 -565 422 -82 48 -171 105 -198 125 l-47 38 42 5 c24 3 111
12 193 21 493 52 979 180 1545 407 263 106 500 246 735 435 126 101 364 329
502 482 101 111 328 407 426 556 40 61 57 80 53 60 -125 -518 -151 -1060 -65
-1343 25 -80 96 -214 139 -262 18 -19 27 -27 20 -17 -7 9 -10 17 -8 17 3 0 43
-25 89 -55 171 -113 412 -161 1029 -206 451 -32 470 -33 734 -14 271 19 415
17 541 -10 110 -23 118 -29 72 -51 -41 -20 -43 -23 -24 -47 10 -14 18 -14 53
-5 34 9 53 8 105 -6 90 -25 320 -118 479 -193 230 -110 431 -199 535 -238 124
-46 312 -95 385 -99 30 -2 83 -9 117 -17 61 -13 62 -13 65 -48 2 -30 7 -37 28
-39 51 -8 174 54 178 89 1 8 5 31 8 51 5 28 1 46 -16 72 -82 134 -460 330
-1014 527 -140 50 -173 65 -155 70 96 28 461 151 584 196 326 121 639 263 813
371 42 26 99 58 125 71 114 55 208 178 193 251 -9 45 -55 100 -101 121 -162
74 -676 -86 -1330 -413 -73 -37 -114 -53 -105 -42 8 11 56 66 105 124 50 58
110 130 135 160 65 82 184 203 275 280 76 64 84 74 145 194 54 107 65 136 68
190 6 87 -5 103 -73 111 -67 9 -106 -5 -219 -76 -219 -139 -550 -418 -1035
-873 l-223 -210 -114 38 c-63 20 -114 39 -114 40 0 2 14 34 31 72 18 38 61
139 96 224 36 85 80 191 98 235 19 44 41 121 50 170 9 50 25 132 36 183 10 51
19 108 19 128 0 45 -23 100 -48 113 -32 17 -92 13 -127 -9 -111 -69 -302 -428
-489 -918 -22 -56 -32 -72 -45 -68 -53 17 -275 68 -386 90 -242 47 -310 55
-500 60 -119 3 -209 10 -240 19 -107 30 -209 104 -164 119 16 5 -5 45 -43 80
-32 30 -33 46 -14 140 25 118 65 191 296 548 48 74 101 169 118 210 16 41 50
125 76 185 84 204 97 320 50 450 -40 112 -41 105 27 155 85 63 378 365 475
490 101 130 244 343 354 526 46 76 124 201 172 278 49 76 100 166 114 200 28
66 55 121 281 566 218 431 289 627 323 890 6 47 18 125 27 173 14 75 14 101 4
165 -19 128 -101 273 -210 375 -28 26 -51 50 -51 52 0 3 14 5 30 5 17 0 30 3
30 8 -1 22 -136 101 -289 167 -216 95 -278 115 -466 153 -304 63 -465 66 -712
13 -355 -76 -560 -80 -928 -16 -304 53 -410 63 -501 45z m2941 -1516 c-15 -49
-44 -132 -67 -184 -59 -140 -200 -441 -204 -437 -2 2 28 75 66 162 69 155 185
443 210 518 24 75 20 29 -5 -59z m-3225 -9228 c92 -8 187 -23 215 -32 l50 -18
-35 -7 c-87 -19 -329 -31 -406 -21 -443 60 -615 88 -588 94 48 12 586 0 764
-16z"/>
</g>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg1" width="1281.8pt" height="1215.8pt" preserveAspectRatio="xMidYMid" version="1.0" viewBox="0 0 1281.8 1215.8" xmlns="http://www.w3.org/2000/svg">
<metadata id="metadata1">Created by potrace 1.15, written by Peter Selinger 2001-2017</metadata>
<g id="g1" transform="matrix(.1 0 0 -.1 1.9156 1216.5)">
<path id="path1" d="m6998 11572c-96-258-333-414-533-587-370-329-494-412-1390-435-874 36-1595-534-2206-1104-62 39-142 56-199 104 0 9 262 294 464 632 74 121 192 285-66 247-417-111-1220-1151-572-305 100 137 564 539 318 683-471 46-1109-1370-1236-1102 23 505 514 977 207 1300-445 44-287-954-795-1541-1448-1651-1018-2700-654-2758 335-53 734 316 734 261 0-180-204-689-108-846 752-1155 2134-1636 3377-2057 99-35-1443-411-1876-970-121-157-587-192-679-346-256-719 2022-1610 3117-1657 812 100 1428-660 2088-1018 130-59 213-134 286 50-220 493-1110 853-956 1021 132 137 1731-15 1731 90 0 8-16 33-35 56-357 414-1665-11-1935 283 0 119 972 340 1341 597 121 97 56 186-113 194-574-4-1043-309-1478-410-184-41-447-69-725 3 83 35 240 88 530 225 448 212 563 317 748 487-10 44-16 92-41 131-101 125-621-82-1438-572-133-80-241-138-256-138-49 0-615 99-802 140-338 75-525 153-473 231 22 29 245 92 467 133 435 77 883 115 1287 305 216 102 386 272 568 422 257 211 446 400 314 650-170 319-510 265-909 653-388 377-556 379-810 585 78 10 157 17 235 26 2709 261 3344 2357 3261 1940-125-518-151-1060-65-1343 200-639 1746-555 2003-537 674 40 1167-320 1761-550 726-269 856-137 765 9-161 263-1223 582-1169 597 508 133 2142 769 1614 1010-297 136-1465-492-1435-455 91 57 965 1014 655 1059-597-178-1028-736-1477-1159-75 29-155 45-228 78 0 20 239 507 275 701 27 105 141 556-120 415-211-131-498-997-534-986-290 78-585 141-886 150-236-1-496 47-461 358 71 337 157 167 490 943 217 528-107 470 77 605 459 340 1060 1370 1115 1494 351 733 1106 1835 145 2401-1027 452-1067-31-2106 150-558 97-937 83-1207-543z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="64" height="64">
<path d="M8.16 23h21.177v-5.86l-4.023-2.307-.694-.3-16.46.113z" fill="none" />
<path
d="M22.012 22.222c.197-.675.122-1.294-.206-1.754-.3-.422-.807-.666-1.416-.694l-11.545-.15c-.075 0-.14-.038-.178-.094s-.047-.13-.028-.206c.038-.113.15-.197.272-.206l11.648-.15c1.38-.066 2.88-1.182 3.404-2.55l.666-1.735a.38.38 0 0 0 .02-.225c-.75-3.395-3.78-5.927-7.4-5.927-3.34 0-6.17 2.157-7.184 5.15-.657-.488-1.5-.75-2.392-.666-1.604.16-2.9 1.444-3.048 3.048a3.58 3.58 0 0 0 .084 1.191A4.84 4.84 0 0 0 0 22.1c0 .234.02.47.047.703.02.113.113.197.225.197H21.58a.29.29 0 0 0 .272-.206l.16-.572z"
/>
<path
d="M25.688 14.803l-.32.01c-.075 0-.14.056-.17.13l-.45 1.566c-.197.675-.122 1.294.206 1.754.3.422.807.666 1.416.694l2.457.15c.075 0 .14.038.178.094s.047.14.028.206c-.038.113-.15.197-.272.206l-2.56.15c-1.388.066-2.88 1.182-3.404 2.55l-.188.478c-.038.094.028.188.13.188h8.797a.23.23 0 0 0 .225-.169A6.41 6.41 0 0 0 32 21.106a6.32 6.32 0 0 0-6.312-6.302"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+147 -106
View File
@@ -697,7 +697,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,minimax,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai,workers_ai">
<div class="range-block-title" data-i18n="Temperature">
Temperature
</div>
@@ -710,7 +710,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes">
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes,workers_ai">
<div class="range-block-title" data-i18n="Frequency Penalty">
Frequency Penalty
</div>
@@ -723,7 +723,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes">
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes,workers_ai">
<div class="range-block-title" data-i18n="Presence Penalty">
Presence Penalty
</div>
@@ -736,7 +736,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt">
<div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt,workers_ai">
<div class="range-block-title" data-i18n="Top K">
Top K
</div>
@@ -749,7 +749,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,minimax,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai,workers_ai">
<div class="range-block-title" data-i18n="Top P">
Top P
</div>
@@ -762,7 +762,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openrouter,nanogpt,chutes">
<div class="range-block" data-source="openrouter,nanogpt,chutes,workers_ai">
<div class="range-block-title" data-i18n="Repetition Penalty">
Repetition Penalty
</div>
@@ -986,7 +986,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai,workers_ai">
<div class="range-block-title justifyLeft" data-i18n="Seed">
Seed
</div>
@@ -1269,8 +1269,8 @@
<span data-i18n="Top K">Top K</span>
<div class="fa-solid fa-circle-info opacity50p" title="Top K sets a maximum amount of top tokens that can be chosen from.&#13;E.g Top K is 20, this means only the 20 highest ranking tokens will be kept (regardless of their probabilities being diverse or limited).&#13;Set to 0 (or -1, depending on your backend) to disable." data-i18n="[title]Top_K_desc"></div>
</small>
<input class="neo-range-slider" type="range" id="top_k_textgenerationwebui" name="volume" min="-1" max="200" step="1">
<input class="neo-range-input" type="number" min="-1" max="200" step="1" data-for="top_k_textgenerationwebui" id="top_k_counter_textgenerationwebui">
<input class="neo-range-slider" type="range" id="top_k_textgenerationwebui" name="volume" min="-1" max="500" step="1">
<input class="neo-range-input" type="number" min="-1" max="500" step="1" data-for="top_k_textgenerationwebui" id="top_k_counter_textgenerationwebui">
</div>
<div data-tg-samplers="top_p" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small>
@@ -1400,7 +1400,7 @@
<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">
</div>
<div data-tg-type="koboldcpp, llamacpp, tabby" id="adaptive_p_block" class="wide100p">
<div data-tg-type="koboldcpp, llamacpp, ooba, tabby" id="adaptive_p_block" class="wide100p">
<h4 class="wide100p textAlignCenter">
<label data-i18n="Adaptive-P">Adaptive-P</label>
<a href="https://github.com/MrJackSpade/adaptive-p-docs" target="_blank">
@@ -1831,6 +1831,7 @@
<div data-name="tfs" draggable="true"><span>Tail Free Sampling</span><small></small></div>
<div data-name="top_a" draggable="true"><span>Top A</span><small></small></div>
<div data-name="min_p" draggable="true"><span>Min P</span><small></small></div>
<div data-name="adaptive_p" draggable="true"><span>Adaptive-P</span><small></small></div>
<div data-name="mirostat" draggable="true"><span>Mirostat</span><small></small></div>
<div data-name="xtc" draggable="true"><span>XTC</span><small></small></div>
<div data-name="encoder_repetition_penalty" draggable="true"><span>Encoder Repetition Penalty</span><small></small></div>
@@ -1996,7 +1997,7 @@
</b>
</div>
</div>
<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">
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,minimax,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai,nanogpt,workers_ai">
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_function_calling" type="checkbox" />
<span data-i18n="Enable function calling">Enable function calling</span>
@@ -2010,8 +2011,22 @@
<span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>
<strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
</div>
<div id="tool_call_recurse_limit_block" class="range-block wide100p">
<div class="range-block-title">
<small data-i18n="Tool Call Recurse Limit">Tool Call Recurse Limit</small>
<div class="fa-solid fa-circle-info opacity50p margin5" data-i18n="[title]Maximum number of tool call recursion steps per generation." title="Maximum number of tool call recursion steps per generation."></div>
</div>
<div class="range-block" data-source="openrouter">
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="tool_call_recurse_limit" name="tool_call_recurse_limit" min="1" max="50" step="1">
</div>
<div class="range-block-counter">
<input type="number" min="1" max="50" step="1" data-for="tool_call_recurse_limit" id="tool_call_recurse_limit_counter">
</div>
</div>
</div>
</div>
<div class="range-block" data-source="openrouter,custom">
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10">
<div class="flex-container oneline-dropdown">
<label for="tool_reasoning_mode" data-i18n="Interleaved Thinking">
@@ -2035,7 +2050,7 @@
</em>
</div>
</div>
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes">
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes,workers_ai">
<label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_media_inlining" type="checkbox" />
<span data-i18n="Send inline media">Send inline media</span>
@@ -2059,7 +2074,7 @@
<strong data-source="makersuite,vertexai" data-i18n="video_inlining_hint_4">Videos must be less than 20 MB and under 1 minute long.</strong>
<strong data-source="makersuite,vertexai" data-i18n="audio_inlining_hint_2">Audio must be less than 20 MB.</strong>
</div>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai,zai,siliconflow,chutes,makersuite,vertexai">
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai,zai,siliconflow,chutes,makersuite,vertexai,workers_ai">
<div class="flex-container oneline-dropdown">
<label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
Inline Image Quality
@@ -2120,7 +2135,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,chutes,azure_openai,nanogpt,zai">
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,chutes,azure_openai,nanogpt,zai,workers_ai">
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
<input id="openai_show_thoughts" type="checkbox" />
<span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2129,12 +2144,12 @@
<span data-i18n="Allows the model to return its thinking process.">
Allows the model to return its thinking process.
</span>
<strong data-i18n="This setting affects visibility only." data-source-mode="except" data-source="zai,moonshot,openrouter">
<strong data-i18n="This setting affects visibility only." data-source-mode="except" data-source="zai,moonshot,openrouter,deepseek">
This setting affects visibility only.
</strong>
</div>
</div>
<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">
<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,deepseek">
<div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
<label for="openai_reasoning_effort">
<span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2445,7 +2460,8 @@
<small data-i18n="Click Authorize below or get the key from">
Click "Authorize" below or get the key from </small> <a target="_blank" href="https://openrouter.ai/keys/">OpenRouter</a>.
<br>
<a href="https://openrouter.ai/account" target="_blank" data-i18n="View Remaining Credits">View Remaining Credits</a>
<a href="https://openrouter.ai/settings/credits" target="_blank" class="openrouter_view_credits" data-i18n="View Remaining Credits">View Remaining Credits</a>
<span class="openrouter_credits_display marginLeft5"></span>
</div>
<div class="flex-container">
<input id="api_key_openrouter-tg" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off">
@@ -2894,6 +2910,7 @@
<option value="azure_openai">Azure OpenAI</option>
<option value="chutes">Chutes</option>
<option value="claude">Claude</option>
<option value="workers_ai">Cloudflare Workers AI</option>
<option value="cohere">Cohere</option>
<!-- Temporarily disabled. -->
<!-- <option value="cometapi">CometAPI</option> -->
@@ -2904,6 +2921,7 @@
<option value="makersuite">Google AI Studio</option>
<option value="vertexai">Google Vertex AI</option>
<option value="mistralai">MistralAI</option>
<option value="minimax">MiniMax</option>
<option value="moonshot">Moonshot AI</option>
<option value="nanogpt">NanoGPT</option>
<option value="openrouter">OpenRouter</option>
@@ -3028,8 +3046,17 @@
<div>
<h4 data-i18n="OpenAI Model">OpenAI Model</h4>
<select id="model_openai_select">
<optgroup label="GPT-5.5">
<option value="gpt-5.5">gpt-5.5</option>
<option value="gpt-5.5-2026-04-23">gpt-5.5-2026-04-23</option>
</optgroup>
<optgroup label="GPT-5.4">
<option value="gpt-5.4">gpt-5.4</option>
<option value="gpt-5.4-2026-03-05">gpt-5.4-2026-03-05</option>
<option value="gpt-5.4-mini">gpt-5.4-mini</option>
<option value="gpt-5.4-mini-2026-03-17">gpt-5.4-mini-2026-03-17</option>
<option value="gpt-5.4-nano">gpt-5.4-nano</option>
<option value="gpt-5.4-nano-2026-03-17">gpt-5.4-nano-2026-03-17</option>
</optgroup>
<optgroup label="GPT-5.3">
<option value="gpt-5.3-chat-latest">gpt-5.3-chat-latest</option>
@@ -3143,6 +3170,7 @@
<h4 data-i18n="Claude Model">Claude Model</h4>
<select id="model_claude_select">
<optgroup label="Versions">
<option value="claude-opus-4-7">claude-opus-4-7</option>
<option value="claude-opus-4-6">claude-opus-4-6</option>
<option value="claude-opus-4-5">claude-opus-4-5</option>
<option value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>
@@ -3176,7 +3204,8 @@
<small data-i18n="Click Authorize below or get the key from">
Click "Authorize" below or get the key from </small> <a target="_blank" href="https://openrouter.ai/keys/">OpenRouter</a>.
<br>
<a href="https://openrouter.ai/account" target="_blank" data-i18n="View Remaining Credits">View Remaining Credits</a>
<a href="https://openrouter.ai/settings/credits" target="_blank" class="openrouter_view_credits" data-i18n="View Remaining Credits">View Remaining Credits</a>
<span class="openrouter_credits_display marginLeft5"></span>
</div>
<div class="flex-container">
<input id="api_key_openrouter" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off">
@@ -3195,36 +3224,6 @@
<input id="openrouter_use_fallback" type="checkbox" />
<span data-i18n="Allow fallback models">Allow fallback models</span>
</label>
<div class="marginTopBot5">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="OpenRouter Model Sorting">OpenRouter Model Sorting</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content m-b-1">
<div class="marginTopBot5">
<label for="openrouter_sort_models" class="checkbox_label">
<select id="openrouter_sort_models">
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
<option data-i18n="Price" value="pricing.prompt">Price (cheapest)</option>
<option data-i18n="Context Size" value="context_length">Context Size</option>
</select>
</label>
</div>
<div class="marginTopBot5">
<label for="openrouter_group_models" class="checkbox_label">
<input id="openrouter_group_models" type="checkbox" />
<span data-i18n="Group by vendors">Group by vendors</span>
</label>
<div class="toggle-description justifyLeft wide100p">
<span data-i18n="Group by vendors Description">
Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting.
</span>
</div>
</div>
</div>
</div>
</div>
<div>
<h4>
<span data-i18n="Model Providers">Model Providers</span>
@@ -3344,6 +3343,8 @@
<option value="gemini-2.0-flash-lite">gemini-2.0-flash-lite</option>
</optgroup>
<optgroup label="Gemma">
<option value="gemma-4-31b-it">gemma-4-31b-it</option>
<option value="gemma-4-26b-a4b-it">gemma-4-26b-a4b-it</option>
<option value="gemma-3n-e4b-it">gemma-3n-e4b-it</option>
<option value="gemma-3n-e2b-it">gemma-3n-e2b-it</option>
<option value="gemma-3-27b-it">gemma-3-27b-it</option>
@@ -3566,6 +3567,32 @@
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
<div id="minimax_form" data-source="minimax">
<h4 data-i18n="MiniMax API Key">MiniMax API Key</h4>
<div class="flex-container">
<input id="api_key_minimax" name="api_key_minimax" class="text_pole flex1" value="" type="text" autocomplete="off">
<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_minimax"></div>
</div>
<div data-for="api_key_minimax" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
For privacy reasons, your API key will be hidden after you click 'Connect'.
</div>
<h4 data-i18n="MiniMax Endpoint">MiniMax Endpoint</h4>
<select id="minimax_endpoint">
<option value="global" data-i18n="Global (minimax.io)">Global (minimax.io)</option>
<option value="cn" data-i18n="China (minimaxi.com)">China (minimaxi.com)</option>
</select>
<h4 data-i18n="MiniMax Model">MiniMax Model</h4>
<select id="model_minimax_select">
<option value="MiniMax-M2.7">MiniMax-M2.7</option>
<option value="MiniMax-M2.7-highspeed">MiniMax-M2.7-highspeed</option>
<option value="MiniMax-M2.5">MiniMax-M2.5</option>
<option value="MiniMax-M2.5-highspeed">MiniMax-M2.5-highspeed</option>
<option value="MiniMax-M2.1">MiniMax-M2.1</option>
<option value="MiniMax-M2.1-highspeed">MiniMax-M2.1-highspeed</option>
<option value="MiniMax-M2">MiniMax-M2</option>
<option value="M2-her">M2-her</option>
</select>
</div>
<div id="electronhub_form" data-source="electronhub">
<h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4>
<div>
@@ -3584,37 +3611,6 @@
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
<div class="marginTopBot5">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Electron Hub Model Sorting">Electron Hub Model Sorting</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content m-b-1">
<div class="marginTopBot5">
<label for="electronhub_sort_models" class="checkbox_label">
<select id="electronhub_sort_models">
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
<option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option>
<option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option>
<option data-i18n="Context Size" value="context_length">Context Size</option>
</select>
</label>
</div>
<div class="marginTopBot5">
<label for="electronhub_group_models" class="checkbox_label">
<input id="electronhub_group_models" type="checkbox" />
<span data-i18n="Group by vendors">Group by vendors</span>
</label>
<div class="toggle-description justifyLeft wide100p">
<span data-i18n="Group by vendors Description">
Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting.
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="chutes_form" data-source="chutes">
<h4 data-i18n="Chutes API Key">Chutes API Key</h4>
@@ -3634,29 +3630,11 @@
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
<div class="marginTopBot5">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Chutes Model Sorting">Chutes Model Sorting</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content m-b-1">
<div class="marginTopBot5">
<label for="chutes_sort_models" class="checkbox_label">
<select id="chutes_sort_models">
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
<option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option>
<option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option>
<option data-i18n="Context Size" value="context_length">Context Size</option>
</select>
</label>
</div>
</div>
</div>
</div>
</div>
<div id="nanogpt_form" data-source="nanogpt">
<h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
<a href="https://nano-gpt.com/balance" target="_blank" rel="noopener noreferrer" class="nanogpt_view_credits" data-i18n="View Remaining Credits">View Remaining Credits</a>
<span class="nanogpt_credits_display marginLeft5"></span>
<div class="flex-container">
<input id="api_key_nanogpt" name="api_key_nanogpt" class="text_pole flex1" value="" type="text" autocomplete="off">
<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_nanogpt"></div>
@@ -3670,6 +3648,39 @@
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
<div>
<h4>
<span data-i18n="Model Providers">Model Providers</span>
<i id="nanogpt_provider_warning" class="fa-solid fa-circle-exclamation displayNone" title="Deselect inapplicable provider(s) or select an applicable provider to avoid a 404 error."></i>
</h4>
<select id="nanogpt_provider">
<option value="" data-i18n="Auto">Auto</option>
</select>
<label class="checkbox_label marginTopBot5" for="nanogpt_payg_override" data-i18n="[title]Force NanoGPT pay-as-you-go billing for this request." title="Force NanoGPT pay-as-you-go billing for this request.">
<input id="nanogpt_payg_override" type="checkbox" />
<span data-i18n="Use pay-as-you-go billing">Use pay-as-you-go billing</span>
</label>
</div>
</div>
<div id="workers_ai_form" data-source="workers_ai">
<h4><a href="https://dash.cloudflare.com/?to=/:account/ai/workers-ai/api-quick-start" target="_blank" rel="noopener noreferrer" data-i18n="Cloudflare Workers AI API Key">Cloudflare Workers AI API Key</a></h4>
<div class="flex-container">
<input id="api_key_workers_ai" name="api_key_workers_ai" class="text_pole flex1" value="" type="text" autocomplete="off">
<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_workers_ai"></div>
</div>
<div data-for="api_key_workers_ai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
For privacy reasons, your API key will be hidden after you click 'Connect'.
</div>
<h4><a href="https://dash.cloudflare.com/?to=/:account/workers-and-pages" target="_blank" rel="noopener noreferrer" data-i18n="Cloudflare Account ID">Cloudflare Account ID</a></h4>
<div class="flex-container">
<input id="workers_ai_account_id" class="text_pole flex1" value="" type="text" autocomplete="off" placeholder="e.g. 023e105f4ecef8ad9ca31a8372d0c353" data-i18n="[placeholder]e.g. 023e105f4ecef8ad9ca31a8372d0c353">
</div>
<div>
<h4 data-i18n="Workers AI Model">Workers AI Model</h4>
<select id="model_workers_ai_select">
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
</div>
<div id="deepseek_form" data-source="deepseek">
<h4 data-i18n="DeepSeek API Key">DeepSeek API Key</h4>
@@ -3683,9 +3694,7 @@
<div>
<h4 data-i18n="DeepSeek Model">DeepSeek Model</h4>
<select id="model_deepseek_select">
<option value="deepseek-chat">deepseek-chat</option>
<option value="deepseek-coder">deepseek-coder</option>
<option value="deepseek-reasoner">deepseek-reasoner</option>
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
</div>
@@ -3924,6 +3933,7 @@
<h4 data-i18n="Z.AI Model">Z.AI Model</h4>
<select id="model_zai_select">
<option value="glm-5-turbo">glm-5-turbo</option>
<option value="glm-5v-turbo">glm-5v-turbo</option>
<option value="glm-5.1">glm-5.1</option>
<option value="glm-5">glm-5</option>
<option value="glm-4.7">glm-4.7</option>
@@ -3986,6 +3996,37 @@
<small data-i18n="The underlying model of your deployment. This is detected automatically when you connect.">The underlying model of your deployment. This is detected automatically when you connect.</small>
</div>
</div>
<div id="model_sorting_form" data-source="openrouter,chutes,electronhub,nanogpt,aimlapi">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Model Sorting">Model Sorting</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content m-b-1">
<div class="marginTopBot5">
<label for="cc_sort_models" class="checkbox_label">
<select id="cc_sort_models">
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
<option data-i18n="Prompt Price (cheapest)" value="pricing.prompt">Prompt Price (cheapest)</option>
<option data-i18n="Completion Price (cheapest)" value="pricing.completion">Completion Price (cheapest)</option>
<option data-i18n="Context Size" value="context_length">Context Size</option>
</select>
</label>
</div>
<div class="marginTopBot5">
<label for="cc_group_models" class="checkbox_label">
<input id="cc_group_models" type="checkbox" />
<span data-i18n="Group by vendors">Group by vendors</span>
</label>
<div class="toggle-description justifyLeft wide100p">
<span data-i18n="Group by vendors Description">
Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting.
</span>
</div>
</div>
</div>
</div>
</div>
<div id="prompt_post_processing_form">
<h4>
<span data-i18n="Prompt Post-Processing">
@@ -4365,7 +4406,7 @@
<small data-i18n="First User Prefix">First User Prefix</small>
<textarea id="instruct_first_input_sequence" data-macros class="text_pole textarea_compact autoSetHeight"></textarea>
</div>
<div class="flexAuto" title="Inserted before the last User's message." data-i18n="[title]instruct_last_input_sequence">
<div class="flexAuto" title="Inserted before the last User's message or as a last prompt line when generating an impersonation." data-i18n="[title]instruct_last_input_sequence">
<small data-i18n="Last User Prefix">Last User Prefix</small>
<textarea id="instruct_last_input_sequence" data-macros class="text_pole wide100p textarea_compact autoSetHeight"></textarea>
</div>
@@ -4775,7 +4816,7 @@
<input type="file" id="world_import_file" accept=".json,.lorebook,.png" name="avatar" hidden>
<div id="world_create_button" class="menu_button menu_button_icon">
<i class="fa-solid fa-globe"></i>
<span data-i18n="New">New</span>
<span data-i18n="Create">Create</span>
</div>
<small data-i18n="or">or</small>
<select id="world_editor_select">
@@ -6733,7 +6774,7 @@
<small class="chat_file_size select_chat_block_filename_item"></small>
<small class="chat_messages_num select_chat_block_filename_item"></small>
</div>
<div class="flex-container gap10px">
<div class="select_chat_actions flex-container gap10px">
<div title="Export JSONL chat file" data-format="jsonl" class="exportRawChatButton opacity50p hoverglow fa-solid fa-file-export" data-i18n="[title]Export JSONL chat file"></div>
<div title="Download chat as plain text document" data-format="txt" class="exportChatButton opacity50p hoverglow fa-solid fa-file-lines" data-i18n="[title]Download chat as plain text document"></div>
<div title="Delete chat file" file_name="" class="PastChat_cross opacity50p hoverglow fa-solid fa-skull" data-i18n="[title]Delete chat file"></div>
+3
View File
@@ -24,6 +24,7 @@ import chalk from 'chalk';
import yaml from 'yaml';
import * as chevrotain from 'chevrotain';
import { gzipSync, gzip } from 'fflate';
import { sha256 } from 'js-sha256';
/**
* Expose the libraries to the 'window' object.
@@ -105,6 +106,7 @@ export default {
chevrotain,
gzipSync,
gzip,
sha256,
};
export {
@@ -132,4 +134,5 @@ export {
chevrotain,
gzipSync,
gzip,
sha256,
};
+84 -28
View File
@@ -213,7 +213,7 @@ import {
tag_import_setting,
applyCharacterTagsToMessageDivs,
} from './scripts/tags.js';
import { initSecrets, readSecretState } from './scripts/secrets.js';
import { checkOpenRouterAuth, initSecrets, readSecretState } from './scripts/secrets.js';
import { markdownExclusionExt } from './scripts/showdown-exclusion.js';
import { markdownUnderscoreExt } from './scripts/showdown-underscore.js';
import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from './scripts/authors-note.js';
@@ -717,6 +717,7 @@ async function firstLoadInit() {
initLoaderOverlay.appendChild(splashMessage);
const initLoaderHandle = loader.show({
slug: 'app-init',
toastMode: loader.ToastMode.NONE,
overlayContent: initLoaderOverlay,
});
@@ -741,12 +742,13 @@ async function firstLoadInit() {
initKoboldSettings();
initNovelAISettings();
initSystemPrompts();
initExtensions();
await initExtensions();
initExtensionSlashCommands();
ToolManager.initToolSlashCommands();
await initPresetManager();
await initSystemMessages();
await getSettings(initLoaderHandle);
await checkOpenRouterAuth();
initKeyboard();
initDynamicStyles();
initTags();
@@ -1909,6 +1911,23 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
return mes;
}
/**
* Creates an Image element for the given API/model icon.
* The image references the matching SVG file from `/img/` and includes a tooltip with API and model info.
* The caller is responsible for appending the image to the DOM and optionally calling `SVGInject` on it.
*
* @param {string} apiName - API identifier matching an SVG file in /img/ (e.g. 'openai', 'openrouter', 'claude')
* @param {string} [modelName=''] - Model name shown in the tooltip
* @returns {HTMLImageElement} The image element (not yet in the DOM)
*/
export function createModelIcon(apiName, modelName = '') {
const image = new Image();
image.classList.add('icon-svg');
image.src = `/img/${apiName}.svg`;
image.title = modelName ? `${apiName} - ${modelName}` : apiName;
return image;
}
/**
* Inserts or replaces an SVG icon adjacent to the provided message's timestamp.
*
@@ -1916,11 +1935,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
* @param {ChatMessageExtra} extra - Contains the API and model details.
*/
function insertSVGIcon(mes, extra) {
// Determine the SVG filename
let modelName = extra?.api || '';
const apiName = extra?.api || '';
// If there's no API information, we can't determine which SVG to use
if (!modelName) {
if (!apiName) {
return;
}
@@ -1937,16 +1954,14 @@ function insertSVGIcon(mes, extra) {
};
};
const createModelImage = (className, targetSelector, insertBefore) => {
const image = new Image();
image.classList.add('icon-svg', className);
image.src = `/img/${modelName}.svg`;
image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
const insertIcon = (className, targetSelector, insertBefore) => {
const image = createModelIcon(apiName, extra?.model);
image.classList.add(className);
insertOrReplaceSVG(image, className, targetSelector, insertBefore);
};
createModelImage('timestamp-icon', '.timestamp');
createModelImage('thinking-icon', '.mes_reasoning_header_title', true);
insertIcon('timestamp-icon', '.timestamp');
insertIcon('thinking-icon', '.mes_reasoning_header_title', true);
}
/**
@@ -2907,6 +2922,11 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou
export function substituteParams(content, options = {}) {
if (!content) return '';
if (typeof content !== 'string') {
console.warn('substituteParams: content will be coerced to string', content);
content = String(content);
}
// Handle legacy signature calls to substituteParams
// We'll simply re-route them to a temporary legacy function. In the future, we'll remove this and cleanly build the options object ourselves.
const isOptionsObject = options && typeof options === 'object' && !Array.isArray(options);
@@ -3847,9 +3867,7 @@ export function createRawPrompt(prompt, api, instructOverride, quietToLoud, syst
// If the prompt was given as a string, convert to a message-style object assuming user role
if (typeof prompt === 'string') {
const message = api === 'openai'
? { role: 'user', content: prompt.trim() }
: { role: 'system', content: prompt };
const message = { role: 'user', content: prompt.trim() };
prompt = [message];
} else { // checks for message-style object
if (prompt.length === 0 && !systemPrompt) throw Error('No messages provided');
@@ -3876,7 +3894,12 @@ export function createRawPrompt(prompt, api, instructOverride, quietToLoud, syst
// prepend system prompt, if provided
if (systemPrompt) {
systemPrompt = substituteParams(systemPrompt);
systemPrompt = isInstruct ? (formatInstructModeStoryString(systemPrompt) + '\n') : systemPrompt.trim();
systemPrompt = isInstruct ? formatInstructModeStoryString(systemPrompt) : systemPrompt.trim();
if (isInstruct && systemPrompt.length > 0 && !systemPrompt.endsWith('\n')) {
if (power_user.instruct.wrap && !power_user.instruct.story_string_suffix) {
systemPrompt += '\n';
}
}
prompt.unshift({ role: 'system', content: systemPrompt });
}
@@ -3906,7 +3929,7 @@ export function createRawPrompt(prompt, api, instructOverride, quietToLoud, syst
* @prop {number} [responseLength] Maximum response length. If unset, the global default value is used.
* @prop {boolean} [trimNames] Whether to allow trimming "{{user}}:" and "{{char}}:" from the response.
* @prop {string} [prefill] An optional prefill for the prompt.
* @prop {object} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.
* @prop {JsonSchema} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.
*/
/**
@@ -4018,7 +4041,7 @@ export async function generateRawData({ prompt = '', api = null, instructOverrid
}
if (jsonSchema) {
return extractJsonFromData(data, { mainApi: api });
return extractJsonFromData(data, { mainApi: api, returnInvalidJson: jsonSchema.returnInvalid });
}
return data;
@@ -4181,6 +4204,7 @@ function removeLastMessage() {
* @property {object} value JSON schema value.
* @property {string} [description] Description of the schema.
* @property {boolean} [strict] If true, the schema will be used in strict mode, meaning that only the fields defined in the schema will be allowed.
* @property {boolean} [returnInvalid] If true, a string that can't be parsed as a JSON will be returned as is, instead of an empty object.
*
* @typedef {object} GenerateOptions
* @property {boolean} [automatic_trigger] If the generation was triggered automatically (e.g. group auto mode).
@@ -4267,7 +4291,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (selected_group && !is_group_generating) {
if (!dryRun) {
// Returns the promise that generateGroupWrapper returns; resolves when generation is done
return generateGroupWrapper(false, type, { quiet_prompt, force_chid, signal: abortController.signal, quietImage });
return generateGroupWrapper(false, type, { quiet_prompt, force_chid, signal: abortController.signal, quietImage, jsonSchema });
}
const characterIndexMap = new Map(characters.map((char, index) => [char.avatar, index]));
@@ -4449,9 +4473,15 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
for (let i = coreChat.length - 1; i >= 0; i--) {
const depth = coreChat.length - i - (isContinue ? 2 : 1);
const isPrefix = isContinue && i === coreChat.length - 1;
// In group chats, only include reasoning from the currently generating character
const isOtherGroupMember = selected_group && coreChat[i].name !== name2;
coreChat[i] = {
...coreChat[i],
mes: promptReasoning.addToMessage(
mes: isOtherGroupMember
? coreChat[i].mes
: promptReasoning.addToMessage(
coreChat[i].mes,
getRegexedString(
String(coreChat[i].extra?.reasoning ?? ''),
@@ -4697,7 +4727,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST);
}
if (lastUserMessageIndex >= 0 && j === lastUserMessageIndex && isInstruct) {
if (lastUserMessageIndex >= 0 && j === lastUserMessageIndex && isInstruct && !isImpersonate) {
// Reformat with the last input sequence (if any)
chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST);
}
@@ -5301,7 +5331,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
streamingProcessor.firstMessageText = '';
}
streamingProcessor.generator = await sendStreamingRequest(type, generate_data);
streamingProcessor.generator = await sendStreamingRequest(type, generate_data, { jsonSchema });
hideSwipeButtons();
let getMessage = await streamingProcessor.generate();
@@ -5390,7 +5420,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (jsonSchema) {
unblockGeneration(type);
return extractJsonFromData(data);
return extractJsonFromData(data, { returnInvalidJson: jsonSchema.returnInvalid ?? false });
}
//const getData = await response.json();
@@ -5823,11 +5853,11 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
} else {
chat.push(message);
await saveChatConditional();
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_SENT, chat_id);
addOneMessage(message);
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
await saveChatConditional();
}
return message;
@@ -6003,7 +6033,7 @@ function setInContextMessages(msgInContextCount, type) {
if (lastMessageBlock.length === 0) {
const firstMessageId = getFirstDisplayedMessageId();
chatElement.find(`.mes[mesid="${firstMessageId}"`).addClass('lastInContext');
chatElement.find(`.mes[mesid="${firstMessageId}"]`).addClass('lastInContext');
}
// Update last id to chat. No metadata save on purpose, gets hopefully saved via another call
@@ -6213,9 +6243,13 @@ export function extractMessageFromData(data, activeApi = null) {
/**
* Extracts JSON from the response data.
* @param {object} data Response data
* @param {object} [options] Extraction options
* @param {string} [options.mainApi] Main API to use
* @param {string} [options.chatCompletionSource] Chat completion source
* @param {boolean} [options.returnInvalidJson=false] Whether to return the raw JSON string even if it fails to parse
* @returns {string} Extracted JSON string from the response data
*/
export function extractJsonFromData(data, { mainApi = null, chatCompletionSource = null } = {}) {
export function extractJsonFromData(data, { mainApi = null, chatCompletionSource = null, returnInvalidJson = false } = {}) {
mainApi = mainApi ?? main_api;
chatCompletionSource = chatCompletionSource ?? oai_settings.chat_completion_source;
@@ -6238,6 +6272,9 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
break;
case chat_completion_sources.PERPLEXITY:
result = tryParse(removeReasoningFromString(text));
if (!result && returnInvalidJson) {
return text;
}
break;
case chat_completion_sources.VERTEXAI:
case chat_completion_sources.MAKERSUITE:
@@ -6258,6 +6295,9 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
case chat_completion_sources.ZAI:
default:
result = tryParse(text);
if (!result && returnInvalidJson) {
return text;
}
break;
}
} break;
@@ -7924,6 +7964,15 @@ export async function getSettings(initLoaderHandle = null) {
const isVersionChanged = settings.currentVersion !== currentVersion;
await loadExtensionSettings(settings, isVersionChanged, enableAutoUpdate);
await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
} else {
Object.assign(extension_settings, (settings.extension_settings ?? {}));
$('#third_party_extension_button').addClass('disabled');
$('#extensions_details').addClass('disabled');
$('#extensions_connect').addClass('disabled');
$('#extensions_notify_updates').attr('disabled', 'disabled');
$('#extensions_autoconnect').attr('disabled', 'disabled');
$('#extensions_url').attr('disabled', 'disabled');
$('#extensions_api_key').attr('disabled', 'disabled');
}
firstRun = !!settings.firstRun;
@@ -10565,6 +10614,7 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
}
const loaderHandle = showLoader ? loader.show({
slug: 'chat-rename',
title: t`Rename Chat`,
message: t`Renaming chat…`,
toastMode: loader.ToastMode.STATIC,
@@ -10602,6 +10652,9 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
if (currentChatId) {
await reloadCurrentChat();
}
const eventData = { avatarId: body.avatar_url, groupId, oldFileName: body.original_file, newFileName: body.renamed_file };
await eventSource.emit(event_types.CHAT_RENAMED, eventData);
} catch {
await delay(500);
await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT);
@@ -11166,6 +11219,7 @@ jQuery(async function () {
$('#select_chat_cross').trigger('click');
const loaderHandle = loader.show({
slug: 'chat-delete',
title: t`Delete Chat`,
message: t`Deleting chat…`,
toastMode: loader.ToastMode.STATIC,
@@ -12460,7 +12514,9 @@ jQuery(async function () {
$('#avatar-and-name-block').slideToggle();
});
$(document).on('mouseup touchend', '#show_more_messages', async function () {
$(document).on('click', '#show_more_messages', async function (event) {
event.stopPropagation();
event.preventDefault();
await showMoreMessages();
});
+1
View File
@@ -848,6 +848,7 @@ class BulkEditOverlay {
const deleteChats = checkbox.prop('checked') ?? false;
const loaderHandle = loader.show({
slug: 'bulk-delete',
title: t`Bulk Delete`,
message: t`Deleting ${characterIds.length} character(s)…`,
toastMode: loader.ToastMode.STATIC,
+2
View File
@@ -407,6 +407,8 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
|| (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)
|| (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
|| (secret_state[SECRET_KEYS.WORKERS_AI] && oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI)
|| (secret_state[SECRET_KEYS.MINIMAX] && oai_settings.chat_completion_source == chat_completion_sources.MINIMAX)
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
|| (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
) {
+1
View File
@@ -16,6 +16,7 @@ const buttonSelectors = [
'.jg-menu .jg-button',
'.bg_example .mobile-only-menu-toggle',
'.paginationjs-pages li a',
'#show_more_messages',
].join(', ');
const listSelectors = [
+22 -7
View File
@@ -1,4 +1,4 @@
import { ActionLoaderToastMode, getActiveLoaderHandles, getLoaderHandleById, hideActionLoader, showActionLoader } from './action-loader.js';
import { ActionLoaderToastMode, getActiveLoaderHandles, getLoaderHandleById, loader } from './action-loader.js';
import { t } from './i18n.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
@@ -7,6 +7,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { isFalseBoolean } from './utils.js';
import { DOMPurify } from '../lib.js';
/**
* Registers slash commands for the action loader module.
@@ -115,6 +116,12 @@ export function registerActionLoaderSlashCommands() {
description: 'Optional title for the toast notification',
typeList: [ARGUMENT_TYPE.STRING],
}),
SlashCommandNamedArgument.fromProps({
name: 'slug',
description: 'Unique slug for the loader (to identify it easily via code or CSS)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'slash-wrap',
}),
SlashCommandNamedArgument.fromProps({
name: 'stopTooltip',
description: 'Tooltip text for the stop button (only used when toast=stoppable)',
@@ -148,7 +155,8 @@ export function registerActionLoaderSlashCommands() {
const title = args.title ? String(args.title) : '';
const stopTooltip = String(args.stopTooltip ?? t`Stop`);
const loader = showActionLoader({
const actionLoader = loader.show({
slug: typeof args.slug === 'string' ? String(args.slug) : 'slash-wrap',
blocking,
toastMode,
message,
@@ -162,7 +170,7 @@ export function registerActionLoaderSlashCommands() {
const result = await closureCopy.execute();
return result.pipe;
} finally {
await loader.hide();
await actionLoader.hide();
}
},
}));
@@ -231,6 +239,12 @@ export function registerActionLoaderSlashCommands() {
description: 'Optional title for the toast notification',
typeList: [ARGUMENT_TYPE.STRING],
}),
SlashCommandNamedArgument.fromProps({
name: 'slug',
description: 'Unique slug for the loader (to identify it easily via code or CSS)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'slash-show',
}),
SlashCommandNamedArgument.fromProps({
name: 'stopTooltip',
description: 'Tooltip text for the stop button (only used when toast=stoppable)',
@@ -258,11 +272,12 @@ export function registerActionLoaderSlashCommands() {
const title = args.title ? String(args.title) : '';
const stopTooltip = String(args.stopTooltip ?? t`Stop`);
const handle = showActionLoader({
const handle = loader.show({
slug: typeof args.slug === 'string' ? String(args.slug) : 'slash-show',
blocking,
toastMode,
message,
title,
message: DOMPurify.sanitize(message),
title: DOMPurify.sanitize(title),
stopTooltip,
onStop: createClosureHandler(args.onStop),
onHide: createClosureHandler(args.onHide, { argName: 'onHide' }),
@@ -307,7 +322,7 @@ export function registerActionLoaderSlashCommands() {
}
// No handle provided - hide all active loaders
const result = await hideActionLoader();
const result = await loader.hide();
return result ? 'true' : 'false';
},
}));
+47 -2
View File
@@ -33,6 +33,7 @@ export const ActionLoaderToastMode = {
* @typedef {object} ActionLoaderOptions
* @property {boolean} [blocking=true] - Whether to show the blocking overlay. Set to false for non-blocking toast-only loaders.
* @property {ActionLoaderToastMode} [toastMode='stoppable'] - Toast display mode
* @property {string} [slug=null] - Unique slug for the loader to identify it easily via code or CSS
* @property {string} [message='Generating...'] - The message to display in the toast
* @property {string} [title] - Optional title for the toast notification
* @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button
@@ -73,8 +74,20 @@ function hasBlockingLoaders() {
* Manages its own toast, stop handler, and lifecycle.
*/
export class ActionLoaderHandle {
/**
* A special empty handle that is already disposed. Useful as a default value to avoid null checks.
* Does not generate any id, toast, or overlay, and all its methods are no-ops.
* @type {ActionLoaderHandle}
*/
static get EMPTY() {
return new ActionLoaderHandle({ predisposed: true });
}
/** @type {string} Unique identifier for this handle */
id;
#id;
/** @type {string|null} Unique slug for the loader */
#slug = null;
/** @type {JQuery<HTMLElement>|null} The toast element for this loader */
#toast = null;
@@ -96,9 +109,11 @@ export class ActionLoaderHandle {
* @param {object} options - Configuration options
* @param {boolean} [options.blocking=true] - Whether to show blocking overlay
* @param {ActionLoaderToastMode} [options.toastMode] - Toast display mode
* @param {string|null} [options.slug] - Unique slug for the loader (to identify it easily via code or CSS)
* @param {string} [options.message='Generating...'] - Message to display in the toast
* @param {string} [options.title] - Title for the toast notification
* @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button
* @param {boolean} [options.predisposed=false] - Whether this handle is already disposed (for special use)
* @param {HTMLElement|string|null} [options.overlayContent] - Custom content for the overlay (replaces default spinner)
* @param {(() => void)|null} [options.onStop] - Custom stop handler
* @param {(() => void)|null} [options.onHide] - Custom hide handler
@@ -106,14 +121,22 @@ export class ActionLoaderHandle {
constructor({
blocking = true,
toastMode = ActionLoaderToastMode.STOPPABLE,
slug = null,
message = t`Generating...`,
title = '',
stopTooltip = t`Stop`,
overlayContent = null,
onStop = null,
onHide = null,
predisposed = false,
} = {}) {
this.id = generateLoaderId();
if (predisposed) {
this.#disposed = true;
return;
}
this.#id = generateLoaderId();
this.#slug = slug;
this.#blocking = blocking;
this.#onStop = onStop;
this.#onHide = onHide;
@@ -148,6 +171,12 @@ export class ActionLoaderHandle {
const toastContent = document.createElement('div');
toastContent.className = 'action-loader-toast';
if (this.#slug) {
toastContent.dataset.slug = this.#slug;
}
toastContent.dataset.loaderId = this.#id;
toastContent.dataset.blocking = this.#blocking.toString();
const messageSpan = document.createElement('span');
messageSpan.className = 'action-loader-message';
messageSpan.textContent = message;
@@ -201,6 +230,22 @@ export class ActionLoaderHandle {
}
}
/**
* The unique identifier for this loader handle.
* @returns {string}
*/
get id() {
return this.#id;
}
/**
* The unique slug for this loader handle, used to identify it easily via code or CSS.
* @returns {string|null}
*/
get slug() {
return this.#slug;
}
/**
* Whether this handle is still active (not disposed).
* @returns {boolean}
+1
View File
@@ -700,6 +700,7 @@ export function initBookmarks() {
}
const loaderHandle = loader.show({
slug: 'chat-load',
title: t`Chat History`,
message: t`Loading chat…`,
toastMode: loader.ToastMode.STATIC,
+8
View File
@@ -72,6 +72,14 @@ function applyBrowserFixes() {
if (isMobile()) {
const fixFunkyPositioning = () => {
if (isFirefox()) {
const active = document.activeElement;
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) {
// The positioning hack below breaks GBoard candidate replacement
// in Firefox Mobile on Android.
return;
}
}
console.debug('[Mobile] Device viewport change detected.');
document.documentElement.style.position = 'fixed';
requestAnimationFrame(() => document.documentElement.style.position = '');
+4 -1
View File
@@ -2288,11 +2288,14 @@ export function initChatUtilities() {
await callGenericPopup(wrapper, POPUP_TYPE.TEXT, '', { wide: true, large: true });
});
$(document).on('click', 'body .mes .mes_text', function () {
$(document).on('click', 'body .mes .mes_text, body .mes .mes_reasoning', function (event) {
if (!power_user.click_to_edit) return;
if (window.getSelection().toString()) return;
if ($('.edit_textarea').length) return;
$(this).closest('.mes').find('.mes_edit').trigger('click');
if ($(event.target).closest('.mes_reasoning').length) {
$('.reasoning_edit_textarea').trigger('focus');
}
});
$(document).on('click', '.open_media_overrides', openExternalMediaOverridesDialog);
+3 -2
View File
@@ -51,6 +51,7 @@ import EventSourceStream from './sse-stream.js';
* @property {string} [reverse_proxy] - Optional reverse proxy URL
* @property {string} [proxy_password] - Optional proxy password
* @property {string} [custom_prompt_post_processing] - Optional custom prompt post-processing
* @property {import('../script.js').JsonSchema} [json_schema] - Optional JSON schema for structured generation
*/
/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
@@ -505,7 +506,7 @@ export class ChatCompletionService {
return async function* streamData() {
let text = '';
const swipes = [];
const state = { reasoning: '', image: '' };
const state = { reasoning: '', images: [], signature: '', toolSignatures: {} };
while (true) {
const { done, value } = await reader.read();
if (done) return;
@@ -591,7 +592,7 @@ export class ChatCompletionService {
}
// Ensure api-url is properly applied for all sources that accept it
['custom_url', 'vertexai_region', 'zai_endpoint', 'siliconflow_endpoint'].forEach(field => {
['custom_url', 'vertexai_region', 'zai_endpoint', 'siliconflow_endpoint', 'minimax_endpoint'].forEach(field => {
// The order is: connection profile => CC preset => CC settings
overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field];
});
+6
View File
@@ -126,6 +126,12 @@ function applyDynamicFocusStyles(styleSheet, { fromExtension = false } = {}) {
// If something like :focus-within or a more specific selector like `.blah:has(:focus-visible)` for elements inside,
// it should be manually defined in CSS.
const focusSelector = rule.selectorText.replace(/:hover/g, ':focus-visible');
// Skip pseudo-elements (::before, ::after, ::-webkit-scrollbar, etc.)
// as they cannot have :focus-visible appended (invalid CSS syntax)
if (focusSelector.includes('::')) {
return;
}
let focusRule = `${focusSelector} { ${rule.style.cssText} }`;
// Wrap the generated rule into the same @media/@supports/@container chain (if any)
+8
View File
@@ -50,6 +50,7 @@ export const event_types = {
FORCE_SET_BACKGROUND: 'force_set_background',
CHAT_DELETED: 'chat_deleted',
CHAT_CREATED: 'chat_created',
CHAT_RENAMED: 'chat_renamed',
GROUP_CHAT_DELETED: 'group_chat_deleted',
GROUP_CHAT_CREATED: 'group_chat_created',
GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
@@ -97,9 +98,16 @@ export const event_types = {
WORLDINFO_SCAN_DONE: 'worldinfo_scan_done',
MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',
PERSONA_CHANGED: 'persona_changed',
PERSONA_CREATED: 'persona_created',
PERSONA_UPDATED: 'persona_updated',
PERSONA_RENAMED: 'persona_renamed',
PERSONA_DELETED: 'persona_deleted',
TTS_JOB_STARTED: 'tts_job_started',
TTS_AUDIO_READY: 'tts_audio_ready',
TTS_JOB_COMPLETE: 'tts_job_complete',
ITEMIZED_PROMPTS_LOADED: 'itemized_prompts_loaded',
ITEMIZED_PROMPTS_SAVED: 'itemized_prompts_saved',
ITEMIZED_PROMPTS_DELETED: 'itemized_prompts_deleted',
};
export const eventSource = new EventEmitter([event_types.APP_READY, event_types.APP_INITIALIZED]);
+555 -117
View File
@@ -1,9 +1,9 @@
import { DOMPurify, Popper } from '../lib.js';
import { Popper } from '../lib.js';
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
import { delay, deleteValueByPath, equalsIgnoreCaseAndAccents, escapeHtml, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
import { getContext } from './st-context.js';
import { isAdmin } from './user.js';
import { addLocaleData, getCurrentLocale, t } from './i18n.js';
@@ -61,6 +61,19 @@ let manifests = {};
*/
const defaultUrl = 'http://localhost:5100';
/**
* Checks if the extension is officially supported by its URL pattern.
* @param {string} url URL to check
* @returns {boolean} True if the URL matches the pattern, false otherwise (or not a valid URL)
*/
export const isOfficialExtension = (url) => {
try {
return /^https:\/\/github\.com\/SillyTavern\/(.+)$/i.test(new URL(url).href);
} catch (e) {
return false;
}
};
let requiresReload = false;
let stateChanged = false;
let saveMetadataTimeout = null;
@@ -266,6 +279,18 @@ export async function doExtrasFetch(endpoint, args = {}) {
return await fetch(endpoint, args);
}
/**
* Generates a CSS selector for an extension based on its name, allowing omission of a common prefix.
* @param {string} name Name of the extension, with or without the "third-party" prefix
* @param {object} [options] Optional parameters
* @param {string} [options.prefix] Optional prefix to ignore when generating the selector (e.g. "third-party")
* @returns {string} CSS selector for the extension, with the prefix removed if it was present and specified in options
*/
function getNameSelector(name, { prefix = 'third-party' } = {}) {
const nameWithoutPrefix = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
return CSS.escape(nameWithoutPrefix);
}
/**
* Discovers extensions from the API.
* @returns {Promise<{name: string, type: string}[]>}
@@ -343,7 +368,7 @@ function onToggleAllExtensions(extensionsToToggle, toggleContainer) {
}
toggleContainer
.find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
.find(`.extension_block[data-name="${getNameSelector(name)}"] .extension_toggle input`)
.prop('checked', enable)
.toggleClass('toggle_enable', !enable)
.toggleClass('toggle_disable', enable)
@@ -354,12 +379,28 @@ function onToggleAllExtensions(extensionsToToggle, toggleContainer) {
return extensionsToToggle;
}
/**
* Checks whether an extension has a specific hook defined in its manifest.
* @param {string} name Extension name (with or without 'third-party' prefix)
* @param {'install' | 'update' | 'delete' | 'clean' | 'enable' | 'disable' | 'activate'} hookName The hook to check
* @returns {boolean}
*/
function hasExtensionHook(name, hookName) {
const fullName = name.startsWith('third-party') ? name : `third-party${name}`;
const manifest = manifests[fullName];
if (!manifest || !manifest.hooks || typeof manifest.hooks !== 'object') {
return false;
}
const hookFunctionName = manifest.hooks[hookName];
return typeof hookFunctionName === 'string' && hookFunctionName.length > 0;
}
/**
* Calls a manifest hook for an extension.
* Hooks are optional function names exported from the extension's JS entry point module.
* The hook function can optionally return a Promise that will be awaited.
* @param {string} name Extension name
* @param {'install' | 'update' | 'delete' | 'enable' | 'disable' | 'activate'} hookName The hook to call
* @param {'install' | 'update' | 'delete' | 'clean' | 'enable' | 'disable' | 'activate'} hookName The hook to call
* @returns {Promise<void>}
*/
async function callExtensionHook(name, hookName) {
@@ -473,6 +514,21 @@ export function findExtension(name) {
return { name: internalExtensionName, enabled: isEnabled };
}
/**
* Returns a deep clone of the manifest for the given extension name.
* Accepts either the short name (e.g. `SillyTavern-MyExtension`) or the full internal key
* (e.g. `third-party/SillyTavern-MyExtension`). Returns null if the extension is not found.
* @param {string} name - Extension name or internal key
* @returns {object|null} Cloned manifest object, or null if not found
*/
export function getExtensionManifest(name) {
const found = extensionNames.find(extName =>
equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`),
);
const manifest = found ? manifests[found] : null;
return manifest ? structuredClone(manifest) : null;
}
/**
* Loads manifest.json files for extensions.
* @param {string[]} names Array of extension names
@@ -821,7 +877,7 @@ function addExtensionLocale(name, manifest) {
}
/**
* Generates HTML string for displaying an extension in the UI.
* Generates an element for displaying an extension in the UI.
*
* @param {string} name - The name of the extension.
* @param {object} manifest - The manifest of the extension.
@@ -829,95 +885,180 @@ function addExtensionLocale(name, manifest) {
* @param {boolean} isDisabled - Whether the extension is disabled or not.
* @param {boolean} isExternal - Whether the extension is external or not.
* @param {string} checkboxClass - The class for the checkbox HTML element.
* @return {string} - The HTML string that represents the extension.
* @return {HTMLElement} - The element that represents the extension.
*/
function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) {
function generateExtensionElement(name, manifest, isActive, isDisabled, isExternal, checkboxClass) {
function getExtensionIcon() {
const type = getExtensionType(name);
const icon = document.createElement('i');
icon.classList.add('fa-sm', 'fa-fw', 'fa-solid');
switch (type) {
case 'global':
return '<i class="fa-sm fa-fw fa-solid fa-server" data-i18n="[title]ext_type_global" title="This is a global extension, available for all users."></i>';
icon.classList.add('fa-server');
icon.title = t`This is a global extension, available for all users.`;
break;
case 'local':
return '<i class="fa-sm fa-fw fa-solid fa-user" data-i18n="[title]ext_type_local" title="This is a local extension, available only for you."></i>';
icon.classList.add('fa-user');
icon.title = t`This is a local extension, available only for you.`;
break;
case 'system':
return '<i class="fa-sm fa-fw fa-solid fa-cog" data-i18n="[title]ext_type_system" title="This is a built-in extension. It cannot be deleted and updates with the app."></i>';
icon.classList.add('fa-cog');
icon.title = t`This is a built-in extension. It cannot be deleted and updates with the app.`;
break;
default:
return '<i class="fa-sm fa-fw fa-solid fa-question" title="Unknown extension type."></i>';
icon.classList.add('fa-question');
icon.title = t`Unknown extension type.`;
break;
}
return icon;
}
const isUserAdmin = isAdmin();
const extensionIcon = getExtensionIcon();
const displayName = manifest.display_name;
const displayVersion = manifest.version || '';
const externalId = name.replace('third-party', '');
let originHtml = '';
if (isExternal) {
originHtml = '<a>';
// Root block
const block = document.createElement('div');
block.classList.add('extension_block');
block.dataset.name = externalId;
// Toggle
const toggleDiv = document.createElement('div');
toggleDiv.classList.add('extension_toggle');
const toggle = document.createElement('input');
toggle.type = 'checkbox';
toggle.dataset.name = name;
if (isActive || isDisabled) {
toggle.title = t`Click to toggle`;
toggle.classList.add(isActive ? 'toggle_disable' : 'toggle_enable');
if (checkboxClass) toggle.classList.add(checkboxClass);
toggle.checked = isActive;
} else {
toggle.title = t`Cannot enable extension`;
toggle.classList.add('extension_missing');
if (checkboxClass) toggle.classList.add(checkboxClass);
toggle.disabled = true;
}
toggleDiv.appendChild(toggle);
block.appendChild(toggleDiv);
let toggleElement = isActive || isDisabled ?
'<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
`<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
// Icon
const iconDiv = document.createElement('div');
iconDiv.classList.add('extension_icon');
iconDiv.appendChild(getExtensionIcon());
block.appendChild(iconDiv);
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
let branchButton = isExternal && isUserAdmin ? `<button class="btn_branch menu_button" data-name="${externalId}" data-i18n="[title]Switch branch" title="Switch branch"><i class="fa-solid fa-code-branch fa-fw"></i></button>` : '';
let modulesInfo = '';
// Text block
const textBlock = document.createElement('div');
textBlock.classList.add('flexGrow', 'extension_text_block');
const statusSpan = document.createElement('span');
statusSpan.className = isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing';
const nameSpan = document.createElement('span');
nameSpan.classList.add('extension_name');
nameSpan.textContent = displayName;
const authorSpan = document.createElement('span');
authorSpan.classList.add('extension_author');
const versionSpan = document.createElement('span');
versionSpan.classList.add('extension_version');
versionSpan.textContent = displayVersion;
statusSpan.append(nameSpan, authorSpan, versionSpan);
if (isActive && Array.isArray(manifest.optional)) {
const optional = new Set(manifest.optional);
modules.forEach(x => optional.delete(x));
if (optional.size > 0) {
const optionalString = DOMPurify.sanitize([...optional].join(', '));
modulesInfo = '<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
const modulesDiv = document.createElement('div');
modulesDiv.classList.add('extension_modules');
const optionalSpan = document.createElement('span');
optionalSpan.classList.add('optional');
optionalSpan.textContent = [...optional].join(', ');
modulesDiv.append(t`Optional modules:`, ' ', optionalSpan);
statusSpan.appendChild(modulesDiv);
}
} else if (!isDisabled) { // Neither active nor disabled
} else if (!isDisabled) {
// Neither active nor disabled
const requirements = new Set(manifest.requires);
modules.forEach(x => requirements.delete(x));
if (requirements.size > 0) {
const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
modulesInfo = `<div class="extension_modules">Missing modules: <span class="failure">${requirementsString}</span></div>`;
const modulesDiv = document.createElement('div');
modulesDiv.classList.add('extension_modules');
const failureSpan = document.createElement('span');
failureSpan.classList.add('failure');
failureSpan.textContent = [...requirements].join(', ');
modulesDiv.append(t`Missing modules:`, ' ', failureSpan);
statusSpan.appendChild(modulesDiv);
}
}
// if external, wrap the name in a link to the repo
if (isExternal) {
const originLink = document.createElement('a');
originLink.appendChild(statusSpan);
textBlock.appendChild(originLink);
} else {
textBlock.appendChild(statusSpan);
}
let extensionHtml = `
<div class="extension_block" data-name="${externalId}">
<div class="extension_toggle">
${toggleElement}
</div>
<div class="extension_icon">
${extensionIcon}
</div>
<div class="flexGrow extension_text_block">
${originHtml}
<span class="${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}">
<span class="extension_name">${DOMPurify.sanitize(displayName)}</span>
<span class="extension_version">${DOMPurify.sanitize(displayVersion)}</span>
${modulesInfo}
</span>
${isExternal ? '</a>' : ''}
</div>
block.appendChild(textBlock);
<div class="extension_actions flex-container alignItemsCenter">
${updateButton}
${branchButton}
${moveButton}
${deleteButton}
</div>
</div>`;
// Actions
const actionsDiv = document.createElement('div');
actionsDiv.classList.add('extension_actions', 'flex-container', 'alignItemsCenter');
return extensionHtml;
/**
* Helper function to create an action button for an extension.
* @param {string} cls Class name
* @param {string} dataName Name of the extension
* @param {string} title Title of the button
* @param {string} iconClasses Classes for the icon
* @returns {HTMLButtonElement} The created button element
*/
function makeActionButton(cls, dataName, title, iconClasses) {
const btn = document.createElement('button');
btn.classList.add(cls, 'menu_button');
btn.dataset.name = dataName;
btn.title = title;
const icon = document.createElement('i');
icon.classList.add(...iconClasses.split(' '));
btn.appendChild(icon);
return btn;
}
if (isExternal) {
const updateBtn = makeActionButton('btn_update', externalId, t`Update available`, 'fa-solid fa-download fa-fw');
updateBtn.classList.add('displayNone');
actionsDiv.appendChild(updateBtn);
}
if (isExternal && hasExtensionHook(externalId, 'clean')) {
actionsDiv.appendChild(makeActionButton('btn_clean', externalId, t`Clean extension data`, 'fa-fw fa-solid fa-broom'));
}
if (isExternal && isUserAdmin) {
actionsDiv.appendChild(makeActionButton('btn_branch', externalId, t`Switch branch`, 'fa-solid fa-code-branch fa-fw'));
actionsDiv.appendChild(makeActionButton('btn_move', externalId, t`Move`, 'fa-solid fa-folder-tree fa-fw'));
}
if (isExternal) {
actionsDiv.appendChild(makeActionButton('btn_delete', externalId, t`Delete`, 'fa-fw fa-solid fa-trash-can'));
}
block.appendChild(actionsDiv);
return block;
}
/**
* Gets extension data and generates the corresponding HTML for displaying the extension.
* Gets extension data and generates the corresponding element for displaying the extension.
*
* @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest.
* @return {object} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string.
* @return {{isExternal: boolean, extensionElement: HTMLElement}} - An object with 'isExternal' indicating whether the extension is external, and 'extensionElement' for the extension's HTML element.
*/
function getExtensionData(extension) {
const name = extension[0];
@@ -927,33 +1068,43 @@ function getExtensionData(extension) {
const isExternal = name.startsWith('third-party');
const checkboxClass = isDisabled ? 'checkbox_disabled' : '';
const extensionElement = generateExtensionElement(name, manifest, isActive, isDisabled, isExternal, checkboxClass);
const extensionHtml = generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass);
return { isExternal, extensionHtml };
return { isExternal, extensionElement };
}
/**
* Gets the module information to be displayed.
*
* @return {string} - The HTML string for the module information.
* @return {HTMLElement} - The element containing the module information.
*/
function getModuleInformation() {
let moduleInfo = modules.length ? `<p>${DOMPurify.sanitize(modules.join(', '))}</p>` : '<p class="failure">' + t`Not connected to the API!` + '</p>';
return `
<h3>` + t`Modules provided by your Extras API:` + `</h3>
${moduleInfo}
`;
const container = document.createElement('div');
const heading = document.createElement('h3');
heading.textContent = t`Modules provided by your Extras API:`;
container.appendChild(heading);
const moduleInfo = document.createElement('p');
if (modules.length) {
moduleInfo.textContent = modules.join(', ');
} else {
moduleInfo.classList.add('failure');
moduleInfo.textContent = t`Not connected to the API!`;
}
container.appendChild(moduleInfo);
return container;
}
/**
* Generates HTML for the extension load errors.
* @returns {string} HTML string containing the errors that occurred while loading extensions.
* Generates HTMLElement for the extension load errors.
* @returns {HTMLElement} - The element containing the extension load errors.
*/
function getExtensionLoadErrorsHtml() {
function getExtensionLoadErrors() {
if (extensionLoadErrors.size === 0) {
return '';
return document.createElement('div');
}
const container = document.createElement('div');
@@ -965,7 +1116,7 @@ function getExtensionLoadErrorsHtml() {
container.appendChild(errorElement);
}
return container.outerHTML;
return container;
}
/**
@@ -982,22 +1133,35 @@ async function showExtensionsDetails() {
initialScrollTop = oldPopup.content.scrollTop;
await oldPopup.completeCancelled();
}
const htmlErrors = getExtensionLoadErrorsHtml();
const htmlDefault = $('<div class="marginBot10"><h3>' + t`Built-in Extensions:` + '</h3></div>');
const errors = getExtensionLoadErrors();
const htmlExternal = $(`<div class="marginBot10">
<div class="flex-container alignitemscenter spaceBetween flexnowrap marginBot10">
<h3 class="margin0">${t`Installed Extensions:`}</h3>
<div class="flex-container third_party_toolbar"></div>
</div>
</div>`);
const defaultContainer = document.createElement('div');
defaultContainer.classList.add('marginBot10');
const defaultHeading = document.createElement('h3');
defaultHeading.textContent = t`Built-in Extensions:`;
defaultContainer.appendChild(defaultHeading);
const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
<i class="fa-solid fa-spinner fa-spin"></i>
<span>` + t`Loading third-party extensions... Please wait...` + `</span>
</div>`);
const externalContainer = document.createElement('div');
externalContainer.classList.add('marginBot10');
const externalHeader = document.createElement('div');
externalHeader.classList.add('flex-container', 'alignitemscenter', 'spaceBetween', 'flexnowrap', 'marginBot10');
const externalHeading = document.createElement('h3');
externalHeading.classList.add('margin0');
externalHeading.textContent = t`Installed Extensions:`;
const thirdPartyToolbar = document.createElement('div');
thirdPartyToolbar.classList.add('flex-container', 'third_party_toolbar');
externalHeader.append(externalHeading, thirdPartyToolbar);
externalContainer.appendChild(externalHeader);
htmlExternal.append(htmlLoading);
const loadingEl = document.createElement('div');
loadingEl.classList.add('flex-container', 'alignItemsCenter', 'justifyCenter', 'marginTop10', 'marginBot5');
const loadingIcon = document.createElement('i');
loadingIcon.classList.add('fa-solid', 'fa-spinner', 'fa-spin');
const loadingSpan = document.createElement('span');
loadingSpan.textContent = t`Loading third-party extensions... Please wait...`;
loadingEl.append(loadingIcon, loadingSpan);
externalContainer.appendChild(loadingEl);
const sortOrderKey = 'extensions_sortByName';
const sortByName = accountStorage.getItem(sortOrderKey) === 'true';
@@ -1006,16 +1170,16 @@ async function showExtensionsDetails() {
let extensionsToToggle = [];
extensions.forEach(value => {
const { isExternal, extensionHtml } = value;
const container = isExternal ? htmlExternal : htmlDefault;
container.append(extensionHtml);
const { isExternal, extensionElement } = value;
const container = isExternal ? externalContainer : defaultContainer;
container.appendChild(extensionElement);
});
const html = $('<div></div>')
const extensionsMenu = $('<div></div>')
.addClass('extensions_info')
.append(htmlErrors)
.append(htmlDefault)
.append(htmlExternal)
.append(errors)
.append(defaultContainer)
.append(externalContainer)
.append(getModuleInformation());
{
@@ -1041,23 +1205,24 @@ async function showExtensionsDetails() {
const toggleAllExtensionsButton = document.createElement('div');
toggleAllExtensionsButton.classList.add('menu_button', 'menu_button_icon');
toggleAllExtensionsButton.title = t`Bulk toggle third-party extensions.`;
toggleAllExtensionsButton.innerHTML = `
<span>${t`Toggle extensions`}</span>
<div class="fa-solid fa-circle-info opacity50p"></div>
`;
const toggleAllLabel = document.createElement('span');
toggleAllLabel.textContent = t`Toggle extensions`;
const toggleAllIcon = document.createElement('div');
toggleAllIcon.classList.add('fa-solid', 'fa-circle-info', 'opacity50p');
toggleAllExtensionsButton.append(toggleAllLabel, toggleAllIcon);
const restoreBulkToggledExtensionsButton = document.createElement('div');
restoreBulkToggledExtensionsButton.classList.add('menu_button', 'menu_button_icon', 'fa-solid', 'fa-arrow-right-rotate', 'displayNone');
restoreBulkToggledExtensionsButton.title = t`Restore toggled extensions.\n\nIt does not restore extensions toggled individually.`;
toggleAllExtensionsButton.addEventListener('click', () => {
extensionsToToggle = onToggleAllExtensions(extensionsToToggle, htmlExternal);
extensionsToToggle = onToggleAllExtensions(extensionsToToggle, $(externalContainer));
for (const extension of extensionsToToggle) {
const { name } = extension;
htmlExternal
.find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
$(externalContainer)
.find(`.extension_block[data-name="${getNameSelector(name)}"] .extension_toggle input`)
.off('click')
.one('click', () => {
extensionsToToggle = extensionsToToggle.filter(ext => ext.name !== name);
@@ -1074,8 +1239,8 @@ async function showExtensionsDetails() {
const { name } = extension;
const isDisabled = extension_settings.disabledExtensions.includes(name);
htmlExternal
.find(`.extension_block[data-name="${name.replace('third-party', '')}"] .extension_toggle input`)
$(externalContainer)
.find(`.extension_block[data-name="${getNameSelector(name)}"] .extension_toggle input`)
.prop('checked', !isDisabled)
.toggleClass('toggle_enable', isDisabled)
.toggleClass('toggle_disable', !isDisabled)
@@ -1099,13 +1264,13 @@ async function showExtensionsDetails() {
});
toolbar.append(updateAllButton, updateEnabledOnlyButton, flexExpander, sortOrderButton);
htmlExternal.find('.third_party_toolbar').append(restoreBulkToggledExtensionsButton, toggleAllExtensionsButton);
html.prepend(toolbar);
thirdPartyToolbar.append(restoreBulkToggledExtensionsButton, toggleAllExtensionsButton);
extensionsMenu.prepend(toolbar);
}
let waitingForSave = false;
const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
const popup = new Popup(extensionsMenu, POPUP_TYPE.TEXT, '', {
okButton: t`Close`,
wide: true,
large: true,
@@ -1147,7 +1312,7 @@ async function showExtensionsDetails() {
});
popupPromise = popup.show();
popup.content.scrollTop = initialScrollTop;
checkForUpdatesManual(sortFn, abortController.signal).finally(() => htmlLoading.remove());
checkForUpdatesManual(sortFn, abortController.signal).finally(() => loadingEl.remove());
} catch (error) {
toastr.error(t`Error loading extensions. See browser console for details.`);
console.error(error);
@@ -1234,6 +1399,7 @@ async function updateExtension(extensionName, quiet, timeout = null) {
* This function makes a POST request to '/api/extensions/delete' with the extension's name.
* If the extension is deleted, it displays a success message.
* Creates a popup for the user to confirm before delete.
* If the extension has a 'clean' hook, an optional checkbox to also run the cleanup is shown.
*/
async function onDeleteClick() {
const extensionName = $(this).data('name');
@@ -1244,13 +1410,50 @@ async function onDeleteClick() {
return;
}
// use callPopup to create a popup for the user to confirm before delete
const confirmation = await callGenericPopup(t`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {});
const hasCleanHook = hasExtensionHook(extensionName, 'clean');
/** @type {import('./popup.js').CustomPopupInput[]} */
const customInputs = hasCleanHook ? [{ id: 'extension_delete_cleanup', label: t`Also clean up extension data`, defaultState: false }] : null;
const popup = new Popup(t`Are you sure you want to delete ${escapeHtml(extensionName)}?`, POPUP_TYPE.CONFIRM, '', { customInputs });
const confirmation = await popup.show();
if (confirmation === POPUP_RESULT.AFFIRMATIVE) {
await deleteExtension(extensionName);
const shouldClean = hasCleanHook && Boolean(popup.inputResults?.get('extension_delete_cleanup'));
await deleteExtension(extensionName, shouldClean);
}
}
/**
* Handles the click event for the clean button of an extension.
* Runs the extension's 'clean' hook after user confirmation, then reloads the page.
*/
async function onCleanClick() {
const extensionName = $(this).data('name');
const confirmation = await Popup.show.confirm(t`Clean extension data`, t`Are you sure you want to clean up data for ${escapeHtml(extensionName)}? This action cannot be undone.`);
if (!confirmation) {
return;
}
await cleanExtension(extensionName);
}
/**
* Runs the 'clean' hook for an extension and reloads the page.
* @param {string} extensionName Extension name (without 'third-party' prefix)
* @returns {Promise<void>}
*/
async function cleanExtension(extensionName) {
const fullExtensionName = extensionName.startsWith('third-party') ? extensionName : `third-party${extensionName}`;
await callExtensionHook(fullExtensionName, 'clean');
// Clean might have updated settings, which could race with the page reload, so we'll force save here
await saveSettings();
toastr.success(t`Extension ${extensionName} data cleaned`);
delay(1000).then(() => location.reload());
}
async function onBranchClick() {
const extensionName = $(this).data('name');
const isCurrentUserAdmin = isAdmin();
@@ -1303,8 +1506,8 @@ async function onMoveClick() {
const confirmationHeader = t`Move extension`;
const confirmationText = source == 'global'
? t`Are you sure you want to move ${extensionName} to your local extensions? This will make it available only for you.`
: t`Are you sure you want to move ${extensionName} to the global extensions? This will make it available for all users.`;
? t`Are you sure you want to move ${escapeHtml(extensionName)} to your local extensions? This will make it available only for you.`
: t`Are you sure you want to move ${escapeHtml(extensionName)} to the global extensions? This will make it available for all users.`;
const confirmation = await Popup.show.confirm(confirmationHeader, confirmationText);
@@ -1353,9 +1556,16 @@ async function moveExtension(extensionName, source, destination) {
/**
* Deletes an extension via the API.
* @param {string} extensionName Extension name to delete
* @param {boolean} [shouldClean=false] Whether to also run the 'clean' hook before deleting
*/
export async function deleteExtension(extensionName) {
await callExtensionHook(extensionName, 'delete');
export async function deleteExtension(extensionName, shouldClean = false) {
const fullExtensionName = extensionName.startsWith('third-party') ? extensionName : `third-party${extensionName}`;
if (shouldClean) {
await callExtensionHook(fullExtensionName, 'clean');
}
await callExtensionHook(fullExtensionName, 'delete');
try {
await fetch('/api/extensions/delete', {
@@ -1370,6 +1580,9 @@ export async function deleteExtension(extensionName) {
console.error('Error:', error);
}
// Delete or clean might have updated settings, which could race with the page reload, so we'll force save here
await saveSettings();
toastr.success(t`Extension ${extensionName} deleted`);
delay(1000).then(() => location.reload());
}
@@ -1398,6 +1611,9 @@ async function getExtensionVersion(extensionName, abortSignal) {
const data = await response.json();
return data;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return;
}
console.error('Error:', error);
}
}
@@ -1476,9 +1692,53 @@ async function switchExtensionBranch(extensionName, isGlobal, branch) {
* Installs a third-party extension via the API.
* @param {string} url Extension repository URL
* @param {boolean} global Is the extension global?
* @returns {Promise<void>}
* @param {string} [branch] Optional branch to install, if not provided the default branch will be used
* @returns {Promise<boolean>} True if the extension was installed successfully, false otherwise
*/
export async function installExtension(url, global, branch = '') {
try {
const parsedUrl = new URL(url);
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
throw new Error('Invalid URL protocol');
}
// Normalize the URL (resolve relative paths, remove redundant segments, etc.)
url = parsedUrl.href;
} catch (error) {
console.error('Invalid URL:', error);
toastr.error(t`Only valid HTTP and HTTPS URLs are allowed.`, t`Invalid URL`);
return false;
}
if (!isOfficialExtension(url)) {
const extensionInstallationWarningKey = 'extensionInstallationWarningShown';
if (accountStorage.getItem(extensionInstallationWarningKey)) {
console.debug('Bypassed URL check for third-party extension (account preference).', url);
} else {
let dismissWarning = false;
const confirmation = await Popup.show.confirm(
t`Install a third-party extension?`,
await renderTemplateAsync('thirdPartyExtensionWarning'),
{
customInputs: [{ id: 'dontAskAgain', type: 'checkbox', label: t`Don't show this warning again`, defaultState: false }],
onClose: (popup) => {
if (!popup.result) {
return;
}
dismissWarning = Boolean(popup.inputResults?.get('dontAskAgain') ?? false);
},
okButton: t`Yes, install it`,
cancelButton: t`No, cancel`,
});
if (!confirmation) {
return false;
}
if (dismissWarning) {
accountStorage.setItem(extensionInstallationWarningKey, '1');
}
}
}
console.debug('Extension installation started', url);
toastr.info(t`Please wait...`, t`Installing extension`);
@@ -1497,11 +1757,11 @@ export async function installExtension(url, global, branch = '') {
const text = await request.text();
toastr.warning(text || request.statusText, t`Extension installation failed`, { timeOut: 5000 });
console.error('Extension installation failed', request.status, request.statusText, text);
return;
return false;
}
const response = await request.json();
toastr.success(t`Extension '${response.display_name}' by ${response.author} (version ${response.version}) has been installed successfully!`, t`Extension installation successful`);
toastr.success(t`Extension '${response.display_name}' has been installed successfully!`, t`Extension installation successful`);
console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
await loadExtensionSettings({}, false, false);
await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED, response);
@@ -1510,6 +1770,8 @@ export async function installExtension(url, global, branch = '') {
const extensionName = `third-party/${response.folderName}`;
await callExtensionHook(extensionName, 'install');
}
return true;
}
/**
@@ -1589,7 +1851,11 @@ async function checkForUpdatesManual(sortFn, abortSignal) {
const promise = enqueueVersionCheck(async () => {
try {
const data = await getExtensionVersion(externalId, abortSignal);
const extensionBlock = document.querySelector(`.extension_block[data-name="${externalId}"]`);
if (!data) {
return;
}
const selector = getNameSelector(externalId, { prefix: '' });
const extensionBlock = document.querySelector(`.extension_block[data-name="${selector}"]`);
if (extensionBlock && data) {
if (data.isUpToDate === false) {
const buttonElement = extensionBlock.querySelector('.btn_update');
@@ -1620,6 +1886,18 @@ async function checkForUpdatesManual(sortFn, abortSignal) {
}
}
const authorElement = extensionBlock.querySelector('.extension_author');
if (authorElement) {
const author = getAuthorFromUrl(origin) || EMPTY_AUTHOR;
if (author.name) {
const icon = document.createElement('i');
icon.classList.add('fa-solid', 'fa-at', 'fa-xs');
const name = document.createElement('span');
name.textContent = author.name;
authorElement.append(icon, name);
}
}
const versionElement = extensionBlock.querySelector('.extension_version');
if (versionElement) {
versionElement.textContent += ` (${branch}-${commitHash.substring(0, 7)})`;
@@ -1672,6 +1950,9 @@ async function checkForExtensionUpdates(force) {
const promise = enqueueVersionCheck(async () => {
try {
const data = await getExtensionVersion(id.replace('third-party', ''));
if (!data) {
return;
}
if (!data.isUpToDate) {
updatesAvailable.push(manifest.display_name);
}
@@ -1758,6 +2039,18 @@ export async function runGenerationInterceptors(chat, contextSize, type) {
return aborted;
}
/**
* Sentinel value that signals a field should be completely removed (unset)
* from the character card rather than being set to any value. Pass this as
* the `value` argument to {@link writeExtensionField} or
* {@link writeExtensionFieldBulk} to delete the key entirely.
*
* Using `null` as a value will set the field to `null` (the key remains).
* Using this sentinel will delete the key from the character card.
* @type {string}
*/
export const UNSET_VALUE = '__@@UNSET@@__';
/**
* Writes a field to the character's data extensions object.
* @param {number|string} characterId Index in the character array
@@ -1772,13 +2065,23 @@ export async function writeExtensionField(characterId, key, value) {
console.warn('Character not found', characterId);
return;
}
const path = `data.extensions.${key}`;
setValueByPath(character, path, value);
const extensionPath = `data.extensions.${key}`;
const isUnset = value === UNSET_VALUE;
if (isUnset) {
deleteValueByPath(character, extensionPath);
} else {
setValueByPath(character, extensionPath, value);
}
// Process JSON data
if (character.json_data) {
const jsonData = JSON.parse(character.json_data);
setValueByPath(jsonData, path, value);
if (isUnset) {
deleteValueByPath(jsonData, extensionPath);
} else {
setValueByPath(jsonData, extensionPath, value);
}
character.json_data = JSON.stringify(jsonData);
// Make sure the data doesn't get lost when saving the current character
@@ -1807,6 +2110,107 @@ export async function writeExtensionField(characterId, key, value) {
}
}
/**
* @typedef {object} BulkExtensionFieldResult
* @property {string[]} updated Avatar filenames that were successfully updated
* @property {string[]} skipped Avatar filenames skipped (filter didn't match or unreadable)
* @property {string[]} failed Avatar filenames where the update failed
*/
/**
* Writes (or deletes) an extension field for multiple characters in a single
* bulk request. Unlike {@link writeExtensionField}, this sends one API call
* for all characters, and the server processes them in parallel.
*
* When `value` is {@link UNSET_VALUE} the extension key is **deleted** from
* each matching character card. Passing `null` sets the field to `null`
* (the key is preserved).
*
* @param {string[]|null} avatars Avatar filenames to update. Pass `null` or an
* empty array to target **all** characters in the user's character directory.
* @param {string} key Extension field name (e.g. "greeting_tools")
* @param {any} value Field value, `null` to set null, or
* {@link UNSET_VALUE} to delete the key entirely
* @param {object} [options={}] Optional settings
* @param {string} [options.filterPath] Dot-path filter the server will only
* update characters where this path is present and not `undefined`;
* `null` still counts as a match. Useful when the frontend has shallow
* character data and cannot pre-filter.
* Defaults to `data.extensions.<key>` when unsetting, so deletion requests
* automatically skip characters where the field is missing/`undefined`.
* @returns {Promise<BulkExtensionFieldResult>} Summary of the bulk operation
*/
export async function writeExtensionFieldBulk(avatars, key, value, { filterPath } = {}) {
const context = getContext();
const extensionPath = `data.extensions.${key}`;
const isUnset = value === UNSET_VALUE;
// Build the server request
const requestBody = {
avatars: Array.isArray(avatars) && avatars.length > 0 ? avatars : [],
data: {
data: {
extensions: {
[key]: value,
},
},
},
};
// Default filter: when unsetting, only touch characters that have the field
const resolvedFilterPath = filterPath ?? (isUnset ? extensionPath : undefined);
if (resolvedFilterPath) {
requestBody.filter = { path: resolvedFilterPath };
}
const mergeResponse = await fetch('/api/characters/merge-attributes', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(requestBody),
});
if (!mergeResponse.ok) {
console.error('Bulk extension field update failed', mergeResponse.statusText);
return { updated: [], skipped: [], failed: [] };
}
/** @type {BulkExtensionFieldResult} */
const result = await mergeResponse.json();
// Sync in-memory character objects for successfully updated characters
const updatedSet = new Set(result.updated);
for (const character of context.characters) {
if (!character || !updatedSet.has(character.avatar)) continue;
if (isUnset) {
deleteValueByPath(character, extensionPath);
} else {
setValueByPath(character, extensionPath, value);
}
// Keep json_data in sync
if (character.json_data) {
const jsonData = JSON.parse(character.json_data);
if (isUnset) {
deleteValueByPath(jsonData, extensionPath);
} else {
setValueByPath(jsonData, extensionPath, value);
}
character.json_data = JSON.stringify(jsonData);
}
}
// If the currently active character was updated, sync the hidden input
if (context.characterId !== undefined) {
const activeChar = context.characters[context.characterId];
if (activeChar && updatedSet.has(activeChar.avatar) && activeChar.json_data) {
$('#character_json_data').val(activeChar.json_data);
}
}
return result;
}
/**
* Prompts the user to enter the Git URL of the extension to import.
* After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
@@ -1853,6 +2257,39 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
await installExtension(url, global, branchName);
}
/**
* Sentinel value representing an empty author, used when author information cannot be extracted from a URL.
* @type {{name: string, url: string}}
*/
export const EMPTY_AUTHOR = Object.freeze({
name: '',
url: '',
});
/**
* Extracts the repository author from a given URL.
* @param {string} url - The URL of the repository.
* @returns {{name: string, url: string}} Object containing the author's name and URL, or empty strings if not found.
*/
export function getAuthorFromUrl(url) {
const result = structuredClone(EMPTY_AUTHOR);
try {
const parsedUrl = new URL(url);
const pathSegments = parsedUrl.pathname.split('/').filter(s => s.length > 0);
// TODO: Handle non-GitHub URLs if needed
if (parsedUrl.host === 'github.com' && pathSegments.length >= 2) {
result.name = pathSegments[0];
result.url = `${parsedUrl.protocol}//${parsedUrl.hostname}/${result.name}`;
}
} catch (error) {
console.debug('Error parsing URL:', error);
}
return result;
}
export async function initExtensions() {
await addExtensionsButtonAndMenu();
$('#extensionsMenuButton').css('display', 'flex');
@@ -1865,6 +2302,7 @@ export async function initExtensions() {
$(document).on('click', '.extensions_info .extension_block .toggle_enable', onEnableExtensionClick);
$(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick);
$(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick);
$(document).on('click', '.extensions_info .extension_block .btn_clean', onCleanClick);
$(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick);
$(document).on('click', '.extensions_info .extension_block .btn_branch', onBranchClick);
+226 -127
View File
@@ -5,12 +5,12 @@ TODO:
import { DOMPurify } from '../../../lib.js';
import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from '../../../script.js';
import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
import { deleteExtension, EMPTY_AUTHOR, extensionNames, getAuthorFromUrl, getContext, installExtension, renderExtensionTemplateAsync, isOfficialExtension } from '../../extensions.js';
import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
import { executeSlashCommandsWithOptions } from '../../slash-commands.js';
import { accountStorage } from '../../util/AccountStorage.js';
import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
import { escapeHtml, flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
import { t, translate } from '../../i18n.js';
import { SlashCommandParser } from '/scripts/slash-commands/SlashCommandParser.js';
export { MODULE_NAME };
const MODULE_NAME = 'assets';
@@ -60,100 +60,34 @@ const KNOWN_TYPES = {
'blip': t`Blip sounds`,
};
const EMPTY_AUTHOR = {
name: '',
url: '',
};
/**
* Extracts the repository author from a given URL.
* @param {string} url - The URL of the repository.
* @returns {{name: string, url: string}} Object containing the author's name and URL, or empty strings if not found.
* Creates the download/delete button element for a single asset, with all interaction handlers attached.
* @param {object} asset The asset data object, containing at least id, name, description and url fields
* @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip'
* @param {number} index Index of the asset in the list of available assets of the same type, used to create a unique element ID
* @returns {JQuery} The button element
*/
function getAuthorFromUrl(url) {
const result = structuredClone(EMPTY_AUTHOR);
try {
const parsedUrl = new URL(url);
const pathSegments = parsedUrl.pathname.split('/').filter(s => s.length > 0);
// TODO: Handle non-GitHub URLs if needed
if (parsedUrl.host === 'github.com' && pathSegments.length >= 2) {
result.name = pathSegments[0];
result.url = `${parsedUrl.protocol}//${parsedUrl.hostname}/${result.name}`;
}
} catch (error) {
console.debug(DEBUG_PREFIX, 'Error parsing URL:', error);
}
return result;
}
async function downloadAssetsList(url) {
updateCurrentAssets().then(async function () {
fetch(url, { cache: 'no-cache' })
.then(response => response.json())
.then(async function (json) {
availableAssets = {};
$('#assets_menu').empty();
console.debug(DEBUG_PREFIX, 'Received assets dictionary', json);
for (const i of json) {
//console.log(DEBUG_PREFIX,i)
if (availableAssets[i.type] === undefined)
availableAssets[i.type] = [];
availableAssets[i.type].push(i);
}
console.debug(DEBUG_PREFIX, 'Updated available assets to', availableAssets);
// First extensions, then everything else
const assetTypes = Object.keys(availableAssets).sort((a, b) => (a === 'extension') ? -1 : (b === 'extension') ? 1 : 0);
$('#assets_type_select').empty();
$('#assets_search').val('');
$('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
for (const type of assetTypes) {
const text = translate(KNOWN_TYPES[type] || type);
const option = $('<option />', { value: type, text: text });
$('#assets_type_select').append(option);
}
if (assetTypes.includes('extension')) {
$('#assets_type_select').val('extension');
}
$('#assets_type_select').off('change').on('change', filterAssets);
$('#assets_search').off('input').on('input', filterAssets);
for (const assetType of assetTypes) {
let assetTypeMenu = $('<div />', { id: 'assets_audio_ambient_div', class: 'assets-list-div' });
assetTypeMenu.attr('data-type', assetType);
assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
if (assetType == 'extension') {
assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
}
for (const asset of availableAssets[assetType].sort((a, b) => a?.name && b?.name && a.name.localeCompare(b.name))) {
const i = availableAssets[assetType].indexOf(asset);
const elemId = `assets_install_${assetType}_${i}`;
let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
function createAssetButton(asset, assetType, index) {
const elemId = `assets_install_${assetType}_${index}`;
const element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
const label = $('<i class="fa-fw fa-solid fa-download fa-lg"></i>');
element.append(label);
//if (DEBUG_TONY_SAMA_FORK_MODE)
// asset["url"] = asset["url"].replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG
console.debug(DEBUG_PREFIX, 'Checking asset', asset.id, asset.url);
const assetInstall = async function () {
element.off('click');
label.removeClass('fa-download');
this.classList.add('asset-download-button-loading');
await installAsset(asset.url, assetType, asset.id);
const result = await installAsset(asset.url, assetType, asset.id);
if (!result) {
this.classList.remove('asset-download-button-loading');
label.addClass('fa-download');
label.removeClass('fa-spinner');
label.removeClass('fa-spin');
element.on('click', assetInstall);
return;
}
label.addClass('fa-check');
this.classList.remove('asset-download-button-loading');
element.on('click', assetDelete);
@@ -171,7 +105,7 @@ async function downloadAssetsList(url) {
const assetDelete = async function () {
if (assetType === 'character') {
toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');
await executeSlashCommandsWithOptions(`/go ${asset.id}`);
await SlashCommandParser.commands.go.callback(null, asset.id);
return;
}
element.off('click');
@@ -204,6 +138,17 @@ async function downloadAssetsList(url) {
element.on('click', assetInstall);
}
return element;
}
/**
* Creates the full visual block element for a single asset.
* @param {object} asset The asset data object, containing at least id, name, description and url fields
* @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip'
* @param {JQuery} element The button element from createAssetButton
* @returns {JQuery} The asset block element
*/
function createAssetBlock(asset, assetType, element) {
console.debug(DEBUG_PREFIX, 'Created element for ', asset.id);
const displayName = DOMPurify.sanitize(asset.name || asset.id);
@@ -214,23 +159,31 @@ async function downloadAssetsList(url) {
const toolTag = assetType === 'extension' && asset.tool;
const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR;
const assetBlock = $('<i></i>')
.append(element)
.append(`<div class="flex-container flexFlowColumn flexNoGap wide100p overflowHidden">
<span class="asset-name flex-container alignitemscenter">
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>` +
(toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' +
t`Tool` + '</span>' : '') +
'<span class="expander"></span>' +
(author.name ? `<a href="${author.url}" target="_blank" class="asset-author-info"><i class="fa-solid fa-at fa-xs"></i><span>${author.name}</span></a>` : '') +
`</span>
<small class="asset-description">
${description}
</small>
</div>`);
const nameSpan = $('<span>', { class: 'asset-name flex-container alignitemscenter' })
.append($('<b>').text(displayName))
.append($('<a>', { class: 'asset_preview', href: url, target: '_blank', title: title })
.append($('<i>', { class: `fa-solid fa-sm ${previewIcon}` })));
if (toolTag) {
const tagSpan = $('<span>', { class: 'tag', title: t`Adds a function tool` })
.append($('<i>', { class: 'fa-solid fa-sm fa-wrench' }))
.append(document.createTextNode(` ${t`Tool`}`));
nameSpan.append(tagSpan);
}
nameSpan.append($('<span>', { class: 'expander' }));
if (author.name) {
nameSpan.append($('<a>', { href: author.url, target: '_blank', class: 'asset-author-info' })
.append($('<i>', { class: 'fa-solid fa-at fa-xs' }))
.append($('<span>').text(author.name)));
}
const infoDiv = $('<div>', { class: 'flex-container flexFlowColumn flexNoGap wide100p overflowHidden' })
.append(nameSpan)
.append($('<small>', { class: 'asset-description' }).text(description));
const assetBlock = $('<i></i>').append(element).append(infoDiv);
assetBlock.find('.tag').on('click', function (e) {
const a = document.createElement('a');
@@ -241,38 +194,128 @@ async function downloadAssetsList(url) {
if (assetType === 'character') {
if (asset.highlight) {
assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
nameSpan.append($('<i>', { class: 'fa-solid fa-sm fa-trophy' }));
}
assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset.url}" alt="${displayName}"></div>`);
nameSpan.prepend($('<div>', { class: 'avatar' }).append($('<img>', { src: asset.url, alt: displayName })));
}
assetBlock.addClass('asset-block');
return assetBlock;
}
/**
* Builds and appends the menu section for a single asset type.
* @param {string} assetType Asset type, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip'
* @returns {Promise<void>}
*/
async function buildAssetTypeSection(assetType) {
const assetTypeMenu = $('<div />', { id: `assets_${assetType}_div`, class: 'assets-list-div' });
assetTypeMenu.attr('data-type', assetType);
assetTypeMenu.append($('<h3>').text(KNOWN_TYPES[assetType] || assetType)).hide();
if (assetType == 'extension') {
assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
}
for (const asset of availableAssets[assetType].sort((a, b) => a?.name && b?.name && a.name.localeCompare(b.name))) {
const i = availableAssets[assetType].indexOf(asset);
const element = createAssetButton(asset, assetType, i);
const assetBlock = createAssetBlock(asset, assetType, element);
if (assetType === 'extension') {
const extensionBlockList = isOfficialExtension(asset.url)
? assetTypeMenu.find('.assets-list-extensions-official .assets-list-extensions')
: assetTypeMenu.find('.assets-list-extensions-community .assets-list-extensions');
extensionBlockList.append(assetBlock);
} else {
assetTypeMenu.append(assetBlock);
}
}
assetTypeMenu.appendTo('#assets_menu');
assetTypeMenu.on('click', 'a.asset_preview', previewAsset);
}
/**
* Parses the fetched assets JSON and renders the full assets menu.
* @param {object[]} json Array of asset objects, each containing at least id, name, description, url and type fields
*/
async function populateAssetsMenu(json) {
availableAssets = {};
$('#assets_menu').empty();
console.debug(DEBUG_PREFIX, 'Received assets dictionary', json);
for (const i of json) {
if (availableAssets[i.type] === undefined)
availableAssets[i.type] = [];
availableAssets[i.type].push(i);
}
console.debug(DEBUG_PREFIX, 'Updated available assets to', availableAssets);
// First extensions, then everything else
const assetTypes = Object.keys(availableAssets).sort((a, b) => (a === 'extension') ? -1 : (b === 'extension') ? 1 : 0);
$('#assets_type_select').empty();
$('#assets_search').val('');
$('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
for (const type of assetTypes) {
const text = translate(KNOWN_TYPES[type] || type);
const option = $('<option />', { value: type, text: text });
$('#assets_type_select').append(option);
}
if (assetTypes.includes('extension')) {
$('#assets_type_select').val('extension');
}
$('#assets_type_select').off('change').on('change', filterAssets);
$('#assets_search').off('input').on('input', filterAssets);
for (const assetType of assetTypes) {
await buildAssetTypeSection(assetType);
}
filterAssets();
$('#assets_filters').show();
$('#assets_menu').show();
})
.catch((error) => {
// Info hint if the user maybe... likely accidently was trying to install an extension and we wanna help guide them? uwu :3
}
/**
* Downloads the assets list from the given URL and populates the menu. Shows error message if something goes wrong.
* @param {URL} url URL to fetch from
*/
async function downloadAssetsList(url) {
await updateCurrentAssets();
try {
const response = await fetch(url, { cache: 'no-cache' });
if (!response.ok) {
throw new Error('Cannot download the assets list.');
}
const json = await response.json();
if (!Array.isArray(json)) {
throw new Error('Assets list is not an array');
}
await populateAssetsMenu(json);
} catch (error) {
// Info hint if the user maybe... likely accidentally was trying to install an extension and we wanna help guide them? uwu :3
const installButton = $('#third_party_extension_button');
flashHighlight(installButton, 10_000);
toastr.info('Click the flashing button at the top right corner of the menu.', 'Trying to install a custom extension?', { timeOut: 10_000 });
// Error logged after, to appear on top
console.error(error);
toastr.error('Problem with assets URL', DEBUG_PREFIX + 'Cannot get assets list');
toastr.error('Problem with assets URL', 'Cannot get assets list');
$('#assets-connect-button').addClass('fa-plug-circle-exclamation');
$('#assets-connect-button').addClass('redOverlayGlow');
});
});
}
}
/**
* Previews the asset by opening its URL. If it's an audio asset, it plays a preview sound. Otherwise, it opens the URL in a new tab.
* @param {JQuery.Event} e Click event
*/
function previewAsset(e) {
const href = $(this).attr('href');
const audioExtensions = ['.mp3', '.ogg', '.wav'];
@@ -295,6 +338,15 @@ function previewAsset(e) {
}
}
/**
* Checks if the asset is already installed.
* For extensions, it checks if the extension name is in the list of installed extensions.
* For characters, it checks if any character has the same avatar URL.
* For other asset types, it checks if any installed asset of the same type has a URL that includes the filename.
* @param {string} assetType Type of the asset, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip'
* @param {string} filename Name or ID of the asset
* @returns {boolean} True if the asset is installed, false otherwise
*/
function isAssetInstalled(assetType, filename) {
let assetList = currentAssets[assetType];
@@ -316,15 +368,22 @@ function isAssetInstalled(assetType, filename) {
return false;
}
/**
* Installs the asset by sending a request to the server to download it. If it's an extension, it uses the existing installExtension function.
* @param {string} url URL of the asset to download
* @param {string} assetType Type of the asset, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip'
* @param {string} filename Name or ID of the asset
* @returns {Promise<boolean>} True if the asset was successfully installed, false otherwise
*/
async function installAsset(url, assetType, filename) {
console.debug(DEBUG_PREFIX, 'Downloading ', url);
const category = assetType;
try {
if (category === 'extension') {
console.debug(DEBUG_PREFIX, 'Installing extension ', url);
await installExtension(url, false);
const result = await installExtension(url, false);
console.debug(DEBUG_PREFIX, 'Extension installed.');
return;
return result;
}
const body = { url, category, filename };
@@ -340,16 +399,25 @@ async function installAsset(url, assetType, filename) {
console.debug(DEBUG_PREFIX, 'Importing character ', filename);
const blob = await result.blob();
const file = new File([blob], filename, { type: blob.type });
await processDroppedFiles([file]);
const fileNameMap = new Map([[file, filename]]);
await processDroppedFiles([file], fileNameMap);
console.debug(DEBUG_PREFIX, 'Character downloaded.');
}
return true;
}
return false;
} catch (err) {
console.log(err);
return [];
return false;
}
}
/**
* Deletes the asset by sending a request to the server to delete it. If it's an extension, it uses the existing deleteExtension function.
* @param {string} assetType Type of the asset, e.g. 'extension', 'character', 'ambient', 'bgm', 'blip'
* @param {string} filename Name or ID of the asset
* @returns {Promise<boolean>} True if the asset was successfully deleted, false otherwise
*/
async function deleteAsset(assetType, filename) {
console.debug(DEBUG_PREFIX, 'Deleting ', assetType, filename);
const category = assetType;
@@ -358,6 +426,7 @@ async function deleteAsset(assetType, filename) {
console.debug(DEBUG_PREFIX, 'Deleting extension ', filename);
await deleteExtension(filename);
console.debug(DEBUG_PREFIX, 'Extension deleted.');
return true;
}
const body = { category, filename };
@@ -369,19 +438,37 @@ async function deleteAsset(assetType, filename) {
});
if (result.ok) {
console.debug(DEBUG_PREFIX, 'Deletion success.');
return true;
}
return false;
} catch (err) {
console.log(err);
return [];
return false;
}
}
/**
* Opens the character browser popup, which shows all available characters and allows downloading them.
* @param {boolean} forceDefault If true, it uses the default ASSETS_JSON_URL instead of the one from the input field.
* @returns {Promise<void>}
*/
async function openCharacterBrowser(forceDefault) {
const url = forceDefault ? ASSETS_JSON_URL : String($('#assets-json-url-field').val());
if (!isValidUrl(url)) {
toastr.error('Please enter a valid URL');
return;
}
const fetchResult = await fetch(url, { cache: 'no-cache' });
if (!fetchResult.ok) {
toastr.error('Cannot download the assets list.');
return;
}
const json = await fetchResult.json();
const characters = json.filter(x => x.type === 'character');
if (!Array.isArray(json)) {
toastr.error('Assets list is not an array');
return;
}
const characters = json.filter(x => x && x.type === 'character');
if (!characters.length) {
toastr.error('No characters found in the assets list', 'Character browser');
return;
@@ -398,12 +485,19 @@ async function openCharacterBrowser(forceDefault) {
downloadButton.toggle(!isInstalled).on('click', async () => {
downloadButton.toggleClass('fa-download fa-spinner fa-spin');
await installAsset(character.url, 'character', character.id);
const result = await installAsset(character.url, 'character', character.id);
if (result) {
downloadButton.hide();
checkMark.show();
} else {
downloadButton.toggleClass('fa-download fa-spinner fa-spin');
}
});
checkMark.toggle(isInstalled);
checkMark.toggle(isInstalled).on('click', async () => {
toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');
await SlashCommandParser.commands.go.callback(null, character.id);
});
listElement.append(characterElement);
}
@@ -435,7 +529,7 @@ async function updateCurrentAssets() {
//#############################//
// This function is called when the extension is loaded
jQuery(async () => {
export async function init() {
// This is an example of loading HTML from a file
const windowTemplate = await renderExtensionTemplateAsync(MODULE_NAME, 'window', {});
const windowHtml = $(windowTemplate);
@@ -457,11 +551,16 @@ jQuery(async () => {
const connectButton = windowHtml.find('#assets-connect-button');
connectButton.on('click', async function () {
const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
const urlString = String(assetsJsonUrl.val()).trim();
if (!isValidUrl(urlString)) {
toastr.error('Please enter a valid URL');
return;
}
const url = new URL(urlString);
const rememberKey = `Assets_SkipConfirm_${getStringHash(url.href)}`;
const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
const confirmation = skipConfirm || await Popup.show.confirm(t`Loading Asset List`, '<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${url}</var>`, {
const confirmation = skipConfirm || await Popup.show.confirm(t`Loading Asset List`, '<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${escapeHtml(url.href)}</var>`, {
customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
onClose: popup => {
if (popup.result) {
@@ -480,7 +579,7 @@ jQuery(async () => {
connectButton.addClass('fa-plug-circle-check');
} catch (error) {
console.error('Error:', error);
toastr.error(`Cannot get assets list from ${url}`);
toastr.error(`Cannot get assets list from ${url.href}`);
connectButton.removeClass('fa-plug-circle-check');
connectButton.addClass('fa-plug-circle-exclamation');
connectButton.removeClass('redOverlayGlow');
@@ -496,4 +595,4 @@ jQuery(async () => {
eventSource.on(event_types.OPEN_CHARACTER_LIBRARY, async (forceDefault) => {
openCharacterBrowser(forceDefault);
});
});
}
@@ -2,3 +2,21 @@
<span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br>
<span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span>
</div>
<div class="assets-list-extensions-official">
<h2 data-i18n="Official Extensions">Official Extensions</h2>
<div class="info-block hint">
<small class="assets-list-description" data-i18n="These extensions are maintained by the SillyTavern team.">
These extensions are maintained by the SillyTavern team.
</small>
</div>
<div class="assets-list-extensions"></div>
</div>
<div class="assets-list-extensions-community">
<h2 data-i18n="Community Extensions">Community Extensions</h2>
<div class="info-block warning">
<small data-i18n="Community extensions are not reviewed or verified by the SillyTavern team. Please exercise caution when installing.">
Community extensions are not reviewed or verified by the SillyTavern team. Please exercise caution when installing.
</small>
</div>
<div class="assets-list-extensions"></div>
</div>
@@ -7,5 +7,8 @@
"css": "style.css",
"author": "Keij#6799",
"version": "0.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+16 -3
View File
@@ -27,15 +27,20 @@
margin-bottom: 0.25em;
}
.assets-list-div h2 {
margin: 0;
font-size: 1.1em;
}
.assets-list-div h3 {
text-transform: capitalize;
}
.assets-list-div i a {
.assets-list-div .asset-block a {
color: inherit;
}
.assets-list-div>i {
.assets-list-div .asset-block {
display: flex;
flex-direction: row;
align-items: center;
@@ -46,7 +51,7 @@
border-bottom: 1px solid var(--SmartThemeBorderColor);
}
.assets-list-div i span:first-of-type {
.assets-list-div .asset-block span:first-of-type {
font-weight: bold;
}
@@ -198,3 +203,11 @@
.asset-name>b {
font-weight: 600;
}
div:is(.assets-list-extensions-official, .assets-list-extensions-community):has(.assets-list-extensions:empty) {
display: none;
}
div:is(.assets-list-extensions-official, .assets-list-extensions-community) {
margin-top: 10px;
}
@@ -243,7 +243,7 @@ function handleCharacterRename(oldAvatar, newAvatar) {
}
}
jQuery(async () => {
export async function init() {
eventSource.on(event_types.APP_READY, cleanUpAttachments);
eventSource.on(event_types.CHARACTER_DELETED, cleanUpCharacterAttachments);
eventSource.on(event_types.CHARACTER_RENAMED, handleCharacterRename);
@@ -407,4 +407,4 @@ jQuery(async () => {
}),
],
}));
});
}
@@ -7,5 +7,8 @@
"css": "style.css",
"author": "Cohee1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+12 -6
View File
@@ -1,8 +1,9 @@
import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { getMessageTimeStamp } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state } from '../../secrets.js';
import { oai_settings } from '../../openai.js';
import { getMultimodalCaption } from '../shared.js';
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -99,7 +100,7 @@ async function wrapCaptionTemplate(caption) {
template += ' {{caption}}';
}
let messageText = substituteParamsExtended(template, { caption: caption });
let messageText = substituteParams(template, { dynamicMacros: { caption: caption } });
if (extension_settings.caption.refine_mode) {
messageText = await Popup.show.input(
@@ -321,6 +322,8 @@ async function captionMultimodal(base64Img, externalPrompt) {
prompt = String(customPrompt).trim();
}
prompt = substituteParams(prompt);
const caption = await getMultimodalCaption(base64Img, prompt);
return { caption };
}
@@ -454,7 +457,7 @@ function isVideoCaptioningAvailable() {
return ['google', 'vertexai', 'zai'].includes(extension_settings.caption.multimodal_api);
}
jQuery(async function () {
export async function init() {
function addSendPictureButton() {
const sendButton = $(`
<div id="send_picture" class="list-group-item flex-container flexGap5">
@@ -504,6 +507,7 @@ jQuery(async function () {
'chutes': SECRET_KEYS.CHUTES,
'electronhub': SECRET_KEYS.ELECTRONHUB,
'pollinations': SECRET_KEYS.POLLINATIONS,
'workers_ai': SECRET_KEYS.WORKERS_AI,
};
if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -580,7 +584,7 @@ jQuery(async function () {
}
async function addRemoteEndpointModels() {
async function processEndpoint(api, url) {
async function processEndpoint(api, url, additionalParams = {}) {
const dropdown = document.getElementById('caption_multimodal_model');
if (!(dropdown instanceof HTMLSelectElement)) {
return;
@@ -591,7 +595,8 @@ jQuery(async function () {
const options = Array.from(dropdown.options);
const response = await fetch(url, {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),
headers: getRequestHeaders(),
body: JSON.stringify(additionalParams),
});
if (!response.ok) {
return;
@@ -620,6 +625,7 @@ jQuery(async function () {
await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');
await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');
await processEndpoint('moonshot', '/api/backends/chat-completions/multimodal-models/moonshot');
await processEndpoint('workers_ai', '/api/backends/chat-completions/multimodal-models/workers_ai', { workers_ai_account_id: oai_settings.workers_ai_account_id });
}
await addSettings();
@@ -804,4 +810,4 @@ jQuery(async function () {
}));
document.body.classList.add('caption');
});
}
@@ -9,5 +9,8 @@
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
@@ -20,6 +20,7 @@
<option value="aimlapi">AI/ML API</option>
<option value="chutes">Chutes</option>
<option value="anthropic">Claude</option>
<option value="workers_ai">Cloudflare Workers AI</option>
<option value="cohere">Cohere</option>
<option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
<option value="electronhub">Electron Hub</option>
@@ -53,7 +54,14 @@
<option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option>
<option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option>
<option data-type="cohere" value="command-a-vision-07-2025">command-a-vision-07-2025</option>
<option data-type="openai" value="gpt-5.5">gpt-5.5</option>
<option data-type="openai" value="gpt-5.5-2026-04-23">gpt-5.5-2026-04-23</option>
<option data-type="openai" value="gpt-5.4">gpt-5.4</option>
<option data-type="openai" value="gpt-5.4-2026-03-05">gpt-5.4-2026-03-05</option>
<option data-type="openai" value="gpt-5.4-mini">gpt-5.4-mini</option>
<option data-type="openai" value="gpt-5.4-mini-2026-03-17">gpt-5.4-mini-2026-03-17</option>
<option data-type="openai" value="gpt-5.4-nano">gpt-5.4-nano</option>
<option data-type="openai" value="gpt-5.4-nano-2026-03-17">gpt-5.4-nano-2026-03-17</option>
<option data-type="openai" value="gpt-5.3-chat-latest">gpt-5.3-chat-latest</option>
<option data-type="openai" value="gpt-5.2">gpt-5.2</option>
<option data-type="openai" value="gpt-5.2-2025-12-11">gpt-5.2-2025-12-11</option>
@@ -88,6 +96,7 @@
<option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>
<option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>
<option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
<option data-type="anthropic" value="claude-opus-4-7">claude-opus-4-7</option>
<option data-type="anthropic" value="claude-opus-4-6">claude-opus-4-6</option>
<option data-type="anthropic" value="claude-opus-4-5">claude-opus-4-5</option>
<option data-type="anthropic" value="claude-opus-4-5-20251101">claude-opus-4-5-20251101</option>
@@ -144,6 +153,11 @@
<option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
<option data-type="google" value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option>
<option data-type="google" value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option>
<option data-type="google" value="gemma-4-31b-it">gemma-4-31b-it</option>
<option data-type="google" value="gemma-4-26b-a4b-it">gemma-4-26b-a4b-it</option>
<option data-type="google" value="gemma-3-27b-it">gemma-3-27b-it</option>
<option data-type="google" value="gemma-3-12b-it">gemma-3-12b-it</option>
<option data-type="google" value="gemma-3-4b-it">gemma-3-4b-it</option>
<option data-type="vertexai" value="gemini-3.1-pro-preview">gemini-3.1-pro-preview</option>
<option data-type="vertexai" value="gemini-3.1-flash-lite-preview">gemini-3.1-flash-lite-preview</option>
<option data-type="vertexai" value="gemini-3.1-flash-image-preview">gemini-3.1-flash-image-preview</option>
@@ -174,6 +188,7 @@
<option data-type="ollama" value="mistral-small3.2">mistral-small3.2</option>
<option data-type="ollama" value="llama3.2-vision">llama3.2-vision</option>
<option data-type="ollama" value="llama4">llama4</option>
<option data-type="zai" value="glm-5v-turbo">glm-5v-turbo</option>
<option data-type="zai" value="glm-4.6v">glm-4.6v</option>
<option data-type="zai" value="glm-4.6v-flashx">glm-4.6v-flashx</option>
<option data-type="zai" value="glm-4.6v-flash">glm-4.6v-flash</option>
@@ -1,7 +1,7 @@
import { DOMPurify, Fuse } from '../../../lib.js';
import { event_types, eventSource, main_api, online_status, saveSettingsDebounced } from '../../../script.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { activateSendButtons, deactivateSendButtons, event_types, eventSource, main_api, online_status, saveSettingsDebounced } from '../../../script.js';
import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js';
@@ -9,11 +9,16 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandDebugController } from '../../slash-commands/SlashCommandDebugController.js';
import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommandScope } from '../../slash-commands/SlashCommandScope.js';
import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4, waitUntilCondition } from '../../utils.js';
import { collapseSpaces, getUniqueName, isFalseBoolean, isTrueBoolean, uuidv4, waitUntilCondition } from '../../utils.js';
import { t } from '../../i18n.js';
import { getSecretLabelById } from '../../secrets.js';
import { performFuzzySearch } from '/scripts/power-user.js';
import { StreamingDisplay } from '/scripts/streaming-display.js';
import { ConnectionManagerRequestService } from '../shared.js';
import { formatReasoning } from '/scripts/reasoning.js';
const MODULE_NAME = 'connection-manager';
const NONE = '<None>';
@@ -474,7 +479,223 @@ async function renderDetailsContent(detailsContent) {
}
}
(async function () {
/**
* Callback for the /profile-genstream command
* Generates text using Connection Manager with streaming display support.
* @param {object} args Named arguments
* @param {string} value Unnamed argument (the prompt)
* @returns {Promise<string>} The generated text, optionally with formatted reasoning
*/
async function generateStreamCallback(args, value) {
if (!value) {
console.warn('WARN: No argument provided for /profile-genstream command');
return '';
}
// Check if Connection Manager is available
const context = getContext();
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
toastr.error(t`Connection Manager is required for /profile-genstream. Use /gen or /genraw instead.`);
return '';
}
const profileIdOrName = args?.profile;
const includeReasoning = isTrueBoolean(args?.reasoning);
const systemPrompt = typeof args?.system == 'string' ? args.system : '';
const maxTokens = Number(args?.length ?? 2048) || 2048;
const lock = isTrueBoolean(args?.lock);
const generatingLabel = typeof args?.generating === 'string' ? args.generating : 'Generating...';
const completedLabel = typeof args?.completed === 'string' ? args.completed : 'Generated';
const enableStop = !isFalseBoolean(args?.stop);
const onStopClosure = args?.onStop instanceof SlashCommandClosure ? args.onStop : null;
const onCompleteClosure = args?.onComplete instanceof SlashCommandClosure ? args.onComplete : null;
// Parse delay: 'infinite' or negative = null (stay open), number = delay in ms
let completeDelay = 3000; // Default 3 seconds
if (args?.delay !== undefined) {
if (typeof args.delay === 'string' && args.delay.toLowerCase() === 'infinite') {
completeDelay = null; // Stay until user closes
} else {
const parsed = Number(args.delay);
if (!isNaN(parsed) && parsed >= 0) {
completeDelay = parsed;
} else if (!isNaN(parsed) && parsed < 0) {
completeDelay = null; // Negative = infinite
}
}
}
// Create abort controller for stop functionality (when stop is enabled)
const abortController = enableStop ? new AbortController() : null;
// Compose the stop handler: abort the request + optionally invoke user closure
const onStopHandler = enableStop ? async () => {
abortController.abort();
if (onStopClosure) {
try {
const localClosure = onStopClosure.getCopy();
localClosure.onProgress = () => { };
await localClosure.execute();
} catch (e) {
console.error('[GenStream] Error executing onStop closure', e);
}
}
} : null;
try {
if (lock) {
deactivateSendButtons();
}
// Determine which profile to use
// Use the currently selected profile if no profile specified
let effectiveProfileId = context.extensionSettings.connectionManager.selectedProfile;
const profiles = context.extensionSettings.connectionManager.profiles;
if (profileIdOrName) {
// Use try to find profile by id first, then fuse search
const profile = profiles.find(p => p.id === profileIdOrName);
if (profile) {
effectiveProfileId = profile.id;
} else {
const keys = [
{ name: 'name', weight: 10 },
];
const fuseResults = performFuzzySearch('profile', profiles, keys, profileIdOrName);
if (fuseResults.length > 0) {
effectiveProfileId = fuseResults[0].item.id;
} else {
toastr.warning(t`Connection profile not found: ${profileIdOrName}`);
return '';
}
}
}
if (!effectiveProfileId) {
toastr.error(t`No connection profile specified or selected. Use profile= argument or select a profile in Connection Manager.`);
return '';
}
// Create streaming display
const display = new StreamingDisplay();
display.show({
label: generatingLabel,
icon: ConnectionManagerRequestService.getProfileIcon(effectiveProfileId),
onStop: onStopHandler,
});
const messages = [
...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
{ role: 'user', content: value },
];
let finalText = '';
let finalReasoning = '';
/** Gets the final (if requested, formatted) text to return for this command @returns {string} */
function buildResultText() {
// Format output with reasoning if requested
if (includeReasoning && finalReasoning) {
const { formatted } = formatReasoning(finalReasoning, finalText);
return formatted;
}
return finalText;
}
try {
// Attempt streaming first
const streamResponse = await ConnectionManagerRequestService.sendRequest(
effectiveProfileId,
messages,
maxTokens,
{ extractData: true, includePreset: true, stream: true, signal: abortController?.signal ?? undefined },
);
if (typeof streamResponse === 'function') {
const generator = streamResponse();
for await (const chunk of generator) {
finalText = chunk.text;
finalReasoning = chunk.state?.reasoning || '';
display.updateReasoning(finalReasoning);
display.updateContent(finalText);
}
} else {
// Non-streaming fallback within the try block
const extracted = streamResponse;
finalText = extracted?.content || '';
finalReasoning = extracted?.reasoning || '';
if (finalReasoning) {
display.updateReasoning(finalReasoning);
}
display.updateContent(finalText);
}
} catch (error) {
// If the user clicked stop, don't retry — show stopped state and return empty
if (abortController?.signal?.aborted) {
display.markStopped({ label: `${generatingLabel} [Stopped]` });
return buildResultText();
}
console.warn('[Slash Commands] Streaming failed, falling back to non-streaming:', error);
display.hide({ instant: true });
// Retry with non-streaming
const response = await ConnectionManagerRequestService.sendRequest(
effectiveProfileId,
messages,
maxTokens,
{ extractData: true, includePreset: true, stream: false },
);
const extracted = /** @type {import('../../custom-request.js').ExtractedData} */ (response);
finalText = extracted?.content || '';
finalReasoning = extracted?.reasoning || '';
// Show quick non-streaming display
display.show({
label: generatingLabel,
icon: ConnectionManagerRequestService.getProfileIcon(effectiveProfileId),
});
if (finalReasoning) {
display.updateReasoning(finalReasoning);
}
display.updateContent(finalText);
}
// Mark as complete with delay (null = stay open until user closes)
display.complete({ label: completedLabel, delay: completeDelay });
// Invoke onComplete closure if provided
if (onCompleteClosure) {
try {
const localClosure = onCompleteClosure.getCopy();
localClosure.onProgress = () => { };
await localClosure.execute();
} catch (e) {
console.error('[GenStream] Error executing onComplete closure', e);
}
}
if (!finalText) {
toastr.warning(t`Generation returned empty result`);
return '';
}
return buildResultText();
} catch (err) {
console.error('Error on /genstream generation', err);
toastr.error(err.message, t`API Error`, { preventDuplicates: true });
return '';
} finally {
if (lock) {
activateSendButtons();
}
}
}
export async function init() {
extension_settings.connectionManager = extension_settings.connectionManager || structuredClone(DEFAULT_SETTINGS);
for (const key of Object.keys(DEFAULT_SETTINGS)) {
@@ -824,4 +1045,114 @@ async function renderDetailsContent(detailsContent) {
return JSON.stringify(profile);
},
}));
})();
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'profile-genstream',
callback: generateStreamCallback,
returns: t`generated text`,
namedArgumentList: [
new SlashCommandNamedArgument(
'lock', t`lock user input during generation`, [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', commonEnumProviders.boolean('onOff')(),
),
SlashCommandNamedArgument.fromProps({
name: 'profile',
description: t`connection profile ID to use for generation`,
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.connectionProfiles(),
}),
SlashCommandNamedArgument.fromProps({
name: 'reasoning',
description: t`include formatted reasoning in the output`,
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
enumProvider: commonEnumProviders.boolean('trueFalse'),
}),
SlashCommandNamedArgument.fromProps({
name: 'system',
description: t`system prompt at the start`,
typeList: [ARGUMENT_TYPE.STRING],
}),
SlashCommandNamedArgument.fromProps({
name: 'length',
description: t`API response length in tokens`,
typeList: [ARGUMENT_TYPE.NUMBER],
defaultValue: '2048',
}),
SlashCommandNamedArgument.fromProps({
name: 'generating',
description: t`label/title for the generation display`,
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'Generating...',
}),
SlashCommandNamedArgument.fromProps({
name: 'completed',
description: t`updated label/title for when generation completes`,
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'Generated',
}),
SlashCommandNamedArgument.fromProps({
name: 'delay',
description: t`auto-hide delay in ms after generation completes. Use "infinite" or negative to keep until manually closed`,
typeList: [ARGUMENT_TYPE.NUMBER],
defaultValue: '3000',
enumList: [
new SlashCommandEnumValue('infinite', 'Keep the streaming display open until manually closed', 'command', '♾️'),
new SlashCommandEnumValue('any delay in seconds', null, 'number', '⌚', () => true, input => input),
],
}),
SlashCommandNamedArgument.fromProps({
name: 'stop',
description: t`show a stop button on the streaming display that aborts generation when clicked`,
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
enumProvider: commonEnumProviders.boolean('trueFalse'),
}),
SlashCommandNamedArgument.fromProps({
name: 'onStop',
description: t`closure to execute when the stop button is clicked (in addition to aborting the request)`,
typeList: [ARGUMENT_TYPE.CLOSURE],
}),
SlashCommandNamedArgument.fromProps({
name: 'onComplete',
description: t`closure to execute after generation completes successfully`,
typeList: [ARGUMENT_TYPE.CLOSURE],
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'prompt',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
}),
],
helpString: `
<div>
${t`Generates text using Connection Manager with streaming display. Shows live generation progress including reasoning (thinking) and content.`}
</div>
<div>
${t`Requires Connection Manager extension. Uses the currently selected profile or the specified profile= argument.`}
</div>
<div>
${t`Use reasoning=true to include formatted reasoning in the output (using the defined reasoning template). This can be parsed later with /reasoning-parse.`}
</div>
<div>
${t`Use delay to control auto-hide behavior: number (ms), "infinite", or negative to keep the display open until manually closed. The display shows a green LED when complete.`}
</div>
<div>
${t`A stop button is shown by default (stop=true). Click it to abort generation and return whatever was streamed so far. Use stop=false to hide the stop button.`}
</div>
<div>
${t`Use onStop and onComplete closures for custom behavior when generation is stopped or completes.`}
</div>
<div>
${t`Example: <pre><code>/profile-genstream profile=my-profile-id reasoning=true Summarize the following text</code></pre>`}
</div>
<div>
${t`Example with infinite display: <pre><code>/profile-genstream delay=infinite Tell me a story</code></pre>`}
</div>
<div>
${t`Example with custom stop handler: <pre><code>/profile-genstream onStop={: /echo "Generation stopped!" :} Tell me a story</code></pre>`}
</div>
`,
}));
}
@@ -7,5 +7,8 @@
"css": "style.css",
"author": "Cohee1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+65 -3
View File
@@ -4,7 +4,7 @@ import { characters, eventSource, event_types, generateQuietPrompt, generateRaw,
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar, isFalseBoolean } from '../../utils.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar, isFalseBoolean, includesIgnoreCaseAndAccents } from '../../utils.js';
import { hideMutedSprites, selected_group } from '../../group-chats.js';
import { isJsonSchemaSupported } from '../../textgen-settings.js';
import { debounce_timeout } from '../../constants.js';
@@ -786,6 +786,32 @@ async function setSpriteSlashCommand({ type }, searchTerm) {
return label;
}
/**
* @param {string} expressionName - Label of the expression to set as fallback
*/
function setFallBackExpressionSlashCommand(args, expressionName) {
expressionName = expressionName.trim().toLowerCase();
if (!expressionName) return extension_settings?.expressions?.fallback_expression || '';
const select = /** @type {HTMLSelectElement} */(document.getElementById('expression_fallback'));
const fallbackExpressions = Array
.from(select?.options || [])
.map(option => option.value)
.filter(expression => expression?.length > 0);
const expressionMatch = fallbackExpressions.find(expression => includesIgnoreCaseAndAccents(expression, expressionName));
if (!expressionMatch) {
toastr.warning(t`No expression found for search term ${expressionName}`, t`Set Fallback Expression`);
return '';
}
$(select).val(expressionMatch).trigger('change');
return expressionMatch;
}
/**
* Returns the sprite folder name (including override) for a character.
* @param {object} char Character object
@@ -2140,7 +2166,7 @@ function migrateSettings() {
}
}
(async function () {
export async function init() {
function addExpressionImage() {
const html = `
<div id="expression-wrapper">
@@ -2316,6 +2342,42 @@ function migrateSettings() {
helpString: 'Force sets the expression for the current character.',
returns: 'The currently set expression label after setting it.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'expression-fallback',
callback: setFallBackExpressionSlashCommand,
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'expression label to set',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
enumProvider: () => [
new SlashCommandEnumValue('#none', 'Sets the fallback expression to no image'),
new SlashCommandEnumValue('#emoji', 'Sets the fallback expression to emojis'),
...localEnumProviders.expressions(),
],
}),
],
helpString: `
<div>
Gets the currently selected expression fallback for all characters.<br />
If a valid expression label is sent, it will be set as the new fallback.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/expression-fallback | /echo</code></pre>
<small>Returns the currently selected fallback.</small>
</li>
<li>
<pre><code>/expression-fallback admiration</code></pre>
<small>Sets a new expression as fallback.</small>
</li>
</ul>
</div>
`,
returns: 'The currently set expression label after setting it.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'expression-folder-override',
aliases: ['spriteoverride', 'costume'],
@@ -2511,4 +2573,4 @@ function migrateSettings() {
</div>
`,
}));
})();
}
@@ -9,5 +9,8 @@
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+2 -2
View File
@@ -818,7 +818,7 @@ function addGalleryWandButton() {
}
// On extension load, ensure the settings are initialized
(function () {
export async function init() {
initSettings();
eventSource.on(event_types.CHARACTER_RENAMED, (oldAvatar, newAvatar) => {
const context = SillyTavern.getContext();
@@ -850,4 +850,4 @@ function addGalleryWandButton() {
}),
);
addGalleryWandButton();
})();
}
@@ -8,5 +8,8 @@
"css": "style.css",
"author": "City-Unit",
"version": "1.5.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+2 -2
View File
@@ -1063,7 +1063,7 @@ function setupListeners() {
});
}
jQuery(async function () {
export async function init() {
async function addExtensionControls() {
const settingsHtml = await renderExtensionTemplateAsync('memory', 'settings', { defaultSettings });
$('#summarize_container').append(settingsHtml);
@@ -1128,4 +1128,4 @@ jQuery(async function () {
() => summaryMacroHandler(),
'Returns the latest memory/summary from the current chat.');
}
});
}
@@ -9,5 +9,8 @@
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
@@ -169,7 +169,7 @@ const handleCharChange = () => {
settings.charConfig = charConfig;
};
const init = async () => {
export async function init() {
await loadSets();
await loadSettings();
log('settings: ', settings);
@@ -214,7 +214,8 @@ const init = async () => {
eventSource.on(event_types.APP_READY, async () => await finalizeInit());
globalThis.quickReplyApi = quickReplyApi;
};
}
const finalizeInit = async () => {
debug('executing startup');
await autoExec.handleStartup();
@@ -229,7 +230,7 @@ const finalizeInit = async () => {
isReady = true;
debug('READY');
};
await init();
const purgeCharacterQuickReplySets = ({ character }) => {
// Remove the character's Quick Reply Sets from the settings.
@@ -7,5 +7,8 @@
"css": "style.css",
"author": "RossAscends#1779",
"version": "2.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+34 -3
View File
@@ -1639,7 +1639,7 @@ async function checkCharEmbeddedRegexScripts() {
function notifyReloadCurrentChat(presetName) {
toastr.info(
t`Reload the chat for regex to take effect` + '<br><u>' + t`Click here to reload immediately` + '</u>',
t`Preset '${presetName}' contains enabled regex scripts`,
t`Preset '${escapeHtml(presetName)}' contains enabled regex scripts`,
{
timeOut: 5000,
escapeHtml: false,
@@ -1709,7 +1709,7 @@ function onPresetRenamed({ apiId, oldName, newName }) {
// Workaround for loading in sequence with other extensions
// NOTE: Always puts extension at the top of the list, but this is fine since it's static
jQuery(async () => {
export async function init() {
if (!Array.isArray(extension_settings.regex)) {
extension_settings.regex = [];
}
@@ -2068,6 +2068,37 @@ jQuery(async () => {
],
helpString: 'Runs a Regex extension script by name on the provided string. The script must be enabled.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'regex-state',
/** @param {object} _ @param {string} name */
callback: (_, name) => {
if (!name) {
toastr.warning('No regex script name provided.');
return '';
}
const scripts = getRegexScripts();
const script = scripts.find(s => equalsIgnoreCaseAndAccents(s.scriptName, name));
if (!script) {
toastr.warning(`Regex script "${name}" not found.`);
return '';
}
return script.disabled ? 'false' : 'true';
},
returns: 'true (for enabled) or false (for disabled)',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'script name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.regexScripts,
}),
],
helpString: 'Returns the current state of a regex script.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'regex-toggle',
callback: toggleRegexCallback,
@@ -2123,4 +2154,4 @@ jQuery(async () => {
presetManager.setupEventListeners();
presetManager.registerSlashCommands();
});
}
@@ -7,5 +7,8 @@
"css": "style.css",
"author": "kingbri",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+35 -1
View File
@@ -1,4 +1,4 @@
import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
import { CONNECT_API_MAP, createModelIcon, getRequestHeaders } from '../../script.js';
import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
import { t } from '../i18n.js';
import { oai_settings, proxies, ZAI_ENDPOINT } from '../openai.js';
@@ -126,6 +126,10 @@ export async function getMultimodalCaption(base64Img, prompt) {
requestBody.zai_endpoint = oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON;
}
if (extension_settings.caption.multimodal_api === 'workers_ai') {
requestBody.workers_ai_account_id = oai_settings.workers_ai_account_id;
}
function getEndpointUrl() {
switch (extension_settings.caption.multimodal_api) {
case 'google':
@@ -283,6 +287,10 @@ function throwIfInvalidModel(useReverseProxy) {
if (multimodalApi === 'pollinations' && !secret_state[SECRET_KEYS.POLLINATIONS]) {
throw new Error('Pollinations API key is not set.');
}
if (multimodalApi === 'workers_ai' && (!secret_state[SECRET_KEYS.WORKERS_AI] || !oai_settings.workers_ai_account_id)) {
throw new Error('Workers AI API key or account ID is not set.');
}
}
/**
@@ -435,10 +443,12 @@ export class ConnectionManagerRequestService {
max_tokens: maxTokens,
model: profile.model,
chat_completion_source: selectedApiMap.source,
secret_id: profile['secret-id'],
custom_url: profile['api-url'],
vertexai_region: profile['api-url'],
zai_endpoint: profile['api-url'],
siliconflow_endpoint: profile['api-url'],
minimax_endpoint: profile['api-url'],
reverse_proxy: proxyPreset?.url,
proxy_password: proxyPreset?.password,
custom_prompt_post_processing: profile['prompt-post-processing'],
@@ -459,6 +469,7 @@ export class ConnectionManagerRequestService {
model: profile.model,
api_type: selectedApiMap.type,
api_server: profile['api-url'],
secret_id: profile['secret-id'],
...overridePayload,
}, {
instructName: includeInstruct ? profile.instruct : undefined,
@@ -533,6 +544,29 @@ export class ConnectionManagerRequestService {
return profile;
}
/**
* Creates a model icon Image element for the given profile (or the currently selected profile).
* Returns null if the profile is not found, has no API, or Connection Manager is unavailable.
* @param {string} [profileId] - Profile ID. If omitted, uses the currently selected profile.
* @returns {HTMLImageElement | null}
*/
static getProfileIcon(profileId) {
if ((SillyTavern.getContext()).extensionSettings.disabledExtensions.includes('connection-manager')) {
return null;
}
const id = profileId ?? (SillyTavern.getContext()).extensionSettings.connectionManager.selectedProfile;
if (!id) return null;
try {
const profile = this.getProfile(id);
if (!profile?.api) return null;
return createModelIcon(profile.api, profile.model);
} catch {
return null;
}
}
/**
* @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
* @returns {boolean}
@@ -2,7 +2,3 @@
<div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion"></div>
<span data-i18n="Generate Image">Generate Image</span>
</div>
<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task" data-i18n="[title]Abort current image generation task"></div>
<span data-i18n="Stop Image Generation">Stop Image Generation</span>
</div>
@@ -62,18 +62,13 @@ import { t, translate } from '../../i18n.js';
import { oai_settings } from '../../openai.js';
import { power_user } from '/scripts/power-user.js';
import { MacrosParser } from '/scripts/macros.js';
import { ActionLoaderHandle, loader } from '/scripts/action-loader.js';
export { MODULE_NAME };
const MODULE_NAME = 'sd';
// This is a 1x1 transparent PNG
const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const CUSTOM_STOP_EVENT = 'sd_stop_generation';
// Generation tracking for status indicator
let activeGenerations = 0;
/** @type {JQuery<HTMLElement>|null} */
let generationToast = null;
const sources = {
extras: 'extras',
@@ -99,6 +94,7 @@ const sources = {
google: 'google',
zai: 'zai',
openrouter: 'openrouter',
workersai: 'workersai',
};
const comfyTypes = {
standard: 'standard',
@@ -1747,6 +1743,9 @@ async function loadSamplers() {
case sources.openrouter:
samplers = ['N/A'];
break;
case sources.workersai:
samplers = ['N/A'];
break;
}
for (const sampler of samplers) {
@@ -1815,6 +1814,34 @@ async function loadAutoSamplers() {
}
}
async function loadSdcppModels() {
if (!extension_settings.sd.sdcpp_url) {
return [{ value: '', text: 'N/A' }];
}
try {
const result = await fetch('/api/sd/sdcpp/models', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url: extension_settings.sd.sdcpp_url }),
});
if (!result.ok) {
return [{ value: '', text: 'N/A' }];
}
const data = await result.json();
if (data?.data?.length > 0) {
return data.data.map(model => ({ value: model.id, text: model.name || model.id }));
}
} catch (error) {
console.error('Failed to load sd.cpp models:', error);
}
return [{ value: '', text: 'N/A' }];
}
async function loadSdcppSamplers() {
// The sdcpp server does not provide an API for samplers, so we return the known list.
return ['euler', 'euler_a', 'heun', 'dpm2', 'dpm++2s_a', 'dpm++2m', 'dpm++2mv2', 'ipndm', 'ipndm_v', 'lcm', 'ddim_trailing', 'tcd'];
@@ -1910,7 +1937,7 @@ async function loadModels() {
models = await loadAutoModels();
break;
case sources.sdcpp:
models = [{ value: '', text: 'N/A' }];
models = await loadSdcppModels();
break;
case sources.drawthings:
models = await loadDrawthingsModels();
@@ -1969,6 +1996,9 @@ async function loadModels() {
case sources.openrouter:
models = await loadOpenRouterModels();
break;
case sources.workersai:
models = await loadWorkersAIImageModels();
break;
}
if (extension_settings.sd.source === sources.electronhub) {
@@ -2096,6 +2126,33 @@ async function loadXAIModels() {
];
}
async function loadWorkersAIImageModels() {
$('#sd_cf_workers_key').toggleClass('success', !!secret_state[SECRET_KEYS.WORKERS_AI]);
if (!secret_state[SECRET_KEYS.WORKERS_AI]) {
return [];
}
if (!oai_settings.workers_ai_account_id) {
toastr.warning('Workers AI account ID is required. Save it in the "API Connections" panel.', 'Image Generation');
return [];
}
const result = await fetch('/api/sd/workersai/models', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
account_id: oai_settings.workers_ai_account_id,
}),
});
if (result.ok) {
return await result.json();
}
return [];
}
async function loadPollinationsModels() {
$('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]);
@@ -2325,6 +2382,8 @@ async function loadDrawthingsModels() {
async function loadOpenAiModels() {
return [
{ value: 'gpt-image-2', text: 'gpt-image-2' },
{ value: 'gpt-image-2-2026-04-21', text: 'gpt-image-2-2026-04-21' },
{ value: 'gpt-image-1.5', text: 'gpt-image-1.5' },
{ value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' },
{ value: 'gpt-image-1', text: 'gpt-image-1' },
@@ -2581,6 +2640,9 @@ async function loadSchedulers() {
case sources.openrouter:
schedulers = ['N/A'];
break;
case sources.workersai:
schedulers = ['N/A'];
break;
}
for (const scheduler of schedulers) {
@@ -2701,6 +2763,9 @@ async function loadVaes() {
case sources.openrouter:
vaes = ['N/A'];
break;
case sources.workersai:
vaes = ['N/A'];
break;
}
for (const vae of vaes) {
@@ -2912,62 +2977,6 @@ function ensureSelectionExists(setting, selector) {
}
}
/**
* Updates the generation status indicator based on active generation count.
* Shows/hides various UI indicators to inform user of background image generation.
*/
function updateGenerationIndicator() {
if (activeGenerations > 0) {
const countText = activeGenerations > 1 ? ` (${activeGenerations})` : '';
const toastText = `<i class="fa-solid fa-spinner fa-spin"></i> ${t`Generating an image`}${countText}...`;
// Show persistent toast if not already showing
if (!generationToast) {
generationToast = toastr.info(
toastText,
'Image Generation',
{
timeOut: 0,
extendedTimeOut: 0,
tapToDismiss: true,
escapeHtml: false,
onHidden: () => {
generationToast = null;
},
},
);
} else if (activeGenerations > 1) {
// Update count in existing toast
const toastMessage = $(generationToast).find('.toast-message');
if (toastMessage.length) {
toastMessage.html(toastText);
}
}
} else {
// Hide toast when done
if (generationToast) {
toastr.clear(generationToast);
generationToast = null;
}
}
}
/**
* Increments the active generation counter and updates indicators.
*/
function startGenerationTracking() {
activeGenerations++;
updateGenerationIndicator();
}
/**
* Decrements the active generation counter and updates indicators.
*/
function endGenerationTracking() {
activeGenerations = Math.max(0, activeGenerations - 1);
updateGenerationIndicator();
}
/**
* Generates an image based on the given trigger word.
* @param {string} initiator The initiator of the image generation
@@ -3028,12 +3037,13 @@ async function generatePicture(initiator, args, trigger, message, callback) {
const dimensions = setTypeSpecificDimensions(generationType);
const abortController = new AbortController();
const stopButton = document.getElementById('sd_stop_gen');
let negativePromptPrefix = args?.negative || '';
let imagePath = '';
const stopListener = () => abortController.abort('Aborted by user');
let loaderHandle = ActionLoaderHandle.EMPTY;
try {
const combineNegatives = (prefix) => { negativePromptPrefix = combinePrefixes(negativePromptPrefix, prefix); };
@@ -3046,16 +3056,19 @@ async function generatePicture(initiator, args, trigger, message, callback) {
await eventSource.emit(event_types.SD_PROMPT_PROCESSING, eventData);
prompt = eventData.prompt; // Allow extensions to modify the prompt
// Track this generation for status indicator
startGenerationTracking();
// Show stop button after prompt is ready (prompt generation uses separate abort mechanism)
$(stopButton).show();
eventSource.once(CUSTOM_STOP_EVENT, stopListener);
if (typeof args?._abortController?.addEventListener === 'function') {
args._abortController.addEventListener('abort', stopListener);
}
// Show non-blocking stoppable toast for this generation
loaderHandle = loader.show({
blocking: false,
slug: `${MODULE_NAME}-image-generation`,
title: t`Image Generation`,
message: t`Generating an image...`,
onStop: stopListener,
});
// generate the image
imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal);
} catch (err) {
@@ -3074,10 +3087,8 @@ async function generatePicture(initiator, args, trigger, message, callback) {
toastr.error(errorText, 'Image Generation');
throw new Error(errorText);
} finally {
$(stopButton).hide();
restoreOriginalDimensions(dimensions);
eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
endGenerationTracking();
await loaderHandle.hide();
}
return imagePath;
@@ -3404,6 +3415,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
case sources.openrouter:
result = await generateOpenRouterImage(prefixedPrompt, signal);
break;
case sources.workersai:
result = await generateWorkersAIImage(prefixedPrompt, negativePrompt, signal);
break;
}
if (!result.data) {
@@ -3858,6 +3872,7 @@ async function generateAutoImage(prompt, negativePrompt, signal) {
async function generateSdcppImage(prompt, negativePrompt, signal) {
const payload = {
url: extension_settings.sd.sdcpp_url,
model: extension_settings.sd.model || undefined,
prompt: prompt,
negative_prompt: negativePrompt,
steps: extension_settings.sd.steps,
@@ -4062,7 +4077,7 @@ async function generateOpenAiImage(prompt, signal) {
const isDalle2 = /dall-e-2/.test(extension_settings.sd.model);
const isDalle3 = /dall-e-3/.test(extension_settings.sd.model);
const isGptImg = /gpt-image-(1|latest)/.test(extension_settings.sd.model);
const isGptImg = /gpt-image-(1|2|latest)/.test(extension_settings.sd.model);
const isSora2 = /sora-2/.test(extension_settings.sd.model);
if (isDalle2 && prompt.length > dalle2PromptLimit) {
@@ -4719,6 +4734,33 @@ async function generateOpenRouterImage(prompt, signal) {
throw new Error(text);
}
async function generateWorkersAIImage(prompt, negativePrompt, signal) {
const result = await fetch('/api/sd/workersai/generate', {
method: 'POST',
headers: getRequestHeaders(),
signal: signal,
body: JSON.stringify({
prompt: prompt,
negative_prompt: negativePrompt,
model: extension_settings.sd.model,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
steps: extension_settings.sd.steps,
scale: extension_settings.sd.scale,
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
account_id: oai_settings.workers_ai_account_id,
}),
});
if (result.ok) {
const data = await result.json();
return { format: data?.format, data: data?.image };
} else {
const text = await result.text();
throw new Error(text);
}
}
async function onComfyOpenWorkflowEditorClick() {
let workflow = await (await fetch('/api/sd/comfy/workflow', {
method: 'POST',
@@ -5029,10 +5071,6 @@ async function addSDGenButtons() {
generatePicture(initiators.wand, {}, param);
}
});
const stopGenButton = $('#sd_stop_gen');
stopGenButton.hide();
stopGenButton.on('click', () => eventSource.emit(CUSTOM_STOP_EVENT));
}
function isValidState() {
@@ -5091,6 +5129,8 @@ function isValidState() {
return secret_state[SECRET_KEYS.ZAI];
case sources.openrouter:
return secret_state[SECRET_KEYS.OPENROUTER];
case sources.workersai:
return !!oai_settings.workers_ai_account_id && secret_state[SECRET_KEYS.WORKERS_AI];
default:
return false;
}
@@ -5115,10 +5155,6 @@ async function sdMessageButton($icon, { animate } = {}) {
$icon.toggleClass(classes.idle, !isBusy);
$icon.toggleClass(classes.busy, isBusy);
$media.toggleClass(classes.animation, isBusy);
// Update generation counter toast
const trackingFunction = isBusy ? startGenerationTracking : endGenerationTracking;
trackingFunction();
}
let $media = jQuery();
@@ -5233,7 +5269,6 @@ async function writePromptFields(characterId) {
* @returns {Promise<MediaAttachment|null>} - A promise that resolves to the newly generated media attachment, or null if generation failed or was aborted.
*/
async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, abortController = new AbortController()) {
const stopButton = document.getElementById('sd_stop_gen');
const stopListener = () => abortController.abort('Aborted by user');
const generationType = mediaAttachment.generation_type ?? message?.extra?.generationType ?? generationMode.FREE;
let dimensions = { width: extension_settings.sd.width, height: extension_settings.sd.height };
@@ -5247,9 +5282,9 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
source: MEDIA_SOURCE.GENERATED,
};
let loaderHandle = ActionLoaderHandle.EMPTY;
try {
$(stopButton).show();
eventSource.once(CUSTOM_STOP_EVENT, stopListener);
const callback = (_a, _b, _c, _d, _e, _f, format) => { result.type = isVideo(format) ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.IMAGE; };
const savedPrompt = mediaAttachment.title ?? message.extra.title ?? '';
const savedNegative = mediaAttachment.negative ?? message.extra.negative ?? '';
@@ -5265,6 +5300,15 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
: context.characters[context.characterId]?.name;
// Show non-blocking stoppable toast for this generation
loaderHandle = loader.show({
blocking: false,
slug: `${MODULE_NAME}-image-generation`,
title: t`Image Generation`,
message: t`Generating an image...`,
onStop: stopListener,
});
onStart();
result.url = await sendGenerationRequest(generationType, prompt, refineArgs.negative, characterName, callback, initiators.swipe, abortController.signal);
result.generation_type = generationType;
@@ -5276,11 +5320,10 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
}
} finally {
onComplete();
$(stopButton).hide();
eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
restoreOriginalDimensions(dimensions);
extension_settings.sd.seed = extension_settings.sd.original_seed;
delete extension_settings.sd.original_seed;
await loaderHandle.hide();
}
if (!result.url) {
@@ -5441,7 +5484,7 @@ function registerFunctionTool() {
});
}
jQuery(async () => {
export async function init() {
await addSDGenButtons();
const getSelectEnumProvider = (id, text) => () => Array.from(document.querySelectorAll(`#${id} > [value]`)).map(x => new SlashCommandEnumValue(x.getAttribute('value'), text ? x.textContent : null));
@@ -5850,6 +5893,9 @@ jQuery(async () => {
extension_settings.sd.google_duration = Number($(this).val());
saveSettingsDebounced();
});
$('#sd_models_refresh').on('click', async () => {
await loadModels();
});
$('#sd_electronhub_quality').on('change', function () {
extension_settings.sd.electronhub_quality = String($(this).val());
saveSettingsDebounced();
@@ -5893,6 +5939,7 @@ jQuery(async () => {
[sources.aimlapi]: SECRET_KEYS.AIMLAPI,
[sources.comfy]: SECRET_KEYS.COMFY_RUNPOD,
[sources.pollinations]: SECRET_KEYS.POLLINATIONS,
[sources.workersai]: SECRET_KEYS.WORKERS_AI,
};
const shouldReloadOptions = Object.entries(keySourceMap).some(([k, v]) => k === extension_settings.sd.source && v === key);
if (!shouldReloadOptions) {
@@ -5948,4 +5995,4 @@ jQuery(async () => {
t`Character's negative Image Generation prompt prefix`,
);
}
});
}
@@ -10,5 +10,8 @@
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
@@ -40,10 +40,11 @@
<span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span>
</label>
<label for="sd_source" data-i18n="Source">Source</label>
<select id="sd_source">
<select id="sd_source" class="text_pole">
<option value="aimlapi">AI/ML API</option>
<option value="bfl">BFL (Black Forest Labs)</option>
<option value="chutes">Chutes</option>
<option value="workersai">Cloudflare Workers AI</option>
<option value="comfy">ComfyUI</option>
<option value="drawthings">DrawThings HTTP API</option>
<option value="electronhub">Electron Hub</option>
@@ -123,7 +124,7 @@
<div class="flex-container" id="sd_electronhub_quality_row">
<div class="flex1">
<label for="sd_electronhub_quality" data-i18n="Image Quality">Image Quality</label>
<select id="sd_electronhub_quality"></select>
<select id="sd_electronhub_quality" class="text_pole"></select>
</div>
</div>
</div>
@@ -191,14 +192,14 @@
<div class="flex-container">
<div data-sd-model="dall-e-3" class="flex1">
<label for="sd_openai_style" data-i18n="Image Style">Image Style</label>
<select id="sd_openai_style">
<select id="sd_openai_style" class="text_pole">
<option value="vivid">Vivid</option>
<option value="natural">Natural</option>
</select>
</div>
<div data-sd-model="gpt-image" class="flex1">
<label for="sd_openai_quality_gpt" data-i18n="Image Quality">Image Quality</label>
<select id="sd_openai_quality_gpt">
<select id="sd_openai_quality_gpt" class="text_pole">
<option value="auto" data-i18n="Auto">Auto</option>
<option value="low" data-i18n="Low">Low</option>
<option value="medium" data-i18n="Medium">Medium</option>
@@ -207,7 +208,7 @@
</div>
<div data-sd-model="dall-e-3,cogview-4,glm-image,cogvideox" class="flex1">
<label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
<select id="sd_openai_quality">
<select id="sd_openai_quality" class="text_pole">
<option value="standard" data-i18n="Standard">Standard</option>
<option value="hd" data-i18n="HD">HD</option>
</select>
@@ -216,7 +217,7 @@
<div data-sd-model="sora-2,sora-2-pro" class="flex-container">
<div class="flex1">
<label for="sd_openai_duration" data-i18n="Duration">Duration</label>
<select id="sd_openai_duration">
<select id="sd_openai_duration" class="text_pole">
<option value="4" data-i18n="Short (4 seconds)">Short (4 seconds)</option>
<option value="8" data-i18n="Medium (8 seconds)">Medium (8 seconds)</option>
<option value="12" data-i18n="Long (16 seconds)">Long (12 seconds)</option>
@@ -226,7 +227,7 @@
</div>
<div data-sd-source="comfy">
<label for="sd_comfy_type">Server Type</label>
<select id="sd_comfy_type">
<select id="sd_comfy_type" class="text_pole">
<option value="standard">Standard Server</option>
<option value="runpod_serverless">RunPod Serverless Endpoint</option>
</select>
@@ -318,7 +319,7 @@
<div class="flex-container">
<div class="flex1">
<label for="sd_stability_style_preset" data-i18n="Style Preset">Style Preset</label>
<select id="sd_stability_style_preset">
<select id="sd_stability_style_preset" class="text_pole">
<option value="anime">Anime</option>
<option value="3d-model">3D Model</option>
<option value="analog-film">Analog Film</option>
@@ -375,6 +376,20 @@
</div>
</div>
<div data-sd-source="workersai">
<a href="https://dash.cloudflare.com" target="_blank" rel="noopener noreferrer">Cloudflare Workers AI</a>
<div class="flex-container flexnowrap alignItemsBaseline marginBot5">
<strong class="flex1" data-i18n="API Key">API Key</strong>
<div id="sd_cf_workers_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_workers_ai">
<i class="fa-fw fa-solid fa-key"></i>
<span data-i18n="Click to set">Click to set</span>
</div>
</div>
<div class="flex-container flexnowrap alignItemsBaseline">
<small class="flex1" data-i18n="Hint: Account ID and API key are pulled from API connections.">Hint: Account ID and API key are pulled from API connections.</small>
</div>
</div>
<div data-sd-source="google">
<div class="flex-container">
<div class="flex1">
@@ -394,7 +409,7 @@
</label>
<div class="flex1">
<label for="sd_google_duration" data-i18n="Duration (Veo)">Duration (Veo)</label>
<select id="sd_google_duration">
<select id="sd_google_duration" class="text_pole">
<option value="4">Short (4 seconds)</option>
<option value="6">Medium (6 seconds)</option>
<option value="8">Long (8 seconds)</option>
@@ -405,37 +420,42 @@
<div class="flex-container">
<div class="flex1">
<label for="sd_model" data-i18n="Model">Model</label>
<select id="sd_model"></select>
<label for="sd_model" class="flex-container justifySpaceBetween">
<span data-i18n="Model">Model</span>
<div id="sd_models_refresh" class="right_menu_button margin0 padding0" title="Refresh model list" data-i18n="[title]Refresh model list">
<i class="fa-solid fa-sync"></i>
</div>
</label>
<select id="sd_model" class="text_pole"></select>
</div>
<div class="flex1" data-sd-source="comfy,auto">
<label for="sd_vae">VAE</label>
<select id="sd_vae"></select>
<select id="sd_vae" class="text_pole"></select>
</div>
</div>
<div class="flex-container">
<div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy,sdcpp">
<label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
<select id="sd_sampler"></select>
<select id="sd_sampler" class="text_pole"></select>
</div>
<div class="flex1" data-sd-source="comfy,auto,novel,sdcpp">
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
<select id="sd_scheduler"></select>
<select id="sd_scheduler" class="text_pole"></select>
</div>
</div>
<div class="flex-container">
<div class="flex1">
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
<select id="sd_resolution"><!-- Populated in JS --></select>
<select id="sd_resolution" class="text_pole"><!-- Populated in JS --></select>
</div>
<div class="flex1" data-sd-source="auto,vlad,drawthings">
<label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label>
<select id="sd_hr_upscaler"></select>
<select id="sd_hr_upscaler" class="text_pole"></select>
</div>
</div>
@@ -1,7 +1,3 @@
.sd_settings label:not(.checkbox_label) {
display: block;
}
#sd_dropdown {
z-index: 30000;
backdrop-filter: blur(var(--SmartThemeBlurStrength));
@@ -101,7 +101,7 @@ async function doCount() {
return count;
}
jQuery(() => {
export function init() {
const buttonHtml = `
<div id="token_counter" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
@@ -115,4 +115,4 @@ jQuery(() => {
returns: 'number of tokens',
helpString: 'Counts the number of tokens in the current chat.',
}));
});
}
@@ -7,5 +7,8 @@
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+2 -2
View File
@@ -707,7 +707,7 @@ const handleMessageReasoningDelete = createEventHandler(removeReasoningDisplayTe
globalThis.translate = translate;
jQuery(async () => {
export async function init() {
const html = await renderExtensionTemplateAsync('translate', 'index');
const buttonHtml = await renderExtensionTemplateAsync('translate', 'buttons');
@@ -801,4 +801,4 @@ jQuery(async () => {
},
returns: ARGUMENT_TYPE.STRING,
}));
});
}
@@ -7,5 +7,8 @@
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
+39 -14
View File
@@ -1,6 +1,7 @@
import { cancelTtsPlay, eventSource, event_types, getCurrentChatId, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique, regexFromString } from '../../utils.js';
import { accountStorage } from '../../util/AccountStorage.js';
import { EdgeTtsProvider } from './edge.js';
import { ElevenLabsTtsProvider } from './elevenlabs.js';
import { SileroTtsProvider } from './silerotts.js';
@@ -165,7 +166,7 @@ async function onNarrateOneMessage() {
}
resetTtsPlayback();
processAndQueueTtsMessage(message, Number(id));
processAndQueueTtsMessage(message, Number(id), { manual: true });
moduleWorker();
}
@@ -186,13 +187,20 @@ async function onNarrateText(args, text) {
? voiceMap[DEFAULT_VOICE_MARKER]
: voiceMap[name];
if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) {
if (voiceMapEntry === DISABLED_VOICE_MARKER) {
toastr.info(`TTS voice for ${name} is disabled.`);
await initVoiceMap(false);
return;
}
if (!voiceMapEntry) {
toastr.info(`Specified voice for ${name} was not found. Check the TTS extension settings.`);
await initVoiceMap(false);
return;
}
resetTtsPlayback();
processAndQueueTtsMessage({ mes: text, name: name });
processAndQueueTtsMessage({ mes: text, name: name }, null, { manual: true });
await moduleWorker();
// Return back to the chat voices
@@ -245,7 +253,7 @@ function isTtsProcessing() {
}
/**
* @typedef {ChatMessage & { id?: number }} TtsMessage
* @typedef {ChatMessage & { id?: number, manual?: boolean, segmentText?: string, segmentType?: string }} TtsMessage
*/
/**
@@ -253,12 +261,15 @@ function isTtsProcessing() {
* (if enabled) and adds each part to the TTS job queue.
* @param {ChatMessage} message - The message object to be processed.
* @param {number|null} [messageId=null] - The chat message index to associate with TTS events.
* @param {object} [options={}] - Additional options for processing.
* @param {boolean} [options.manual=false] - Whether this TTS job was manually triggered (e.g., from the UI) rather than automatically from a new chat message.
* @returns {void}
*/
function processAndQueueTtsMessage(message, messageId = null) {
function processAndQueueTtsMessage(message, messageId = null, { manual = false } = {}) {
/** @type {TtsMessage} */
const clone = structuredClone(message);
clone.id = messageId ?? null;
clone.manual = manual ?? false;
if (!extension_settings.tts.narrate_by_paragraphs) {
ttsJobQueue.push(clone);
@@ -408,9 +419,10 @@ function onAudioControlClicked() {
// Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
if (!audioElement.paused || isTtsProcessing()) {
resetTtsPlayback();
} else {
} else if (context?.chat?.length > 0) {
// Default play behavior if not processing or playing is to play the last message.
processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
const id = context.chat.length - 1;
processAndQueueTtsMessage(context.chat[id], id, { manual: true });
}
updateUiAudioPlayState();
}
@@ -481,8 +493,10 @@ async function processAudioJobQueue() {
// TTS Control //
//################//
/** @type {TtsMessage[]} */
const ttsJobQueue = [];
let currentTtsJob; // Null if nothing is currently being processed
/** @type {TtsMessage|null} */
let currentTtsJob = null; // Null if nothing is currently being processed
function completeTtsJob() {
console.info(`Current TTS job for ${currentTtsJob?.name} completed.`);
@@ -621,7 +635,18 @@ async function processTtsQueue() {
const voiceMapEntry = voiceMap[voiceMapKey] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[voiceMapKey];
if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) {
if (voiceMapEntry === DISABLED_VOICE_MARKER) {
const storageKey = `tts_disabled_warned_${char}`;
if (!accountStorage.getItem(storageKey) || currentTtsJob.manual) {
accountStorage.setItem(storageKey, 'true');
toastr.info(`TTS voice for ${char} is disabled.`);
}
currentTtsJob = null;
setTimeout(() => wrapper.update(), 0);
return;
}
if (!voiceMapEntry) {
throw `${char} not in voicemap. Configure character in extension settings voice map`;
}
@@ -649,7 +674,6 @@ async function processTtsQueue() {
text = substituteParams(text);
if (extension_settings.tts.skip_codeblocks) {
text = text.replace(/^\s{4}.*$/gm, '').trim();
text = text.replace(/```.*?```/gs, '').trim();
text = text.replace(/~~~.*?~~~/gs, '').trim();
}
@@ -724,6 +748,7 @@ async function processTtsQueue() {
mes: currentTtsJob.mes,
extra: currentTtsJob.extra,
id: currentTtsJob.id,
manual: currentTtsJob.manual,
};
ttsJobQueue.unshift(segmentJob);
}
@@ -824,7 +849,7 @@ async function playFullConversation() {
context.chat.forEach((msg, i) => {
if (!msg.is_system && msg.mes !== '...' && msg.mes !== '') {
processAndQueueTtsMessage(msg, i);
processAndQueueTtsMessage(msg, i, { manual: false });
}
});
@@ -1164,7 +1189,7 @@ async function onMessageEvent(messageId, lastCharIndex) {
message.id = messageId;
ttsJobQueue.push(message);
} else {
processAndQueueTtsMessage(message, messageId);
processAndQueueTtsMessage(message, messageId, { manual: false });
}
}
@@ -1506,7 +1531,7 @@ async function initVoiceMapInternal(unrestricted) {
updateVoiceMap();
}
jQuery(async function () {
export async function init() {
async function addExtensionControls() {
const settingsHtml = $(await renderExtensionTemplateAsync('tts', 'settings'));
$('#tts_container').append(settingsHtml);
@@ -1594,4 +1619,4 @@ jQuery(async function () {
}));
document.body.appendChild(audioElement);
});
}
+4 -1
View File
@@ -11,5 +11,8 @@
"css": "style.css",
"author": "Ouoertheo#7264",
"version": "1.0.0",
"homePage": "None"
"homePage": "None",
"hooks": {
"activate": "init"
}
}
+264 -219
View File
@@ -44,6 +44,7 @@ import { oai_settings } from '../../openai.js';
* @property {string} text - The hashed message text
* @property {number} hash - The hash used as the vector key
* @property {number} index - The index of the message in the chat
* @property {boolean} [summaryFailed] - Whether summarization failed for this message (used internally to skip messages that fail summarization)
*/
const MODULE_NAME = 'vectors';
@@ -77,10 +78,13 @@ const settings = {
summarize_sent: false,
summary_source: 'main',
summary_prompt: 'Ignore previous instructions. Summarize the most important parts of the message. Limit yourself to 250 words or less. Your response should include nothing but the summary.',
summary_retries: 2,
summary_threshold: 200,
force_chunk_delimiter: '',
// For chats
enabled_chats: false,
keep_hidden: false,
template: 'Past events:\n{{text}}',
depth: 2,
position: extension_prompt_types.IN_PROMPT,
@@ -117,9 +121,76 @@ const settings = {
const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
const webllmProvider = new WebLlmVectorProvider();
/**
* Cache for storing summaries of messages by their hash.
* @type {Map<number, string>}
*/
const cachedSummaries = new Map();
/**
* Hashes skipped this Vectorize All session (summary or embed failure). Cleared on next Vectorize All click.
* @type {Set<number>}
*/
const skippedHashes = new Set();
/**
* Error causes treated as fatal abort Vectorize All rather than skip.
* @type {Set<string>}
*/
const FATAL_CAUSES = new Set(['account_id_missing', 'api_key_missing', 'api_url_missing', 'api_model_missing', 'extras_module_missing', 'webllm_not_supported', 'summary_endpoint_invalid']);
const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];
/**
* @typedef {object} RemoteEmbeddingEndpointConfig
* @property {string} url - The API endpoint URL
* @property {string} settingsKey - The key in settings for the selected model
* @property {string} selectId - The ID of the select element (without #)
* @property {string} [valueProperty='id'] - Property name for the option value
* @property {string} [textProperty] - Property name for the option text. Falls back to valueProperty
* @property {() => object} [getBody] - Function returning the request body
* @property {(models: any[]) => any[]} [filter] - Optional post-fetch filter for models
*/
/** @type {Record<string, RemoteEmbeddingEndpointConfig>} */
const remoteEmbeddingEndpoints = {
chutes: {
url: '/api/openai/chutes/models/embedding',
settingsKey: 'chutes_model',
selectId: 'vectors_chutes_model',
valueProperty: 'slug',
textProperty: 'name',
},
nanogpt: {
url: '/api/openai/nanogpt/models/embedding',
settingsKey: 'nanogpt_model',
selectId: 'vectors_nanogpt_model',
textProperty: 'name',
},
electronhub: {
url: '/api/openai/electronhub/models',
settingsKey: 'electronhub_model',
selectId: 'vectors_electronhub_model',
textProperty: 'name',
filter: models => models.filter(m => Array.isArray(m?.endpoints) && m.endpoints.includes('/v1/embeddings')),
},
openrouter: {
url: '/api/openrouter/models/embedding',
settingsKey: 'openrouter_model',
selectId: 'vectors_openrouter_model',
textProperty: 'name',
},
siliconflow: {
url: '/api/openai/siliconflow/models/embedding',
settingsKey: 'siliconflow_model',
selectId: 'vectors_siliconflow_model',
getBody: () => ({ siliconflow_endpoint: oai_settings.siliconflow_endpoint }),
},
workers_ai: {
url: '/api/openai/workers-ai/models/embedding',
settingsKey: 'workers_ai_model',
selectId: 'vectors_workers_ai_model',
getBody: () => ({ workers_ai_account_id: oai_settings.workers_ai_account_id }),
},
};
/**
* Gets the Collection ID for a file embedded in the chat.
* @param {string} fileUrl URL of the file
@@ -145,10 +216,12 @@ async function onVectorizeAllClick() {
// Clear all cached summaries to ensure that new ones are created
// upon request of a full vectorise
cachedSummaries.clear();
skippedHashes.clear();
const batchSize = getBatchSize();
const elapsedLog = [];
let finished = false;
let initialPending = null; // total items pending at the start of this run — set on first sync return
$('#vectorize_progress').show();
$('#vectorize_progress_percent').text('0');
$('#vectorize_progress_eta').text('...');
@@ -162,16 +235,27 @@ async function onVectorizeAllClick() {
const startTime = Date.now();
const remaining = await synchronizeChat(batchSize);
const elapsed = Date.now() - startTime;
if (remaining === null) {
// synchronizeChat already surfaced a toast; bail out of the loop.
throw new Error('Vectorization aborted');
}
elapsedLog.push(elapsed);
finished = remaining <= 0;
const total = getContext().chat.length;
const processed = total - remaining;
const processedPercent = Math.round((processed / total) * 100); // percentage of the work done
if (initialPending === null) {
initialPending = Math.max(0, remaining + batchSize);
}
const pending = Math.max(0, remaining);
const processed = Math.max(0, initialPending - pending);
const processedPercent = initialPending > 0
? Math.min(100, Math.round((processed / initialPending) * 100))
: 100;
const lastElapsed = elapsedLog.slice(-5); // last 5 elapsed times
const averageElapsed = lastElapsed.reduce((a, b) => a + b, 0) / lastElapsed.length; // average time needed to process one item
const pace = averageElapsed / batchSize; // time needed to process one item
const remainingTime = Math.round(pace * remaining / 1000);
const remainingTime = Math.round(pace * pending / 1000);
$('#vectorize_progress_percent').text(processedPercent);
$('#vectorize_progress_eta').text(remainingTime);
@@ -180,6 +264,9 @@ async function onVectorizeAllClick() {
throw new Error('Chat changed');
}
}
if (skippedHashes.size > 0) {
toastr.warning(`${skippedHashes.size} message(s) skipped due to errors. Click Vectorize All again to retry.`, 'Vectorization partial');
}
} catch (error) {
console.error('Vectors: Failed to vectorize all', error);
} finally {
@@ -250,7 +337,7 @@ async function summarizeExtra(element) {
if (apiResult.ok) {
const data = await apiResult.json();
element.text = data.summary;
element.text = removeReasoningFromString(data.summary);
}
} catch (error) {
console.log(error);
@@ -282,45 +369,70 @@ async function summarizeWebLLM(element) {
}
const messages = [{ role: 'system', content: settings.summary_prompt }, { role: 'user', content: element.text }];
element.text = await generateWebLlmChatPrompt(messages);
element.text = removeReasoningFromString(await generateWebLlmChatPrompt(messages));
return true;
}
/**
* Summarizes messages using the chosen method.
* @param {HashedMessage[]} hashedMessages Array of hashed messages
* @param {string} endpoint Type of endpoint to use
* @returns {Promise<HashedMessage[]>} Summarized messages
* Runs one summarization attempt for a single element via the chosen endpoint.
* @param {HashedMessage} element
* @param {string} endpoint
* @returns {Promise<boolean>} Whether the attempt succeeded.
*/
async function summarize(hashedMessages, endpoint = 'main') {
for (const element of hashedMessages) {
const cachedSummary = cachedSummaries.get(element.hash);
if (!cachedSummary) {
let success = true;
async function summarizeOne(element, endpoint) {
switch (endpoint) {
case 'main':
success = await summarizeMain(element);
break;
return await summarizeMain(element);
case 'extras':
success = await summarizeExtra(element);
break;
return await summarizeExtra(element);
case 'webllm':
success = await summarizeWebLLM(element);
break;
return await summarizeWebLLM(element);
default:
console.error('Unsupported endpoint', endpoint);
success = false;
break;
throw new Error(`Unsupported summary endpoint: ${endpoint}`, { cause: 'summary_endpoint_invalid' });
}
if (success) {
cachedSummaries.set(element.hash, element.text);
} else {
break;
}
} else {
}
/**
* Summarizes messages using the chosen method. Every returned element has been
* summarized (via live call or cache). Throws if any element fails after
* `settings.summary_retries` attempts.
* @param {HashedMessage[]} hashedMessages Array of hashed messages (mutated in place)
* @param {string} endpoint Type of endpoint to use
* @param {Object} [options] Options for summarization behavior
* @param {boolean} [options.skipOnFailure=false] If true, tags failed elements with `summaryFailed = true` instead of throwing
* @returns {Promise<HashedMessage[]>} Summarized messages
*/
async function summarize(hashedMessages, endpoint = 'main', { skipOnFailure = false } = {}) {
const maxAttempts = Math.max(1, Number(settings.summary_retries) || 1);
for (const element of hashedMessages) {
const cachedSummary = cachedSummaries.get(element.hash);
if (cachedSummary) {
element.text = cachedSummary;
continue;
}
let success = false;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
success = await summarizeOne(element, endpoint);
if (success) break;
} catch (error) {
if (FATAL_CAUSES.has(error?.cause)) throw error;
console.warn(`Vectors: summary attempt ${attempt}/${maxAttempts} threw for hash ${element.hash}`, error);
}
console.warn(`Vectors: summary attempt ${attempt}/${maxAttempts} failed for hash ${element.hash}`);
}
if (!success) {
if (skipOnFailure) {
console.warn(`Vectors: summarization exhausted ${maxAttempts} attempt(s) for hash ${element.hash} — marking for skip`);
element.summaryFailed = true;
continue;
}
throw new Error(`Summarization failed after ${maxAttempts} attempt(s)`, { cause: 'summary_failed' });
}
cachedSummaries.set(element.hash, element.text);
}
return hashedMessages;
}
@@ -347,21 +459,43 @@ async function synchronizeChat(batchSize = 5) {
return -1;
}
const hashedMessages = context.chat.filter(x => !x.is_system).map(x => ({ text: String(substituteParams(x.mes)), hash: getStringHash(substituteParams(x.mes)), index: context.chat.indexOf(x) }));
/** @type {HashedMessage[]} */
const hashedMessages = context.chat.filter(x => settings.keep_hidden || !x.is_system).map(x => ({ text: String(substituteParams(x.mes)), hash: getStringHash(substituteParams(x.mes)), index: context.chat.indexOf(x) }));
const hashesInCollection = await getSavedHashes(chatId);
let newVectorItems = hashedMessages.filter(x => !hashesInCollection.includes(x.hash));
const newVectorItems = hashedMessages
.filter(x => !hashesInCollection.includes(x.hash))
.filter(x => !skippedHashes.has(x.hash));
const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x));
let batch = newVectorItems.slice(0, batchSize);
if (settings.summarize) {
newVectorItems = await summarize(newVectorItems, settings.summary_source);
const minLength = Math.max(0, Number(settings.summary_threshold) || 0);
const toSummarize = minLength > 0 ? batch.filter(x => x.text.length >= minLength) : batch;
if (toSummarize.length > 0) {
await summarize(toSummarize, settings.summary_source, { skipOnFailure: true });
const failed = toSummarize.filter(x => x.summaryFailed);
if (failed.length > 0) {
for (const item of failed) skippedHashes.add(item.hash);
batch = batch.filter(x => !x.summaryFailed);
}
}
}
if (newVectorItems.length > 0) {
const chunkedBatch = splitByChunks(newVectorItems.slice(0, batchSize));
if (batch.length > 0) {
const chunkedBatch = splitByChunks(batch);
console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batchSize}...`);
console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batch.length}...`);
try {
await insertVectorItems(chatId, chunkedBatch);
} catch (insertError) {
if (FATAL_CAUSES.has(insertError?.cause)) {
throw insertError;
}
console.warn('Vectors: insert failed for batch — marking for skip', insertError);
for (const item of batch) skippedHashes.add(item.hash);
}
}
if (deletedHashes.length > 0) {
@@ -388,6 +522,12 @@ async function synchronizeChat(batchSize = 5) {
return 'Extras API must provide an "embeddings" module.';
case 'webllm_not_supported':
return 'WebLLM extension is not installed or the model is not set.';
case 'account_id_missing':
return 'Workers AI account ID is required. Save it in the "API Connections" panel.';
case 'summary_endpoint_invalid':
return 'Summarization endpoint is not supported.';
case 'summary_failed':
return 'Summarization failed after the configured number of retries.';
default:
return 'Check server console for more details';
}
@@ -397,7 +537,7 @@ async function synchronizeChat(batchSize = 5) {
const message = getErrorMessage(error.cause);
toastr.error(message, 'Vectorization failed', { preventDuplicates: true });
return -1;
return null;
} finally {
syncBlocked = false;
}
@@ -771,7 +911,11 @@ async function getQueryText(chat, initiator) {
.slice(0, settings.query);
if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) {
hashedMessages = await summarize(hashedMessages, settings.summary_source);
const minLength = Math.max(0, Number(settings.summary_threshold) || 0);
const toSummarize = minLength > 0 ? hashedMessages.filter(x => x.text.length >= minLength) : hashedMessages;
if (toSummarize.length > 0) {
await summarize(toSummarize, settings.summary_source, { skipOnFailure: true });
}
}
const queryText = hashedMessages.map(x => x.text).join('\n');
@@ -842,6 +986,10 @@ function getVectorsRequestBody(args = {}) {
body.model = extension_settings.vectors.siliconflow_model;
body.siliconflow_endpoint = oai_settings.siliconflow_endpoint;
break;
case 'workers_ai':
body.model = extension_settings.vectors.workers_ai_model || '@cf/baai/bge-m3';
body.workers_ai_account_id = oai_settings.workers_ai_account_id;
break;
default:
break;
}
@@ -935,6 +1083,7 @@ function throwIfSourceInvalid() {
settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] ||
settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] ||
settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE] ||
settings.source === 'workers_ai' && !secret_state[SECRET_KEYS.WORKERS_AI] ||
settings.source === 'siliconflow' && !secret_state[SECRET_KEYS.SILICONFLOW]) {
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
}
@@ -963,6 +1112,10 @@ function throwIfSourceInvalid() {
if (settings.source === 'webllm' && (!isWebLlmSupported() || !settings.webllm_model)) {
throw new Error('Vectors: WebLLM is not supported', { cause: 'webllm_not_supported' });
}
if (settings.source === 'workers_ai' && !oai_settings.workers_ai_account_id) {
throw new Error('Vectors: Workers AI account ID missing', { cause: 'account_id_missing' });
}
}
/**
@@ -972,11 +1125,12 @@ function throwIfSourceInvalid() {
* @returns {Promise<void>}
*/
async function deleteVectorItems(collectionId, hashes) {
const args = await getAdditionalArgs([]);
const response = await fetch('/api/vector/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
...getVectorsRequestBody(args),
collectionId: collectionId,
hashes: hashes,
source: settings.source,
@@ -1154,211 +1308,77 @@ function toggleSettings() {
$('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');
$('#google_vectorsModel').toggle(settings.source === 'palm' || settings.source === 'vertexai');
$('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow');
$('#workers_ai_vectorsModel').toggle(settings.source === 'workers_ai');
$('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));
switch (settings.source) {
case 'webllm':
if (settings.source === 'webllm') {
loadWebLlmModels();
break;
case 'electronhub':
loadElectronHubModels();
break;
case 'openrouter':
loadOpenRouterModels();
break;
case 'chutes':
loadChutesModels();
break;
case 'nanogpt':
loadNanoGPTModels();
break;
case 'siliconflow':
loadSiliconFlowModels();
break;
}
}
async function loadChutesModels() {
try {
const response = await fetch('/api/openai/chutes/models/embedding', {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
/** @type {Array<any>} */
const data = await response.json();
const models = Array.isArray(data) ? data : [];
populateChutesModelSelect(models);
} catch (err) {
console.warn('Chutes models fetch failed', err);
populateChutesModelSelect([]);
}
}
function populateChutesModelSelect(models) {
const select = $('#vectors_chutes_model');
select.empty();
for (const m of models) {
const option = document.createElement('option');
option.value = m.slug;
option.text = m.name;
select.append(option);
}
if (!settings.chutes_model && models.length) {
settings.chutes_model = models[0].slug;
}
$('#vectors_chutes_model').val(settings.chutes_model);
}
async function loadNanoGPTModels() {
try {
const response = await fetch('/api/openai/nanogpt/models/embedding', {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
/** @type {Array<any>} */
const data = await response.json();
const models = Array.isArray(data) ? data : [];
populateNanoGPTModelSelect(models);
} catch (err) {
console.warn('NanoGPT models fetch failed', err);
populateNanoGPTModelSelect([]);
}
}
function populateNanoGPTModelSelect(models) {
const select = $('#vectors_nanogpt_model');
select.empty();
for (const m of models) {
const option = document.createElement('option');
option.value = m.id;
option.text = m.name || m.id;
select.append(option);
}
if (!settings.nanogpt_model && models.length) {
settings.nanogpt_model = models[0].id;
}
$('#vectors_nanogpt_model').val(settings.nanogpt_model);
}
async function loadElectronHubModels() {
try {
const response = await fetch('/api/openai/electronhub/models', {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
/** @type {Array<any>} */
const data = await response.json();
// filter by embeddings endpoint
const models = Array.isArray(data) ? data.filter(m => Array.isArray(m?.endpoints) && m.endpoints.includes('/v1/embeddings')) : [];
populateElectronHubModelSelect(models);
} catch (err) {
console.warn('Electron Hub models fetch failed', err);
populateElectronHubModelSelect([]);
} else if (settings.source in remoteEmbeddingEndpoints) {
loadRemoteEmbeddingModels(settings.source);
}
}
/**
* Populates the Electron Hub model select element.
* @param {{ id: string, name: string }[]} models Electron Hub models
* Loads models from a remote embedding endpoint and populates the corresponding select element.
* @param {string} source - The source key matching a remoteEmbeddingEndpoints entry
*/
function populateElectronHubModelSelect(models) {
const select = $('#vectors_electronhub_model');
async function loadRemoteEmbeddingModels(source) {
const config = remoteEmbeddingEndpoints[source];
if (!config) {
return;
}
const { url, settingsKey, selectId, getBody, filter } = config;
const valueProperty = config.valueProperty || 'id';
const textProperty = config.textProperty;
/**
* Populates the select element with the given models.
* @param {any[]} models - Array of model objects
*/
function populateSelect(models) {
const select = $(`#${selectId}`);
select.empty();
for (const m of models) {
const option = document.createElement('option');
option.value = m.id;
option.text = m.name || m.id;
option.value = m[valueProperty];
option.text = textProperty ? (m[textProperty] || m[valueProperty]) : m[valueProperty];
select.append(option);
}
if (!settings.electronhub_model && models.length) {
settings.electronhub_model = models[0].id;
if (!settings[settingsKey] && models.length) {
settings[settingsKey] = models[0][valueProperty];
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
}
select.val(settings[settingsKey]);
}
$('#vectors_electronhub_model').val(settings.electronhub_model);
}
async function loadOpenRouterModels() {
try {
const response = await fetch('/api/openrouter/models/embedding', {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
/** @type {Array<any>} */
const data = await response.json();
const models = Array.isArray(data) ? data : [];
populateOpenRouterModelSelect(models);
} catch (err) {
console.warn('OpenRouter models fetch failed', err);
populateOpenRouterModelSelect([]);
}
}
const body = typeof getBody === 'function' ? getBody() : {};
/**
* Populates the OpenRouter model select element.
* @param {{ id: string, name: string }[]} models OpenRouter models
*/
function populateOpenRouterModelSelect(models) {
const select = $('#vectors_openrouter_model');
select.empty();
for (const m of models) {
const option = document.createElement('option');
option.value = m.id;
option.text = m.name || m.id;
select.append(option);
}
if (!settings.openrouter_model && models.length) {
settings.openrouter_model = models[0].id;
}
$('#vectors_openrouter_model').val(settings.openrouter_model);
}
async function loadSiliconFlowModels() {
try {
const response = await fetch('/api/openai/siliconflow/models/embedding', {
/** @type {RequestInit} */
const fetchOptions = {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
siliconflow_endpoint: oai_settings.siliconflow_endpoint,
}),
});
body: JSON.stringify(body || {}),
};
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
/** @type {Array<any>} */
const data = await response.json();
const models = Array.isArray(data) ? data : [];
populateSiliconFlowModelSelect(models);
} catch (err) {
console.warn('SiliconFlow models fetch failed', err);
populateSiliconFlowModelSelect([]);
let models = Array.isArray(data) ? data : [];
if (filter) {
models = filter(models);
}
}
function populateSiliconFlowModelSelect(models) {
const select = $('#vectors_siliconflow_model');
select.empty();
for (const m of models) {
const option = document.createElement('option');
option.value = m.id;
option.text = m.id;
select.append(option);
populateSelect(models);
} catch (err) {
console.warn(`${source} models fetch failed`, err);
populateSelect([]);
}
if (!settings.siliconflow_model && models.length) {
settings.siliconflow_model = models[0].id;
}
$('#vectors_siliconflow_model').val(settings.siliconflow_model);
}
/**
@@ -1496,10 +1516,11 @@ async function onViewStatsClick() {
{ timeOut: 10000, escapeHtml: false },
);
$('#chat .mes.vectorized').removeClass('vectorized');
const chat = getContext().chat;
for (const message of chat) {
if (hashesInCollection.includes(getStringHash(substituteParams(message.mes)))) {
const messageElement = $(`.mes[mesid="${chat.indexOf(message)}"]`);
const messageElement = $(`#chat .mes[mesid="${chat.indexOf(message)}"]`);
messageElement.addClass('vectorized');
}
}
@@ -1704,7 +1725,7 @@ async function activateWorldInfo(chat) {
await eventSource.emit(event_types.WORLDINFO_FORCE_ACTIVATE, activatedEntries);
}
jQuery(async () => {
export async function init() {
if (!extension_settings.vectors) {
extension_settings.vectors = settings;
}
@@ -1726,6 +1747,11 @@ jQuery(async () => {
saveSettingsDebounced();
toggleSettings();
});
$('#vectors_keep_hidden').prop('checked', settings.keep_hidden).on('input', () => {
settings.keep_hidden = !!$('#vectors_keep_hidden').prop('checked');
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_enabled_files').prop('checked', settings.enabled_files).on('input', () => {
settings.enabled_files = $('#vectors_enabled_files').prop('checked');
Object.assign(extension_settings.vectors, settings);
@@ -1778,6 +1804,11 @@ jQuery(async () => {
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_workers_ai_model').val(settings.workers_ai_model).on('change', () => {
settings.workers_ai_model = String($('#vectors_workers_ai_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {
settings.openrouter_model = String($('#vectors_openrouter_model').val());
Object.assign(extension_settings.vectors, settings);
@@ -1888,6 +1919,20 @@ jQuery(async () => {
saveSettingsDebounced();
});
$('#vectors_summary_retries').val(settings.summary_retries).on('input', () => {
const parsed = Number($('#vectors_summary_retries').val());
settings.summary_retries = Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : 1;
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_summary_threshold').val(settings.summary_threshold).on('input', () => {
const parsed = Number($('#vectors_summary_threshold').val());
settings.summary_threshold = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0;
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => {
settings.message_chunk_size = Number($('#vectors_message_chunk_size').val());
Object.assign(extension_settings.vectors, settings);
@@ -2310,4 +2355,4 @@ jQuery(async () => {
}
await purgeAllVectorIndexes();
});
});
}
@@ -10,5 +10,8 @@
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
"homePage": "https://github.com/SillyTavern/SillyTavern",
"hooks": {
"activate": "init"
}
}
@@ -11,6 +11,7 @@
</label>
<select id="vectors_source" class="text_pole">
<option value="chutes">Chutes</option>
<option value="workers_ai">Cloudflare Workers AI</option>
<option value="cohere">Cohere</option>
<option value="electronhub">Electron Hub</option>
<option value="extras">Extras (deprecated)</option>
@@ -205,6 +206,16 @@
</i>
</div>
<div class="flex-container flexFlowColumn" id="workers_ai_vectorsModel">
<label for="vectors_workers_ai_model" data-i18n="Vectorization Model">
Vectorization Model
</label>
<select id="vectors_workers_ai_model" class="text_pole"></select>
<i data-i18n="Hint: Set your Workers AI API key and Account ID in API Connections.">
Hint: Set your Workers AI API key and Account ID in API Connections.
</i>
</div>
<div class="flex-container marginTopBot5">
<div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance.">
<label for="vectors_query">
@@ -408,6 +419,10 @@
<hr>
<div id="vectors_chats_settings">
<label class="checkbox_label expander" for="vectors_keep_hidden" title="Keep hidden (system) messages in the vector index and include them in query results.">
<input id="vectors_keep_hidden" type="checkbox" class="checkbox">
<span data-i18n="Include hidden messages">Include hidden messages</span>
</label>
<div id="vectors_advanced_settings">
<label for="vectors_template" data-i18n="Injection Template">
Injection Template
@@ -478,6 +493,16 @@
<label for="vectors_summary_prompt" title="Summary Prompt:">Summary Prompt:</label>
<small data-i18n="Only used when Main API or WebLLM Extension is selected.">Only used when Main API or WebLLM Extension is selected.</small>
<textarea id="vectors_summary_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation."></textarea>
<label for="vectors_summary_retries" title="Number of attempts per message before aborting vectorization.">
<span data-i18n="Summarization retries per message">Summarization retries per message</span>
</label>
<input id="vectors_summary_retries" type="number" class="text_pole widthUnset" min="1" max="10" step="1" />
<label for="vectors_summary_threshold" title="Messages shorter than this (in characters) are embedded as-is without summarization. Set to 0 to always summarize.">
<span data-i18n="Summarization min length (chars)">Summarization min length (chars)</span>
</label>
<input id="vectors_summary_threshold" type="number" class="text_pole widthUnset" min="0" step="1" />
</div>
</div>
<small data-i18n="Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.">
+11 -1
View File
@@ -289,6 +289,16 @@ export class FilterHelper {
return this.filterDataByState(data, state, isFolder);
}
/**
* Filters an array of entities based on a tri-state filter value.
* SELECTED keeps entities where filterFunc returns true; EXCLUDED removes them; UNDEFINED returns data unchanged.
* @param {any[]} data The data to filter
* @param {FilterState|string} state The tri-state filter value (SELECTED, EXCLUDED, or UNDEFINED)
* @param {Function} filterFunc A predicate function applied to each entity
* @param {object} [options] Options object
* @param {boolean} [options.includeFolders=false] If true, entities with type 'tag' always pass through
* @returns {any[]} The filtered data
*/
filterDataByState(data, state, filterFunc, { includeFolders = false } = {}) {
if (isFilterState(state, FILTER_STATES.SELECTED)) {
return data.filter(entity => filterFunc(entity) || (includeFolders && entity.type == 'tag'));
@@ -414,7 +424,7 @@ export class FilterHelper {
typeScores.set(uid, score);
}
this.scoreCache.set(type, typeScores);
console.debug('search scores chached', type, typeScores);
console.debug('search scores cached', type, typeScores);
}
/**
+2 -1
View File
@@ -139,7 +139,8 @@ async function getLocaleData(language) {
function findLang(language) {
const supportedLang = langs.find(x => x.lang === language);
if (!supportedLang && language !== 'en') {
const isEn = language.startsWith('en'); // includes 'en', and more specific locales like 'en-us', 'en-au', etc
if (!supportedLang && !isEn) {
console.warn(`Unsupported language: ${language}`);
}
return supportedLang;
+1 -1
View File
@@ -597,7 +597,7 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
function getSequence() {
// User impersonation prompt
if (isImpersonate) {
return instruct.input_sequence;
return instruct.last_input_sequence || instruct.input_sequence;
}
// Neutral / system / quiet prompt
+5
View File
@@ -31,6 +31,8 @@ export async function loadItemizedPrompts(chatId) {
if (!itemizedPrompts) {
itemizedPrompts = [];
}
await eventSource.emit(event_types.ITEMIZED_PROMPTS_LOADED, { chatId: chatId });
} catch {
console.log('Error loading itemized prompts for chat', chatId);
itemizedPrompts = [];
@@ -48,6 +50,7 @@ export async function saveItemizedPrompts(chatId) {
}
await promptStorage.setItem(chatId, itemizedPrompts);
await eventSource.emit(event_types.ITEMIZED_PROMPTS_SAVED, { chatId: chatId });
} catch {
console.log('Error saving itemized prompts for chat', chatId);
}
@@ -84,6 +87,7 @@ export async function deleteItemizedPrompts(chatId) {
}
await promptStorage.removeItem(chatId);
await eventSource.emit(event_types.ITEMIZED_PROMPTS_DELETED, { chatId: chatId, all: false });
} catch {
console.log('Error deleting itemized prompts for chat', chatId);
}
@@ -96,6 +100,7 @@ export async function clearItemizedPrompts() {
try {
await promptStorage.clear();
itemizedPrompts = [];
await eventSource.emit(event_types.ITEMIZED_PROMPTS_DELETED, { all: true });
} catch {
console.log('Error clearing itemized prompts');
}
+1
View File
@@ -202,6 +202,7 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
mirostat_eta: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_eta : undefined,
use_default_badwordsids: (kai_flags.can_use_default_badwordsids || isHorde) ? kai_settings.use_default_badwordsids : undefined,
grammar: (kai_flags.can_use_grammar || isHorde) ? substituteParams(kai_settings.grammar) : undefined,
grammar_retain_state: (kai_flags.can_use_grammar && !!isContinue) ? true : undefined,
sampler_seed: kai_settings.seed >= 0 ? kai_settings.seed : undefined,
api_server: kai_settings.api_server,
};
+1
View File
@@ -28,6 +28,7 @@ export function showLoader() {
// Create a blocking loader with no toast (matches old behavior)
legacyLoaderHandle = loader.show({
slug: 'legacy-loader',
blocking: true,
toastMode: loader.ToastMode.NONE,
});
+7 -6
View File
@@ -58,10 +58,11 @@ export class MacrosParser {
*
* @param {string} method
* @param {string} replacement
* @param {IArguments} [methodArgs=null]
* @returns {void}
*/
static #logDeprecated(method, replacement) {
console.warn(`[DEPRECATED] MacrosParser.${method} is deprecated and will be removed in a future version. Use ${replacement} instead.`);
static #logDeprecated(method, replacement, methodArgs = null) {
console.warn(`[DEPRECATED] MacrosParser.${method} is deprecated and will be removed in a future version. Use ${replacement} instead. Arguments:`, (methodArgs ?? 'none'));
}
/**
@@ -155,7 +156,7 @@ export class MacrosParser {
* @returns {string|MacroFunction|undefined} The macro value
*/
static get(key) {
MacrosParser.#logDeprecated('get', 'macros.registry.getMacro (from scripts/macros/macro-system.js)');
MacrosParser.#logDeprecated('get', 'macros.registry.getMacro (from scripts/macros/macro-system.js)', arguments);
return MacrosParser.#macros.get(key);
}
@@ -165,7 +166,7 @@ export class MacrosParser {
* @returns {boolean} True if the macro is registered, false otherwise
*/
static has(key) {
MacrosParser.#logDeprecated('has', 'macros.registry.hasMacro (from scripts/macros/macro-system.js)');
MacrosParser.#logDeprecated('has', 'macros.registry.hasMacro (from scripts/macros/macro-system.js)', arguments);
if (power_user.experimental_macro_engine) {
return macroSystem.registry.hasMacro(key);
}
@@ -180,7 +181,7 @@ export class MacrosParser {
* @param {string} [description] Optional description of the macro
*/
static registerMacro(key, value, description = '') {
MacrosParser.#logDeprecated('registerMacro', 'macros.registry.registerMacro (from scripts/macros/macro-system.js) or substituteParams({ dynamicMacros })');
MacrosParser.#logDeprecated('registerMacro', 'macros.registry.registerMacro (from scripts/macros/macro-system.js) or substituteParams({ dynamicMacros })', arguments);
if (typeof key !== 'string') {
throw new Error('Macro key must be a string');
}
@@ -223,7 +224,7 @@ export class MacrosParser {
* @param {string} key Macro name (key)
*/
static unregisterMacro(key) {
MacrosParser.#logDeprecated('unregisterMacro', 'macros.registry.unregisterMacro (from scripts/macros/macro-system.js)');
MacrosParser.#logDeprecated('unregisterMacro', 'macros.registry.unregisterMacro (from scripts/macros/macro-system.js)', arguments);
if (typeof key !== 'string') {
throw new Error('Macro key must be a string');
}
+593 -340
View File
File diff suppressed because it is too large Load Diff
+1030 -68
View File
File diff suppressed because it is too large Load Diff
+54 -4
View File
@@ -2,7 +2,7 @@ import dialogPolyfill from '../lib/dialog-polyfill.esm.js';
import { shouldSendOnEnter } from './RossAscends-mods.js';
import { t } from './i18n.js';
import { power_user, toastPositionClasses } from './power-user.js';
import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
import { clamp, removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
/** @readonly */
/** @enum {Number} */
@@ -79,8 +79,12 @@ export const POPUP_RESULT = {
* @property {string} label - The label text for the input
* @property {string?} [tooltip=null] - Optional tooltip to be displayed. Default placeholder in input controls, tooltip icon behind the checkbox for those.
* @property {boolean|string|undefined} [defaultState=false] - The default state when opening the popup (false if not set)
* @property {('checkbox'|'text'|'textarea')?} [type='checkbox'] - The type of the input (default is checkbox)
* @property {('checkbox'|'text'|'textarea'|'number')?} [type='checkbox'] - The type of the input (default is checkbox)
* @property {number?} [rows=1] - The number of rows for the input field, if the input is 'textarea'
* @property {number?} [min] - The minimum value for number inputs
* @property {number?} [max] - The maximum value for number inputs
* @property {number?} [step] - The step value for number inputs
* @property {boolean?} [disabled=false] - Whether the input should be disabled
*/
/**
@@ -331,6 +335,7 @@ export class Popup {
inputElement.type = 'checkbox';
inputElement.id = input.id;
inputElement.checked = Boolean(input.defaultState ?? false);
inputElement.disabled = Boolean(input.disabled ?? false);
label.appendChild(inputElement);
const labelText = document.createElement('span');
labelText.innerText = input.label;
@@ -356,6 +361,7 @@ export class Popup {
inputElement.id = input.id;
inputElement.value = String(input.defaultState ?? '');
inputElement.placeholder = input.tooltip ?? '';
inputElement.disabled = Boolean(input.disabled ?? false);
setTitleFromTooltip(inputElement, input.tooltip);
const labelText = document.createElement('span');
@@ -377,6 +383,7 @@ export class Popup {
inputElement.value = String(input.defaultState ?? '');
inputElement.rows = input.rows ?? 1;
inputElement.placeholder = input.tooltip ?? '';
inputElement.disabled = Boolean(input.disabled ?? false);
setTitleFromTooltip(inputElement, input.tooltip);
const labelText = document.createElement('span');
@@ -386,9 +393,48 @@ export class Popup {
label.appendChild(labelText);
label.appendChild(inputElement);
this.inputControls.appendChild(label);
} else if (input.type === 'number') {
const label = document.createElement('label');
label.classList.add('text_label', 'justifyCenter');
label.setAttribute('for', input.id);
const inputElement = document.createElement('input');
inputElement.classList.add('text_pole', 'result-control');
inputElement.type = 'number';
inputElement.id = input.id;
inputElement.value = String(input.defaultState ?? '');
inputElement.placeholder = input.tooltip ?? '';
inputElement.min = String(input.min ?? '');
inputElement.max = String(input.max ?? '');
inputElement.step = String(input.step ?? '');
inputElement.disabled = Boolean(input.disabled ?? false);
setTitleFromTooltip(inputElement, input.tooltip);
inputElement.addEventListener('change', () => {
const value = parseFloat(inputElement.value);
if (isNaN(value)) return;
const min = Number.isFinite(input.min) ? input.min : -Infinity;
const max = Number.isFinite(input.max) ? input.max : Infinity;
const clamped = clamp(value, min, max);
if (clamped !== value) {
inputElement.value = String(clamped);
toastr.warning(t`Value must be between ${min} and ${max}. Clamped to ${clamped}.`);
}
});
const labelText = document.createElement('span');
labelText.innerText = input.label;
labelText.dataset.i18n = input.label;
label.appendChild(labelText);
label.appendChild(inputElement);
this.inputControls.appendChild(label);
} else {
console.warn('Unknown custom input type. Only checkbox, text and textarea are supported.', input);
console.warn('Unknown custom input type. Only checkbox, text, number and textarea are supported.', input);
return;
}
});
@@ -672,6 +718,10 @@ export class Popup {
}
}
if (!control) {
return;
}
if (applyAutoFocus) {
control.setAttribute('autofocus', '');
// Manually enable tabindex too, as this might only be applied by the interactable functionality in the background, but too late for HTML autofocus
@@ -720,7 +770,7 @@ export class Popup {
this.inputResults = new Map(this.customInputs.map(input => {
/** @type {HTMLInputElement} */
const inputControl = this.dlg.querySelector(`#${input.id}`);
const value = ['text', 'textarea'].includes(input.type) ? inputControl.value : inputControl.checked;
const value = ['text', 'textarea', 'number'].includes(input.type) ? inputControl.value : inputControl.checked;
return [inputControl.id, value];
}));
}
+2 -11
View File
@@ -68,6 +68,7 @@ import { bindModelTemplates } from './chat-templates.js';
import { IMAGE_OVERSWIPE, MEDIA_DISPLAY } from './constants.js';
import { t } from './i18n.js';
import { getBackgroundPath, isCustomBackgroundUrl } from './backgrounds.js';
import { persona_description_positions as _persona_description_positions } from './personas.js';
export const toastPositionClasses = [
'toast-top-left',
@@ -110,17 +111,7 @@ export const send_on_enter_options = {
ENABLED: 1,
};
export const persona_description_positions = {
IN_PROMPT: 0,
/**
* @deprecated Use persona_description_positions.IN_PROMPT instead.
*/
AFTER_CHAR: 1,
TOP_AN: 2,
BOTTOM_AN: 3,
AT_DEPTH: 4,
NONE: 9,
};
export const persona_description_positions = _persona_description_positions;
export const power_user = {
charListGrid: false,

Some files were not shown because too many files have changed in this diff Show More