@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
|
||||
"js/ts.tsdk.path": "./node_modules/typescript/lib"
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -500,6 +500,11 @@
|
||||
.horde_multiple_hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
max-height: 50px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/*landscape mode phones and ipads*/
|
||||
|
||||
@@ -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: '';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
.welcomePanel.recentHidden .welcomeRecent,
|
||||
.welcomePanel.recentHidden .recentChatsTitle,
|
||||
.welcomePanel.recentHidden .hideRecentChats,
|
||||
.welcomePanel.recentHidden .recentChatsSettings,
|
||||
.welcomePanel:not(.recentHidden) .showRecentChats {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -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" />
|
||||
</g>
|
||||
<?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 |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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. 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). 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-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">
|
||||
<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. 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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,18 +4473,24 @@ 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(
|
||||
coreChat[i].mes,
|
||||
getRegexedString(
|
||||
String(coreChat[i].extra?.reasoning ?? ''),
|
||||
regex_placement.REASONING,
|
||||
{ isPrompt: true, depth: depth },
|
||||
mes: isOtherGroupMember
|
||||
? coreChat[i].mes
|
||||
: promptReasoning.addToMessage(
|
||||
coreChat[i].mes,
|
||||
getRegexedString(
|
||||
String(coreChat[i].extra?.reasoning ?? ''),
|
||||
regex_placement.REASONING,
|
||||
{ isPrompt: true, depth: depth },
|
||||
),
|
||||
isPrefix,
|
||||
coreChat[i].extra?.reasoning_duration,
|
||||
),
|
||||
isPrefix,
|
||||
coreChat[i].extra?.reasoning_duration,
|
||||
),
|
||||
};
|
||||
if (promptReasoning.isLimitReached()) {
|
||||
break;
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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';
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = '');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,219 +60,262 @@ 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);
|
||||
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);
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
const pathSegments = parsedUrl.pathname.split('/').filter(s => s.length > 0);
|
||||
console.debug(DEBUG_PREFIX, 'Checking asset', asset.id, asset.url);
|
||||
|
||||
// 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}`;
|
||||
const assetInstall = async function () {
|
||||
element.off('click');
|
||||
label.removeClass('fa-download');
|
||||
this.classList.add('asset-download-button-loading');
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug(DEBUG_PREFIX, 'Error parsing URL:', error);
|
||||
label.addClass('fa-check');
|
||||
this.classList.remove('asset-download-button-loading');
|
||||
element.on('click', assetDelete);
|
||||
element.on('mouseenter', function () {
|
||||
label.removeClass('fa-check');
|
||||
label.addClass('fa-trash');
|
||||
label.addClass('redOverlayGlow');
|
||||
}).on('mouseleave', function () {
|
||||
label.addClass('fa-check');
|
||||
label.removeClass('fa-trash');
|
||||
label.removeClass('redOverlayGlow');
|
||||
});
|
||||
};
|
||||
|
||||
const assetDelete = async function () {
|
||||
if (assetType === 'character') {
|
||||
toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');
|
||||
await SlashCommandParser.commands.go.callback(null, asset.id);
|
||||
return;
|
||||
}
|
||||
element.off('click');
|
||||
await deleteAsset(assetType, asset.id);
|
||||
label.removeClass('fa-check');
|
||||
label.removeClass('redOverlayGlow');
|
||||
label.removeClass('fa-trash');
|
||||
label.addClass('fa-download');
|
||||
element.off('mouseenter').off('mouseleave');
|
||||
element.on('click', assetInstall);
|
||||
};
|
||||
|
||||
if (isAssetInstalled(assetType, asset.id)) {
|
||||
console.debug(DEBUG_PREFIX, 'installed, checked');
|
||||
label.toggleClass('fa-download');
|
||||
label.toggleClass('fa-check');
|
||||
element.on('click', assetDelete);
|
||||
element.on('mouseenter', function () {
|
||||
label.removeClass('fa-check');
|
||||
label.addClass('fa-trash');
|
||||
label.addClass('redOverlayGlow');
|
||||
}).on('mouseleave', function () {
|
||||
label.addClass('fa-check');
|
||||
label.removeClass('fa-trash');
|
||||
label.removeClass('redOverlayGlow');
|
||||
});
|
||||
} else {
|
||||
console.debug(DEBUG_PREFIX, 'not installed, unchecked');
|
||||
element.prop('checked', false);
|
||||
element.on('click', assetInstall);
|
||||
}
|
||||
|
||||
return result;
|
||||
return element;
|
||||
}
|
||||
|
||||
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();
|
||||
/**
|
||||
* 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);
|
||||
|
||||
console.debug(DEBUG_PREFIX, 'Received assets dictionary', json);
|
||||
const displayName = DOMPurify.sanitize(asset.name || asset.id);
|
||||
const description = DOMPurify.sanitize(asset.description || '');
|
||||
const url = isValidUrl(asset.url) ? asset.url : '';
|
||||
const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
|
||||
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
|
||||
const toolTag = assetType === 'extension' && asset.tool;
|
||||
const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR;
|
||||
|
||||
for (const i of json) {
|
||||
//console.log(DEBUG_PREFIX,i)
|
||||
if (availableAssets[i.type] === undefined)
|
||||
availableAssets[i.type] = [];
|
||||
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}` })));
|
||||
|
||||
availableAssets[i.type].push(i);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
nameSpan.append($('<span>', { class: 'expander' }));
|
||||
|
||||
$('#assets_type_select').empty();
|
||||
$('#assets_search').val('');
|
||||
$('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
|
||||
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)));
|
||||
}
|
||||
|
||||
for (const type of assetTypes) {
|
||||
const text = translate(KNOWN_TYPES[type] || type);
|
||||
const option = $('<option />', { value: type, text: text });
|
||||
$('#assets_type_select').append(option);
|
||||
}
|
||||
const infoDiv = $('<div>', { class: 'flex-container flexFlowColumn flexNoGap wide100p overflowHidden' })
|
||||
.append(nameSpan)
|
||||
.append($('<small>', { class: 'asset-description' }).text(description));
|
||||
|
||||
if (assetTypes.includes('extension')) {
|
||||
$('#assets_type_select').val('extension');
|
||||
}
|
||||
const assetBlock = $('<i></i>').append(element).append(infoDiv);
|
||||
|
||||
$('#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' });
|
||||
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);
|
||||
label.addClass('fa-check');
|
||||
this.classList.remove('asset-download-button-loading');
|
||||
element.on('click', assetDelete);
|
||||
element.on('mouseenter', function () {
|
||||
label.removeClass('fa-check');
|
||||
label.addClass('fa-trash');
|
||||
label.addClass('redOverlayGlow');
|
||||
}).on('mouseleave', function () {
|
||||
label.addClass('fa-check');
|
||||
label.removeClass('fa-trash');
|
||||
label.removeClass('redOverlayGlow');
|
||||
});
|
||||
};
|
||||
|
||||
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}`);
|
||||
return;
|
||||
}
|
||||
element.off('click');
|
||||
await deleteAsset(assetType, asset.id);
|
||||
label.removeClass('fa-check');
|
||||
label.removeClass('redOverlayGlow');
|
||||
label.removeClass('fa-trash');
|
||||
label.addClass('fa-download');
|
||||
element.off('mouseenter').off('mouseleave');
|
||||
element.on('click', assetInstall);
|
||||
};
|
||||
|
||||
if (isAssetInstalled(assetType, asset.id)) {
|
||||
console.debug(DEBUG_PREFIX, 'installed, checked');
|
||||
label.toggleClass('fa-download');
|
||||
label.toggleClass('fa-check');
|
||||
element.on('click', assetDelete);
|
||||
element.on('mouseenter', function () {
|
||||
label.removeClass('fa-check');
|
||||
label.addClass('fa-trash');
|
||||
label.addClass('redOverlayGlow');
|
||||
}).on('mouseleave', function () {
|
||||
label.addClass('fa-check');
|
||||
label.removeClass('fa-trash');
|
||||
label.removeClass('redOverlayGlow');
|
||||
});
|
||||
} else {
|
||||
console.debug(DEBUG_PREFIX, 'not installed, unchecked');
|
||||
element.prop('checked', false);
|
||||
element.on('click', assetInstall);
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX, 'Created element for ', asset.id);
|
||||
|
||||
const displayName = DOMPurify.sanitize(asset.name || asset.id);
|
||||
const description = DOMPurify.sanitize(asset.description || '');
|
||||
const url = isValidUrl(asset.url) ? asset.url : '';
|
||||
const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
|
||||
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
|
||||
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>`);
|
||||
|
||||
assetBlock.find('.tag').on('click', function (e) {
|
||||
const a = document.createElement('a');
|
||||
a.href = 'https://docs.sillytavern.app/for-contributors/function-calling/';
|
||||
a.target = '_blank';
|
||||
a.click();
|
||||
});
|
||||
|
||||
if (assetType === 'character') {
|
||||
if (asset.highlight) {
|
||||
assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
|
||||
}
|
||||
assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset.url}" alt="${displayName}"></div>`);
|
||||
}
|
||||
|
||||
assetBlock.addClass('asset-block');
|
||||
|
||||
assetTypeMenu.append(assetBlock);
|
||||
}
|
||||
assetTypeMenu.appendTo('#assets_menu');
|
||||
assetTypeMenu.on('click', 'a.asset_preview', previewAsset);
|
||||
}
|
||||
|
||||
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
|
||||
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');
|
||||
$('#assets-connect-button').addClass('fa-plug-circle-exclamation');
|
||||
$('#assets-connect-button').addClass('redOverlayGlow');
|
||||
});
|
||||
assetBlock.find('.tag').on('click', function (e) {
|
||||
const a = document.createElement('a');
|
||||
a.href = 'https://docs.sillytavern.app/for-contributors/function-calling/';
|
||||
a.target = '_blank';
|
||||
a.click();
|
||||
});
|
||||
|
||||
if (assetType === 'character') {
|
||||
if (asset.highlight) {
|
||||
nameSpan.append($('<i>', { class: 'fa-solid fa-sm fa-trophy' }));
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', '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);
|
||||
downloadButton.hide();
|
||||
checkMark.show();
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,5 +11,8 @@
|
||||
"css": "style.css",
|
||||
"author": "Ouoertheo#7264",
|
||||
"version": "1.0.0",
|
||||
"homePage": "None"
|
||||
"homePage": "None",
|
||||
"hooks": {
|
||||
"activate": "init"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* 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 summarizeOne(element, endpoint) {
|
||||
switch (endpoint) {
|
||||
case 'main':
|
||||
return await summarizeMain(element);
|
||||
case 'extras':
|
||||
return await summarizeExtra(element);
|
||||
case 'webllm':
|
||||
return await summarizeWebLLM(element);
|
||||
default:
|
||||
throw new Error(`Unsupported summary endpoint: ${endpoint}`, { cause: 'summary_endpoint_invalid' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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') {
|
||||
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) {
|
||||
let success = true;
|
||||
switch (endpoint) {
|
||||
case 'main':
|
||||
success = await summarizeMain(element);
|
||||
break;
|
||||
case 'extras':
|
||||
success = await summarizeExtra(element);
|
||||
break;
|
||||
case 'webllm':
|
||||
success = await summarizeWebLLM(element);
|
||||
break;
|
||||
default:
|
||||
console.error('Unsupported endpoint', endpoint);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
if (success) {
|
||||
cachedSummaries.set(element.hash, element.text);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
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}...`);
|
||||
await insertVectorItems(chatId, chunkedBatch);
|
||||
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':
|
||||
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([]);
|
||||
if (settings.source === 'webllm') {
|
||||
loadWebLlmModels();
|
||||
} 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');
|
||||
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);
|
||||
async function loadRemoteEmbeddingModels(source) {
|
||||
const config = remoteEmbeddingEndpoints[source];
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
if (!settings.electronhub_model && models.length) {
|
||||
settings.electronhub_model = models[0].id;
|
||||
}
|
||||
$('#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}`);
|
||||
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[valueProperty];
|
||||
option.text = textProperty ? (m[textProperty] || m[valueProperty]) : m[valueProperty];
|
||||
select.append(option);
|
||||
}
|
||||
/** @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([]);
|
||||
if (!settings[settingsKey] && models.length) {
|
||||
settings[settingsKey] = models[0][valueProperty];
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
select.val(settings[settingsKey]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', {
|
||||
const body = typeof getBody === 'function' ? getBody() : {};
|
||||
|
||||
/** @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.">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||