Merge branch 'staging' into stats-2.0
@@ -4,6 +4,7 @@ npm-debug.log
|
||||
readme*
|
||||
Start.bat
|
||||
/dist
|
||||
/backups/
|
||||
/backups
|
||||
cloudflared.exe
|
||||
access.log
|
||||
/data
|
||||
|
12
.eslintrc.js
@@ -42,11 +42,21 @@ module.exports = {
|
||||
showdownKatex: 'readonly',
|
||||
SVGInject: 'readonly',
|
||||
toastr: 'readonly',
|
||||
Readability: 'readonly',
|
||||
isProbablyReaderable: 'readonly',
|
||||
},
|
||||
},
|
||||
],
|
||||
// There are various vendored libraries that shouldn't be linted
|
||||
ignorePatterns: ['public/lib/**/*', '*.min.js', 'src/ai_horde/**/*'],
|
||||
ignorePatterns: [
|
||||
'public/lib/**/*',
|
||||
'*.min.js',
|
||||
'src/ai_horde/**/*',
|
||||
'plugins/**/*',
|
||||
'data/**/*',
|
||||
'backups/**/*',
|
||||
'node_modules/**/*',
|
||||
],
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'no-control-regex': 'off',
|
||||
|
1
.gitignore
vendored
@@ -25,6 +25,7 @@ public/stats.json
|
||||
/docker/config
|
||||
/docker/user
|
||||
/docker/extensions
|
||||
/docker/data
|
||||
.DS_Store
|
||||
public/settings.json
|
||||
/thumbnails
|
||||
|
16
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:19.1.0-alpine3.16
|
||||
FROM node:lts-alpine3.18
|
||||
|
||||
# Arguments
|
||||
ARG APP_HOME=/home/node/app
|
||||
@@ -26,19 +26,9 @@ COPY . ./
|
||||
|
||||
# Copy default chats, characters and user avatars to <folder>.default folder
|
||||
RUN \
|
||||
IFS="," RESOURCES="assets,backgrounds,user,context,instruct,QuickReplies,movingUI,themes,characters,chats,groups,group chats,User Avatars,worlds,OpenAI Settings,NovelAI Settings,KoboldAI Settings,TextGen Settings" && \
|
||||
\
|
||||
echo "*** Store default $RESOURCES in <folder>.default ***" && \
|
||||
for R in $RESOURCES; do mv "public/$R" "public/$R.default"; done || true && \
|
||||
\
|
||||
echo "*** Create symbolic links to config directory ***" && \
|
||||
for R in $RESOURCES; do ln -s "../config/$R" "public/$R"; done || true && \
|
||||
\
|
||||
rm -f "config.yaml" "public/settings.json" || true && \
|
||||
rm -f "config.yaml" || true && \
|
||||
ln -s "./config/config.yaml" "config.yaml" || true && \
|
||||
ln -s "../config/settings.json" "public/settings.json" || true && \
|
||||
mkdir "config" || true && \
|
||||
mkdir -p "public/user" || true
|
||||
mkdir "config" || true
|
||||
|
||||
# Cleanup unnecessary files
|
||||
RUN \
|
||||
|
@@ -1,8 +1,12 @@
|
||||
# -- NETWORK CONFIGURATION --
|
||||
# -- DATA CONFIGURATION --
|
||||
# Root directory for user data storage
|
||||
dataRoot: ./data
|
||||
# -- SERVER CONFIGURATION --
|
||||
# Listen for incoming connections
|
||||
listen: false
|
||||
# Server port
|
||||
port: 8000
|
||||
# -- SECURITY CONFIGURATION --
|
||||
# Toggle whitelist mode
|
||||
whitelistMode: true
|
||||
# Whitelist of allowed IP addresses
|
||||
@@ -16,7 +20,15 @@ basicAuthUser:
|
||||
password: "password"
|
||||
# Enables CORS proxy middleware
|
||||
enableCorsProxy: false
|
||||
# Disable security checks - NOT RECOMMENDED
|
||||
# Enable multi-user mode
|
||||
enableUserAccounts: false
|
||||
# Enable discreet login mode: hides user list on the login screen
|
||||
enableDiscreetLogin: false
|
||||
# Used to sign session cookies. Will be auto-generated if not set
|
||||
cookieSecret: ''
|
||||
# Disable CSRF protection - NOT RECOMMENDED
|
||||
disableCsrfProtection: false
|
||||
# Disable startup security checks - NOT RECOMMENDED
|
||||
securityOverride: false
|
||||
# -- ADVANCED CONFIGURATION --
|
||||
# Open the browser automatically
|
||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 384 KiB |
Before Width: | Height: | Size: 487 KiB After Width: | Height: | Size: 487 KiB |
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 307 KiB |
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 318 KiB |
Before Width: | Height: | Size: 581 KiB After Width: | Height: | Size: 581 KiB |
Before Width: | Height: | Size: 561 KiB After Width: | Height: | Size: 561 KiB |
Before Width: | Height: | Size: 505 KiB After Width: | Height: | Size: 505 KiB |
Before Width: | Height: | Size: 501 KiB After Width: | Height: | Size: 501 KiB |
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 443 KiB |
Before Width: | Height: | Size: 480 KiB After Width: | Height: | Size: 480 KiB |
Before Width: | Height: | Size: 660 KiB After Width: | Height: | Size: 660 KiB |
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 371 KiB |
Before Width: | Height: | Size: 616 KiB After Width: | Height: | Size: 616 KiB |
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 305 KiB |
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 436 KiB |
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
Before Width: | Height: | Size: 629 KiB After Width: | Height: | Size: 629 KiB |
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 656 KiB |
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 528 KiB |
@@ -1,4 +1,108 @@
|
||||
[
|
||||
{
|
||||
"filename": "settings.json",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"filename": "themes/Dark Lite.json",
|
||||
"type": "theme"
|
||||
},
|
||||
{
|
||||
"filename": "themes/Cappuccino.json",
|
||||
"type": "theme"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/__transparent.png",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/_black.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/_white.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/bedroom clean.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/bedroom cyberpunk.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/bedroom red.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/bedroom tatami.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/cityscape medieval market.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/cityscape medieval night.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/cityscape postapoc.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/forest treehouse fireworks air baloons (by kallmeflocc).jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/japan classroom side.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/japan classroom.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/japan path cherry blossom.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/japan university.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape autumn great tree.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape beach day.png",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape beach night.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape mountain lake.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape postapoc.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/landscape winter lake house.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/royal.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/tavern day.jpg",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "default_Seraphina.png",
|
||||
"type": "character"
|
||||
@@ -211,7 +315,6 @@
|
||||
"filename": "presets/novel/Writers-Daemon-Kayra.json",
|
||||
"type": "novel_preset"
|
||||
},
|
||||
|
||||
{
|
||||
"filename": "presets/textgen/Asterism.json",
|
||||
"type": "textgen_preset"
|
||||
@@ -527,5 +630,17 @@
|
||||
{
|
||||
"filename": "presets/instruct/Llama 3 Instruct.json",
|
||||
"type": "instruct"
|
||||
},
|
||||
{
|
||||
"filename": "presets/moving-ui/Default.json",
|
||||
"type": "moving_ui"
|
||||
},
|
||||
{
|
||||
"filename": "presets/moving-ui/Black Magic Time.json",
|
||||
"type": "moving_ui"
|
||||
},
|
||||
{
|
||||
"filename": "presets/quick-replies/Default.json",
|
||||
"type": "quick_replies"
|
||||
}
|
||||
]
|
||||
|
@@ -18,7 +18,7 @@
|
||||
"input_suffix": "<|eot_id|>",
|
||||
"system_suffix": "<|eot_id|>",
|
||||
"user_alignment_message": "",
|
||||
"system_same_as_user": false,
|
||||
"system_same_as_user": true,
|
||||
"last_system_sequence": "",
|
||||
"name": "Llama 3 Instruct"
|
||||
}
|
||||
|
@@ -95,7 +95,7 @@
|
||||
"user_prompt_bias": "",
|
||||
"show_user_prompt_bias": true,
|
||||
"markdown_escape_strings": "",
|
||||
"fast_ui_mode": false,
|
||||
"fast_ui_mode": true,
|
||||
"avatar_style": 0,
|
||||
"chat_display": 0,
|
||||
"chat_width": 50,
|
||||
@@ -115,16 +115,17 @@
|
||||
"italics_text_color": "rgba(145, 145, 145, 1)",
|
||||
"underline_text_color": "rgba(188, 231, 207, 1)",
|
||||
"quote_text_color": "rgba(225, 138, 36, 1)",
|
||||
"chat_tint_color": "rgba(23, 23, 23, 1)",
|
||||
"blur_tint_color": "rgba(23, 23, 23, 1)",
|
||||
"user_mes_blur_tint_color": "rgba(0, 0, 0, 0.9)",
|
||||
"bot_mes_blur_tint_color": "rgba(0, 0, 0, 0.9)",
|
||||
"user_mes_blur_tint_color": "rgba(30, 30, 30, 0.9)",
|
||||
"bot_mes_blur_tint_color": "rgba(30, 30, 30, 0.9)",
|
||||
"shadow_color": "rgba(0, 0, 0, 1)",
|
||||
"waifuMode": false,
|
||||
"movingUI": false,
|
||||
"movingUIState": {},
|
||||
"movingUIPreset": "Default",
|
||||
"noShadows": true,
|
||||
"theme": "Default (Dark) 1.7.1",
|
||||
"theme": "Dark Lite",
|
||||
"auto_swipe": false,
|
||||
"auto_swipe_minimum_length": 0,
|
||||
"auto_swipe_blacklist": [],
|
||||
@@ -139,7 +140,7 @@
|
||||
"hotswap_enabled": true,
|
||||
"timer_enabled": false,
|
||||
"timestamps_enabled": true,
|
||||
"timestamp_model_icon": false,
|
||||
"timestamp_model_icon": true,
|
||||
"mesIDDisplay_enabled": false,
|
||||
"max_context_unlocked": false,
|
||||
"prefer_character_prompt": true,
|
||||
@@ -193,7 +194,8 @@
|
||||
"encode_tags": false,
|
||||
"enableLabMode": false,
|
||||
"enableZenSliders": false,
|
||||
"ui_mode": 1
|
||||
"ui_mode": 1,
|
||||
"forbid_external_media": true
|
||||
},
|
||||
"extension_settings": {
|
||||
"apiUrl": "http://localhost:5100",
|
35
default/content/themes/Cappuccino.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Cappuccino",
|
||||
"blur_strength": 3,
|
||||
"main_text_color": "rgba(255, 255, 255, 1)",
|
||||
"italics_text_color": "rgba(230, 210, 190, 1)",
|
||||
"underline_text_color": "rgba(205, 180, 160, 1)",
|
||||
"quote_text_color": "rgba(165, 140, 115, 1)",
|
||||
"blur_tint_color": "rgba(34, 30, 32, 0.95)",
|
||||
"chat_tint_color": "rgba(50, 45, 50, 0.75)",
|
||||
"user_mes_blur_tint_color": "rgba(34, 30, 32, 0.75)",
|
||||
"bot_mes_blur_tint_color": "rgba(34, 30, 32, 0.75)",
|
||||
"shadow_color": "rgba(0, 0, 0, 0.3)",
|
||||
"shadow_width": 1,
|
||||
"border_color": "rgba(80, 80, 80, 0.89)",
|
||||
"font_scale": 1,
|
||||
"fast_ui_mode": false,
|
||||
"waifuMode": false,
|
||||
"avatar_style": 0,
|
||||
"chat_display": 1,
|
||||
"noShadows": false,
|
||||
"chat_width": 50,
|
||||
"timer_enabled": false,
|
||||
"timestamps_enabled": true,
|
||||
"timestamp_model_icon": true,
|
||||
"mesIDDisplay_enabled": true,
|
||||
"message_token_count_enabled": false,
|
||||
"expand_message_actions": false,
|
||||
"enableZenSliders": false,
|
||||
"enableLabMode": false,
|
||||
"hotswap_enabled": true,
|
||||
"custom_css": "",
|
||||
"bogus_folders": true,
|
||||
"reduced_motion": false,
|
||||
"compact_input_area": true
|
||||
}
|
35
default/content/themes/Dark Lite.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Dark Lite",
|
||||
"blur_strength": 10,
|
||||
"main_text_color": "rgba(220, 220, 210, 1)",
|
||||
"italics_text_color": "rgba(145, 145, 145, 1)",
|
||||
"underline_text_color": "rgba(188, 231, 207, 1)",
|
||||
"quote_text_color": "rgba(225, 138, 36, 1)",
|
||||
"blur_tint_color": "rgba(23, 23, 23, 1)",
|
||||
"chat_tint_color": "rgba(23, 23, 23, 1)",
|
||||
"user_mes_blur_tint_color": "rgba(30, 30, 30, 0.9)",
|
||||
"bot_mes_blur_tint_color": "rgba(30, 30, 30, 0.9)",
|
||||
"shadow_color": "rgba(0, 0, 0, 1)",
|
||||
"shadow_width": 2,
|
||||
"border_color": "rgba(0, 0, 0, 1)",
|
||||
"font_scale": 1,
|
||||
"fast_ui_mode": true,
|
||||
"waifuMode": false,
|
||||
"avatar_style": 0,
|
||||
"chat_display": 0,
|
||||
"noShadows": true,
|
||||
"chat_width": 50,
|
||||
"timer_enabled": false,
|
||||
"timestamps_enabled": true,
|
||||
"timestamp_model_icon": true,
|
||||
"mesIDDisplay_enabled": false,
|
||||
"message_token_count_enabled": false,
|
||||
"expand_message_actions": false,
|
||||
"enableZenSliders": "",
|
||||
"enableLabMode": "",
|
||||
"hotswap_enabled": true,
|
||||
"custom_css": "",
|
||||
"bogus_folders": true,
|
||||
"reduced_motion": false,
|
||||
"compact_input_area": true
|
||||
}
|
@@ -8,7 +8,6 @@ services:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- "./extensions:/home/node/app/public/scripts/extensions/third-party"
|
||||
- "./config:/home/node/app/config"
|
||||
- "./user:/home/node/app/public/user"
|
||||
- "./data:/home/node/app/data"
|
||||
restart: unless-stopped
|
||||
|
@@ -1,38 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Initialize missing user files
|
||||
IFS="," RESOURCES="assets,backgrounds,user,context,instruct,QuickReplies,movingUI,themes,characters,chats,groups,group chats,User Avatars,worlds,OpenAI Settings,NovelAI Settings,KoboldAI Settings,TextGen Settings"
|
||||
for R in $RESOURCES; do
|
||||
if [ ! -e "config/$R" ]; then
|
||||
echo "Resource not found, copying from defaults: $R"
|
||||
cp -r "public/$R.default" "config/$R"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -e "config/config.yaml" ]; then
|
||||
echo "Resource not found, copying from defaults: config.yaml"
|
||||
cp -r "default/config.yaml" "config/config.yaml"
|
||||
fi
|
||||
|
||||
if [ ! -e "config/settings.json" ]; then
|
||||
echo "Resource not found, copying from defaults: settings.json"
|
||||
cp -r "default/settings.json" "config/settings.json"
|
||||
fi
|
||||
|
||||
CONFIG_FILE="config.yaml"
|
||||
|
||||
echo "Starting with the following config:"
|
||||
cat $CONFIG_FILE
|
||||
|
||||
if grep -q "listen: false" $CONFIG_FILE; then
|
||||
echo -e "\033[1;31mThe listen parameter is set to false. If you can't connect to the server, edit the \"docker/config/config.yaml\" file and restart the container.\033[0m"
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
if grep -q "whitelistMode: true" $CONFIG_FILE; then
|
||||
echo -e "\033[1;31mThe whitelistMode parameter is set to true. If you can't connect to the server, edit the \"docker/config/config.yaml\" file and restart the container.\033[0m"
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Start the server
|
||||
exec node server.js
|
||||
exec node server.js --listen
|
||||
|
20
index.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { UserDirectoryList, User } from "./src/users";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
export interface Request {
|
||||
user: {
|
||||
profile: User;
|
||||
directories: UserDirectoryList;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'express-session' {
|
||||
export interface SessionData {
|
||||
handle: string;
|
||||
touch: number;
|
||||
// other properties...
|
||||
}
|
||||
}
|
@@ -12,6 +12,9 @@
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
"**/node_modules/*",
|
||||
"public/lib",
|
||||
"backups/*",
|
||||
"data/*"
|
||||
]
|
||||
}
|
947
package-lock.json
generated
@@ -4,17 +4,21 @@
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@dqbd/tiktoken": "^1.0.13",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"command-exists": "^1.2.9",
|
||||
"compression": "^1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cookie-session": "^2.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"csrf-csrf": "^2.2.3",
|
||||
"express": "^4.19.2",
|
||||
"form-data": "^4.0.0",
|
||||
"google-translate-api-browser": "^3.0.1",
|
||||
"gpt3-tokenizer": "^1.1.5",
|
||||
"he": "^1.2.0",
|
||||
"helmet": "^7.1.0",
|
||||
"ip-matching": "^2.1.2",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jimp": "^0.22.10",
|
||||
@@ -22,10 +26,12 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-fetch": "^2.6.11",
|
||||
"node-persist": "^4.0.1",
|
||||
"open": "^8.4.2",
|
||||
"png-chunk-text": "^1.0.0",
|
||||
"png-chunks-encode": "^1.0.0",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"rate-limiter-flexible": "^5.0.0",
|
||||
"response-time": "^2.3.2",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sillytavern-transformers": "^2.14.6",
|
||||
@@ -62,7 +68,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/SillyTavern/SillyTavern.git"
|
||||
},
|
||||
"version": "1.11.8",
|
||||
"version": "1.12.0-preview",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start-multi": "node server.js --disableCsrf",
|
||||
@@ -79,6 +85,7 @@
|
||||
},
|
||||
"main": "server.js",
|
||||
"devDependencies": {
|
||||
"@types/jquery": "^3.5.29",
|
||||
"eslint": "^8.55.0",
|
||||
"jquery": "^3.6.4"
|
||||
}
|
||||
|
@@ -60,7 +60,8 @@ function convertConfig() {
|
||||
try {
|
||||
console.log(color.blue('Converting config.conf to config.yaml. Your old config.conf will be renamed to config.conf.bak'));
|
||||
const config = require(path.join(process.cwd(), './config.conf'));
|
||||
fs.renameSync('./config.conf', './config.conf.bak');
|
||||
fs.copyFileSync('./config.conf', './config.conf.bak');
|
||||
fs.rmSync('./config.conf');
|
||||
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) {
|
||||
@@ -106,7 +107,6 @@ function addMissingConfigValues() {
|
||||
*/
|
||||
function createDefaultFiles() {
|
||||
const files = {
|
||||
settings: './public/settings.json',
|
||||
config: './config.yaml',
|
||||
user: './public/css/user.css',
|
||||
};
|
||||
@@ -167,29 +167,6 @@ function copyWasmFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the custom background into settings.json.
|
||||
*/
|
||||
function migrateBackground() {
|
||||
if (!fs.existsSync('./public/css/bg_load.css')) return;
|
||||
|
||||
const bgCSS = fs.readFileSync('./public/css/bg_load.css', 'utf-8');
|
||||
const bgMatch = /url\('([^']*)'\)/.exec(bgCSS);
|
||||
if (!bgMatch) return;
|
||||
const bgFilename = bgMatch[1].replace('../backgrounds/', '');
|
||||
|
||||
const settings = fs.readFileSync('./public/settings.json', 'utf-8');
|
||||
const settingsJSON = JSON.parse(settings);
|
||||
if (Object.hasOwn(settingsJSON, 'background')) {
|
||||
console.log(color.yellow('Both bg_load.css and the "background" setting exist. Please delete bg_load.css manually.'));
|
||||
return;
|
||||
}
|
||||
|
||||
settingsJSON.background = { name: bgFilename, url: `url('backgrounds/${bgFilename}')` };
|
||||
fs.writeFileSync('./public/settings.json', JSON.stringify(settingsJSON, null, 4));
|
||||
fs.rmSync('./public/css/bg_load.css');
|
||||
}
|
||||
|
||||
try {
|
||||
// 0. Convert config.conf to config.yaml
|
||||
convertConfig();
|
||||
@@ -199,8 +176,6 @@ try {
|
||||
copyWasmFiles();
|
||||
// 3. Add missing config values
|
||||
addMissingConfigValues();
|
||||
// 4. Migrate bg_load.css to settings.json
|
||||
migrateBackground();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
# Put images here to select them as a user persona avatar.
|
@@ -1 +0,0 @@
|
||||
Put ambient audio files here.
|
@@ -1 +0,0 @@
|
||||
Put bgm audio files here
|
@@ -1 +0,0 @@
|
||||
Put blip audio files here
|
@@ -1 +0,0 @@
|
||||
Put live2d model folders here
|
@@ -1 +0,0 @@
|
||||
Put VRM animation files here
|
@@ -1 +0,0 @@
|
||||
Put VRM model files here
|
@@ -1,8 +0,0 @@
|
||||
# Put PNG character cards here.
|
||||
|
||||
To create a sprites folder, name it the same as your character (NOT the PNG file).
|
||||
|
||||
For example:
|
||||
|
||||
- Character: /characters/Asuka Langley.png
|
||||
- Sprite: /characters/Asuka Langley/joy.png
|
@@ -1,5 +0,0 @@
|
||||
# Put Chat JSONL files here in subfolders corresponding to character names
|
||||
|
||||
For example:
|
||||
|
||||
- /chats/Robot/chat.jsonl
|
5
public/css/accounts.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.userAccount {
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
44
public/css/login.css
Normal file
@@ -0,0 +1,44 @@
|
||||
body.login #shadow_popup {
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.login .logo {
|
||||
max-width: 30px;
|
||||
}
|
||||
|
||||
body.login #logoBlock {
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.login .userSelect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 5px;
|
||||
padding: 3px 5px;
|
||||
width: 30%;
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.login .userSelect .userName,
|
||||
body.login .userSelect .userHandle {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body.login .userSelect:hover {
|
||||
background-color: var(--black30a);
|
||||
}
|
@@ -86,6 +86,10 @@
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.marginLeft5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.overflowYAuto {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -98,6 +102,14 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justifySpaceEvenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.justifySpaceAround {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.alignitemsflexstart {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
@@ -249,6 +261,10 @@
|
||||
flex-basis: 48%
|
||||
}
|
||||
|
||||
.flexBasis30p {
|
||||
flex-basis: 30%;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
@@ -483,6 +499,10 @@ textarea:disabled {
|
||||
font-size: calc(var(--mainFontSize) * 1.2) !important;
|
||||
}
|
||||
|
||||
.fontsize90p {
|
||||
font-size: calc(var(--mainFontSize) * 0.9) !important;
|
||||
}
|
||||
|
||||
.fontsize80p {
|
||||
font-size: calc(var(--mainFontSize) * 0.8) !important;
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
# Put Group Chat JSONL files here
|
@@ -1 +0,0 @@
|
||||
# Put Group JSON files here
|
BIN
public/img/logo.png
Normal file
After Width: | Height: | Size: 23 KiB |
3
public/img/perplexity.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M101.008 42L190.99 124.905L190.99 124.886L190.99 42.1913H208.506L208.506 125.276L298.891 42V136.524L336 136.524V272.866H299.005V357.035L208.506 277.525L208.506 357.948H190.99L190.99 278.836L101.11 358V272.866H64V136.524H101.008V42ZM177.785 153.826H81.5159V255.564H101.088V223.472L177.785 153.826ZM118.625 231.149V319.392L190.99 255.655L190.99 165.421L118.625 231.149ZM209.01 254.812V165.336L281.396 231.068V272.866H281.489V318.491L209.01 254.812ZM299.005 255.564H318.484V153.826L222.932 153.826L299.005 222.751V255.564ZM281.375 136.524V81.7983L221.977 136.524L281.375 136.524ZM177.921 136.524H118.524V81.7983L177.921 136.524Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 786 B |
@@ -90,6 +90,7 @@
|
||||
<script type="module" src="scripts/bulk-edit.js"></script>
|
||||
<script type="module" src="scripts/cfg-scale.js"></script>
|
||||
<script type="module" src="scripts/chats.js"></script>
|
||||
<script type="module" src="scripts/user.js"></script>
|
||||
<title>SillyTavern</title>
|
||||
</head>
|
||||
|
||||
@@ -141,10 +142,10 @@
|
||||
</a>
|
||||
</h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset" data-preset-manager-for="kobold" class="flex1 flexBasis100p text_pole">
|
||||
<select id="settings_preset" data-preset-manager-for="kobold" class="flex1 text_pole">
|
||||
<option value="gui" data-i18n="guikoboldaisettings">GUI KoboldAI Settings</option>
|
||||
</select>
|
||||
<div class="flex-container flexBasis100p justifyCenter">
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input type="file" hidden data-preset-manager-file="kobold" accept=".json, .settings">
|
||||
<i data-newbie-hidden data-preset-manager-update="kobold" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
|
||||
<i data-newbie-hidden data-preset-manager-new="kobold" class="menu_button fa-solid fa-file-circle-plus" title="Save preset as" data-i18n="[title]Save preset as"></i>
|
||||
@@ -163,10 +164,10 @@
|
||||
</a>
|
||||
</h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_novel" class="flex1 flexBasis100p text_pole" data-preset-manager-for="novel">
|
||||
<select id="settings_preset_novel" class="flex1 text_pole" data-preset-manager-for="novel">
|
||||
<option value="gui" data-i18n="default">Default</option>
|
||||
</select>
|
||||
<div class="flex-container flexBasis100p justifyCenter">
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input type="file" hidden data-preset-manager-file="novel" accept=".json, .settings">
|
||||
<i data-newbie-hidden data-preset-manager-update="novel" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
|
||||
<i data-newbie-hidden data-preset-manager-new="novel" class="menu_button fa-solid fa-file-circle-plus" title="Save preset as" data-i18n="[title]Save preset as"></i>
|
||||
@@ -184,7 +185,7 @@
|
||||
<select id="settings_preset_openai" class="flex1 text_pole" data-preset-manager-for="openai">
|
||||
<option value="gui" data-i18n="default">Default</option>
|
||||
</select>
|
||||
<div class="flex-container flexBasis100p justifyCenter">
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input id="openai_preset_import_file" type="file" accept=".json,.settings" hidden />
|
||||
<i id="update_oai_preset" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
|
||||
<i id="new_oai_preset" class="menu_button fa-solid fa-file-circle-plus" title="Save preset as" data-i18n="[title]Save preset as"></i>
|
||||
@@ -200,7 +201,7 @@
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_textgenerationwebui" class="flex1 text_pole" data-preset-manager-for="textgenerationwebui">
|
||||
</select>
|
||||
<div class="flex-container flexBasis100p justifyCenter">
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input type="file" hidden data-preset-manager-file="textgenerationwebui" accept=".json, .settings">
|
||||
<i data-newbie-hidden data-preset-manager-update="textgenerationwebui" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
|
||||
<i data-newbie-hidden data-preset-manager-new="textgenerationwebui" class="menu_button fa-solid fa-file-circle-plus" title="Save preset as" data-i18n="[title]Save preset as"></i>
|
||||
@@ -321,7 +322,7 @@
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block">
|
||||
<div class="range-block-title" data-i18n="Rep. Pen. Range.">
|
||||
Repetition Penalty Range
|
||||
Rep Pen Range
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
@@ -373,7 +374,7 @@
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block">
|
||||
<div class="range-block-title" data-i18n="Tail Free Sampling">
|
||||
Tail Free Sampling
|
||||
TFS
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
@@ -471,7 +472,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere">
|
||||
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity">
|
||||
<div class="range-block-title" data-i18n="Temperature">
|
||||
Temperature
|
||||
</div>
|
||||
@@ -484,7 +485,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere">
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere,perplexity">
|
||||
<div class="range-block-title" data-i18n="Frequency Penalty">
|
||||
Frequency Penalty
|
||||
</div>
|
||||
@@ -497,7 +498,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere">
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere,perplexity">
|
||||
<div class="range-block-title" data-i18n="Presence Penalty">
|
||||
Presence Penalty
|
||||
</div>
|
||||
@@ -523,7 +524,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="claude,openrouter,ai21,makersuite,cohere">
|
||||
<div data-newbie-hidden class="range-block" data-source="claude,openrouter,ai21,makersuite,cohere,perplexity">
|
||||
<div class="range-block-title" data-i18n="Top K">
|
||||
Top K
|
||||
</div>
|
||||
@@ -536,7 +537,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere">
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity">
|
||||
<div class="range-block-title" data-i18n="Top-p">
|
||||
Top P
|
||||
</div>
|
||||
@@ -788,7 +789,7 @@
|
||||
<div id="advanced-ai-config-block" class="width100p">
|
||||
<div id="kobold_api-settings">
|
||||
<div class="flex-container gap10h5v justifyCenter">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="temperature">Temperature</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Temperature controls the randomness in token selection" title="Temperature controls the randomness in token selection: - low temperature (<1.0) leads to more predictable text, favoring higher probability tokens. - high temperature (>1.0) increases creativity and diversity in the output by giving lower probability tokens a better chance. Set to 1.0 for the original probabilities."></div>
|
||||
@@ -796,7 +797,7 @@
|
||||
<input class="neo-range-slider" type="range" id="temp" name="volume" min="0.0" max="4.0" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0.0" max="4.0" step="0.01" data-for="temp" id="temp_counter">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<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 to disable."></div>
|
||||
@@ -804,7 +805,7 @@
|
||||
<input class="neo-range-slider" type="range" id="top_k" name="volume" min="0" max="100" step="1">
|
||||
<input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="top_k" id="top_k_counter">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
Top P
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Top P (a.k.a. nucleus sampling) adds up all the top tokens required to add up to the target percentage. E.g If the Top 2 tokens are both 25%, and Top P is 0.50, only the Top 2 tokens are considered. Set to 1.0 to disable."></div>
|
||||
@@ -812,7 +813,7 @@
|
||||
<input class="neo-range-slider" type="range" id="top_p" name="volume" min="0" max="1" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="top_p" id="top_p_counter">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Typical P">Typical P</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Typical P Sampling prioritizes tokens based on their deviation from the average entropy of the set. It maintains tokens whose cumulative probability is close to a predefined threshold (e.g., 0.5), emphasizing those with average information content. Set to 1.0 to disable."></div>
|
||||
@@ -820,7 +821,7 @@
|
||||
<input class="neo-range-slider" type="range" id="typical_p" name="volume" min="0" max="1" step="0.001">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="typical_p" id="typical_p_counter">
|
||||
</div>
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Min P">Min P</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Min P sets a base minimum probability. This is scaled according to the top token's probability. E.g If Top token is 80% probability, and Min P is 0.1, only tokens higher than 8% would be considered. Set to 0 to disable."></div>
|
||||
@@ -828,7 +829,7 @@
|
||||
<input class="neo-range-slider" type="range" id="min_p" name="volume" min="0" max="1" step="0.001">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="min_p" id="min_p_counter">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Top A">Top A</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Top A sets a threshold for token selection based on the square of the highest token probability. E.g if the Top-A value is 0.2 and the top token's probability is 50%, tokens with probabilities below 5% (0.2 * 0.5^2) are excluded. Set to 0 to disable."></div>
|
||||
@@ -836,29 +837,29 @@
|
||||
<input class="neo-range-slider" type="range" id="top_a" name="volume" min="0" max="1" step="0.001">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="top_a" id="top_a_counter">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Tail Free Sampling">Tail Free Sampling</span>
|
||||
<span data-i18n="Tail Free Sampling">TFS</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Tail-Free Sampling (TFS) searches for a tail of low-probability tokens in the distribution, by analyzing the rate of change in token probabilities using derivatives. It retains tokens up to a threshold (e.g., 0.3) based on the normalized second derivative. The closer to 0, the more discarded tokens. Set to 1.0 to disable."></div>
|
||||
</small>
|
||||
<input class="neo-range-slider" type="range" id="tfs" name="volume" min="0" max="1" step="0.001">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="tfs" id="tfs_counter">
|
||||
</div>
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="rep.pen">Repetition Penalty</span>
|
||||
</small>
|
||||
<input class="neo-range-slider" type="range" id="rep_pen" name="volume" min="1" max="3" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="1" max="3" step="0.01" data-for="rep_pen" id="rep_pen_counter">
|
||||
</div>
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="rep.pen range">Repetition Penalty Range</span>
|
||||
<span data-i18n="rep.pen range">Rep Pen Range</span>
|
||||
</small>
|
||||
<input class="neo-range-slider" type="range" id="rep_pen_range" name="volume" min="0" max="4096" step="1">
|
||||
<input class="neo-range-input" type="number" min="0" max="4096" step="1" data-for="rep_pen_range" id="rep_pen_range_counter">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Rep. Pen. Slope">Repetition Penalty Slope</span>
|
||||
</small>
|
||||
@@ -892,7 +893,7 @@
|
||||
</div>
|
||||
<hr class="wide100p">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter justifyCenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter justifyCenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<label class="checkbox_label alignItemsBaseline" for="use_default_badwordsids">
|
||||
<input id="use_default_badwordsids" type="checkbox" />
|
||||
<span>
|
||||
@@ -901,7 +902,7 @@
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter textAlignCenter flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter textAlignCenter flexBasis30p flexGrow flexShrink gap0">
|
||||
<!-- <hr class="wide100p"> -->
|
||||
<small data-i18n="Seed">Seed</small>
|
||||
<!-- Max value is 2**64 - 1 -->
|
||||
@@ -1185,7 +1186,7 @@
|
||||
<input type="number" id="n_textgenerationwebui" class="text_pole textAlignCenter" min="1" value="1" step="1" />
|
||||
</div>
|
||||
<div class="flex-container gap10h5v justifyCenter">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="temperature">Temperature</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Temperature controls the randomness in token selection" title="Temperature controls the randomness in token selection: - low temperature (<1.0) leads to more predictable text, favoring higher probability tokens. - high temperature (>1.0) increases creativity and diversity in the output by giving lower probability tokens a better chance. Set to 1.0 for the original probabilities."></div>
|
||||
@@ -1193,7 +1194,7 @@
|
||||
<input class="neo-range-slider" type="range" id="temp_textgenerationwebui" name="volume" min="0.0" max="5.0" step="0.01" x-setting-id="temp">
|
||||
<input class="neo-range-input" type="number" min="0.0" max="5.0" step="0.01" data-for="temp_textgenerationwebui" id="temp_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Top K">Top K</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Top K sets a maximum amount of top tokens that can be chosen from" 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."></div>
|
||||
@@ -1201,7 +1202,7 @@
|
||||
<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">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Top P">Top P</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Top P (a.k.a. nucleus sampling)" title="Top P (a.k.a. nucleus sampling) adds up all the top tokens required to add up to the target percentage. E.g If the Top 2 tokens are both 25%, and Top P is 0.50, only the Top 2 tokens are considered. Set to 1.0 to disable."></div>
|
||||
@@ -1209,7 +1210,7 @@
|
||||
<input class="neo-range-slider" type="range" id="top_p_textgenerationwebui" name="volume" min="0" max="1" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="top_p_textgenerationwebui" id="top_p_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Typical P">Typical P</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Typical P Sampling prioritizes tokens based on their deviation from the average entropy of the set" title="Typical P Sampling prioritizes tokens based on their deviation from the average entropy of the set. It maintains tokens whose cumulative probability is close to a predefined threshold (e.g., 0.5), emphasizing those with average information content. Set to 1.0 to disable."></div>
|
||||
@@ -1217,7 +1218,7 @@
|
||||
<input class="neo-range-slider" type="range" id="typical_p_textgenerationwebui" name="volume" min="0" max="1" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="typical_p_textgenerationwebui" id="typical_p_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Min P">Min P</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Min P sets a base minimum probability" title="Min P sets a base minimum probability. This is scaled according to the top token's probability. E.g If Top token is 80% probability, and Min P is 0.1, only tokens higher than 8% would be considered. Set to 0 to disable."></div>
|
||||
@@ -1225,7 +1226,7 @@
|
||||
<input class="neo-range-slider" type="range" id="min_p_textgenerationwebui" name="volume" min="0" max="1" step="0.001">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="min_p_textgenerationwebui" id="min_p_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Top A">Top A</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Top A sets a threshold for token selection based on the square of the highest token probability" title="Top A sets a threshold for token selection based on the square of the highest token probability. E.g if the Top-A value is 0.2 and the top token's probability is 50%, tokens with probabilities below 5% (0.2 * 0.5^2) are excluded. Set to 0 to disable."></div>
|
||||
@@ -1233,15 +1234,15 @@
|
||||
<input class="neo-range-slider" type="range" id="top_a_textgenerationwebui" name="volume" min="0" max="1" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="top_a_textgenerationwebui" id="top_a_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Tail Free Sampling">Tail Free Sampling</span>
|
||||
<span data-i18n="Tail Free Sampling">TFS</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Tail-Free Sampling (TFS)" title="Tail-Free Sampling (TFS) searches for a tail of low-probability tokens in the distribution, by analyzing the rate of change in token probabilities using derivatives. It retains tokens up to a threshold (e.g., 0.3) based on the normalized second derivative. The closer to 0, the more discarded tokens. Set to 1.0 to disable."></div>
|
||||
</small>
|
||||
<input class="neo-range-slider" type="range" id="tfs_textgenerationwebui" name="volume" min="0" max="1" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="tfs_textgenerationwebui" id="tfs_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="ooba,mancer" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="ooba,mancer" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Epsilon Cutoff">Epsilon Cutoff</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled" title="Epsilon cutoff sets a probability floor below which tokens are excluded from being sampled. In units of 1e-4; a reasonable value is 3. Set to 0 to disable."></div>
|
||||
@@ -1249,7 +1250,7 @@
|
||||
<input class="neo-range-slider" type="range" id="epsilon_cutoff_textgenerationwebui" name="volume" min="0" max="9" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="9" step="0.01" data-for="epsilon_cutoff_textgenerationwebui" id="epsilon_cutoff_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="ooba,mancer" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="ooba,mancer" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
<span data-i18n="Eta Cutoff">Eta Cutoff</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Eta cutoff is the main parameter of the special Eta Sampling technique. In units of 1e-4; a reasonable value is 3. Set to 0 to disable. See the paper Truncation Sampling as Language Model Desmoothing by Hewitt et al. (2022) for details." title="Eta cutoff is the main parameter of the special Eta Sampling technique. In units of 1e-4; a reasonable value is 3. Set to 0 to disable. See the paper Truncation Sampling as Language Model Desmoothing by Hewitt et al. (2022) for details."></div>
|
||||
@@ -1257,42 +1258,42 @@
|
||||
<input class="neo-range-slider" type="range" id="eta_cutoff_textgenerationwebui" name="volume" min="0" max="20" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="20" step="0.01" data-for="eta_cutoff_textgenerationwebui" id="eta_cutoff_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="rep.pen">Repetition Penalty</small>
|
||||
<input class="neo-range-slider" type="range" id="rep_pen_textgenerationwebui" name="volume" min="1" max="3" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="1" max="3" step="0.01" data-for="rep_pen_textgenerationwebui" id="rep_pen_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-forAphro="False" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="rep.pen range">Repetition Penalty Range</small>
|
||||
<div data-forAphro="False" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="rep.pen range">Rep Pen Range</small>
|
||||
<input class="neo-range-slider" type="range" id="rep_pen_range_textgenerationwebui" name="volume" min="-1" max="8192" step="1">
|
||||
<input class="neo-range-input" type="number" min="-1" max="8192" step="1" data-for="rep_pen_range_textgenerationwebui" id="rep_pen_range_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Encoder Rep. Pen.">Encoder Penalty</small>
|
||||
<input class="neo-range-slider" type="range" id="encoder_rep_pen_textgenerationwebui" name="volume" min="0.8" max="1.5" step="0.01" />
|
||||
<input class="neo-range-input" type="number" min="0.8" max="1.5" step="0.01" data-for="encoder_rep_pen_textgenerationwebui" id="encoder_rep_pen_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Frequency Penalty">Frequency Penalty</small>
|
||||
<input class="neo-range-slider" type="range" id="freq_pen_textgenerationwebui" name="volume" min="-2" max="2" step="0.01" />
|
||||
<input class="neo-range-input" type="number" data-for="freq_pen_textgenerationwebui" min="-2" max="2" step="0.01" id="freq_pen_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Presence Penalty">Presence Penalty</small>
|
||||
<input class="neo-range-slider" type="range" id="presence_pen_textgenerationwebui" name="volume" min="-2" max="2" step="0.01" />
|
||||
<input class="neo-range-input" type="number" min="-2" max="2" step="0.01" data-for="presence_pen_textgenerationwebui" id="presence_pen_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="No Repeat Ngram Size">No Repeat Ngram Size</small>
|
||||
<input class="neo-range-slider" type="range" id="no_repeat_ngram_size_textgenerationwebui" name="volume" min="0" max="20" step="1">
|
||||
<input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="no_repeat_ngram_size_textgenerationwebui" id="no_repeat_ngram_size_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="mancer, ooba, dreamgen" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="mancer, ooba, dreamgen" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Min Length">Min Length</small>
|
||||
<input class="neo-range-slider" type="range" id="min_length_textgenerationwebui" name="volume" min="0" max="2000" step="1" />
|
||||
<input class="neo-range-input" type="number" min="0" max="2000" step="1" data-for="min_length_textgenerationwebui" id="min_length_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Max Tokens Second">Maximum tokens/second</small>
|
||||
<input class="neo-range-slider" type="range" id="max_tokens_second_textgenerationwebui" name="volume" min="0" max="20" step="1" />
|
||||
<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">
|
||||
@@ -2307,6 +2308,7 @@
|
||||
<option value="makersuite">Google MakerSuite</option>
|
||||
<option value="mistralai">MistralAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="perplexity">Perplexity</option>
|
||||
<option value="scale">Scale</option>
|
||||
<option value="windowai">Window AI</option>
|
||||
</optgroup>
|
||||
@@ -2705,6 +2707,33 @@
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<div id="perplexity_form" data-source="perplexity">
|
||||
<h4 data-i18n="Perplexity API Key">Perplexity API Key</h4>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_perplexity" name="api_key_perplexity" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
|
||||
<div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_perplexity"></div>
|
||||
</div>
|
||||
<div data-for="api_key_perplexity" class="neutral_warning">
|
||||
For privacy reasons, your API key will be hidden after you reload the page.
|
||||
</div>
|
||||
<h4 data-i18n="Perplexity Model">Perplexity Model</h4>
|
||||
<select id="model_perplexity_select">
|
||||
<optgroup label="Perplexity Models">
|
||||
<option value="sonar-small-chat">sonar-small-chat</option>
|
||||
<option value="sonar-small-online">sonar-small-online</option>
|
||||
<option value="sonar-medium-chat">sonar-medium-chat</option>
|
||||
<option value="sonar-medium-online">sonar-medium-online</option>
|
||||
</optgroup>
|
||||
<optgroup label="Open-Source Models">
|
||||
<option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
|
||||
<option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
|
||||
<option value="codellama-70b-instruct">codellama-70b-instruct</option>
|
||||
<option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
|
||||
<option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
|
||||
<option value="mixtral-8x22b-instruct">mixtral-8x22b-instruct</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
<h4 data-i18n="Cohere API Key">Cohere API Key</h4>
|
||||
<div class="flex-container">
|
||||
@@ -3487,7 +3516,21 @@
|
||||
<small id="version_display"></small>
|
||||
</div>
|
||||
<div name="UserSettingsRowTwo" class="flex-container flexFlowRow">
|
||||
<textarea id="settingsSearch" class="textarea_compact wide100p" rows="1" placeholder="Search Settings" data-i18n="[placeholder]Search Settings"></textarea>
|
||||
<div id="account_controls" class="flex-container">
|
||||
<div id="account_button" class="margin0 menu_button_icon menu_button">
|
||||
<i class="fa-fw fa-solid fa-user-shield"></i>
|
||||
<span data-i18n="Account">Account</span>
|
||||
</div>
|
||||
<div id="admin_button" class="margin0 menu_button_icon menu_button" >
|
||||
<i class="fa-fw fa-solid fa-user-tie"></i>
|
||||
<span data-i18n="Admin Panel">Admin Panel</span>
|
||||
</div>
|
||||
<div id="logout_button" class="margin0 menu_button_icon menu_button">
|
||||
<i class="fa-fw fa-solid fa-right-from-bracket"></i>
|
||||
<span data-i18n="Logout">Logout</span>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="settingsSearch" class="textarea_compact flex1" rows="1" placeholder="Search Settings" data-i18n="[placeholder]Search Settings"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="user-settings-block-content" class="flex-container spaceEvenly">
|
||||
@@ -3502,7 +3545,7 @@
|
||||
<div id="ui_preset_export_button" class="menu_button menu_button_icon margin0" title="Export a theme file" data-i18n="[title]Export a theme file">
|
||||
<i class="fa-solid fa-file-export"></i>
|
||||
</div>
|
||||
<div id="ui-preset-delete-button" class="menu_button menu_button_icon margin0" title="Delete a theme" data-i18n="[title]Delete a theme" >
|
||||
<div id="ui-preset-delete-button" class="menu_button menu_button_icon margin0" title="Delete a theme" data-i18n="[title]Delete a theme">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3930,8 +3973,8 @@
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</label>
|
||||
<label class="checkbox_label" for="forbid_external_images" title="Disalow embedded media from other domains in chat messages." data-i18n="[title]Disalow embedded media from other domains in chat messages">
|
||||
<input id="forbid_external_images" type="checkbox" />
|
||||
<label class="checkbox_label" for="forbid_external_media" title="Disalow embedded media from other domains in chat messages." data-i18n="[title]Disalow embedded media from other domains in chat messages">
|
||||
<input id="forbid_external_media" type="checkbox" />
|
||||
<span data-i18n="Forbid External Media">Forbid External Media</span>
|
||||
</label>
|
||||
<label data-newbie-hidden class="checkbox_label" for="allow_name2_display">
|
||||
@@ -4419,9 +4462,7 @@
|
||||
<div class="flex1 flexGap5" title="Inserted before each part of the joined fields.">
|
||||
<label for="rm_group_generation_mode_join_prefix" class="flexnowrap width100p whitespacenowrap">
|
||||
<span data-i18n="Join Prefix">Join Prefix</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p"
|
||||
data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)"
|
||||
title="When 'Join character cards' is selected, all respective fields of the characters are being joined together. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)">
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)" title="When 'Join character cards' is selected, all respective fields of the characters are being joined together. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)">
|
||||
</div>
|
||||
</label>
|
||||
<textarea id="rm_group_generation_mode_join_prefix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="—" rows="1"></textarea>
|
||||
@@ -4429,9 +4470,7 @@
|
||||
<div class="flex1 flexGap5" title="Inserted after each part of the joined fields.">
|
||||
<label for="rm_group_generation_mode_join_suffix" class="flexnowrap width100p whitespacenowrap">
|
||||
<span data-i18n="Join Suffix">Join Suffix</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p"
|
||||
data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)"
|
||||
title="When 'Join character cards' is selected, all respective fields of the characters are being joined together. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)">
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)" title="When 'Join character cards' is selected, all respective fields of the characters are being joined together. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> with the name of the part (e.g.: description, personality, scenario, etc.)">
|
||||
</div>
|
||||
</label>
|
||||
<textarea id="rm_group_generation_mode_join_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="—" rows="1"></textarea>
|
||||
@@ -4939,7 +4978,7 @@
|
||||
<option value="3" data-role="" data-i18n="After AN">
|
||||
↓AN
|
||||
</option>
|
||||
<option value="4" data-role="0" data-i18n="at Depth System" >
|
||||
<option value="4" data-role="0" data-i18n="at Depth System">
|
||||
@D ⚙️
|
||||
</option>
|
||||
<option value="4" data-role="1" data-i18n="at Depth User">
|
||||
@@ -5351,17 +5390,16 @@
|
||||
Enable simple UI mode
|
||||
</span>
|
||||
</label>
|
||||
<h3 data-i18n="Your Persona">
|
||||
Your Persona
|
||||
</h3>
|
||||
<div class="justifyLeft margin-bot-10px">
|
||||
<span data-i18n="Before you get started, you must select a user name.">
|
||||
Before you get started, you must select a user name.
|
||||
<span data-i18n="Before you get started, you must select a persona name.">
|
||||
Before you get started, you must select a persona name.
|
||||
</span>
|
||||
This can be changed at any time via the <code><i class="fa-solid fa-face-smile"></i></code> icon.
|
||||
</div>
|
||||
<h4 data-i18n="UI Language:">UI Language:</h4>
|
||||
<select name="onboarding_ui_language">
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
<h4 data-i18n="User Name:">User Name:</h4>
|
||||
<h4 data-i18n="Persona Name:">Persona Name:</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div id="group_member_template" class="template_element">
|
||||
@@ -5964,10 +6002,7 @@
|
||||
<div class="fa-fw fa-solid fa-grip drag-grabber"></div>
|
||||
<div class="fa-fw fa-solid fa-circle-xmark dragClose" id="closeZoom"></div>
|
||||
</div>
|
||||
<img class="zoomed_avatar_img" src=""
|
||||
data-izoomify-url=""
|
||||
data-izoomify-magnify="1.8"
|
||||
data-izoomify-duration="300" alt="">
|
||||
<img class="zoomed_avatar_img" src="" data-izoomify-url="" data-izoomify-magnify="1.8" data-izoomify-duration="300" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
1
public/lib/epub.min.js
vendored
Normal file
13
public/lib/jszip.min.js
vendored
Normal file
81
public/login.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<base href="/">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, viewport-fit=cover, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="darkreader-lock">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="img/apple-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="img/apple-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="img/apple-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="img/apple-icon-144x144.png" />
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/st-tailwind.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/login.css">
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
|
||||
<link href="webfonts/NotoSans/stylesheet.css" rel="stylesheet">
|
||||
<!-- fontawesome webfonts-->
|
||||
<link href="css/fontawesome.css" rel="stylesheet">
|
||||
<link href="css/solid.css" rel="stylesheet">
|
||||
<link href="css/user.css" rel="stylesheet">
|
||||
<script src="lib/jquery-3.5.1.min.js"></script>
|
||||
<script src="scripts/login.js"></script>
|
||||
<title>SillyTavern</title>
|
||||
</head>
|
||||
|
||||
<body class="login">
|
||||
<div id="shadow_popup" style="opacity: 0;">
|
||||
<div id="dialogue_popup">
|
||||
<div id="dialogue_popup_holder">
|
||||
<div id="dialogue_popup_text">
|
||||
<div id="userSelectBlock" class="flex-container flexFlowColumn alignItemsCenter">
|
||||
<h2 id="logoBlock" class="flex-container">
|
||||
<img src="img/logo.png" alt="SillyTavern" class="logo">
|
||||
<span>Welcome to SillyTavern</span>
|
||||
</h2>
|
||||
<h3 id="normalLoginPrompt">
|
||||
Select an Account
|
||||
</h3>
|
||||
<h3 id="discreetLoginPrompt">
|
||||
Enter Login Details
|
||||
</h3>
|
||||
<div id="userListBlock" class="wide100p">
|
||||
<div id="userList" class="flex-container justifySpaceEvenly"></div>
|
||||
<div id="handleEntryBlock" style="display:none;" class="flex-container flexFlowColumn alignItemsCenter">
|
||||
<input id="userHandle" class="text_pole" type="text" placeholder="User handle" autocomplete="username">
|
||||
</div>
|
||||
<div id="passwordEntryBlock" style="display:none;"
|
||||
class="flex-container flexFlowColumn alignItemsCenter">
|
||||
<input id="userPassword" class="text_pole" type="password" placeholder="Password" autocomplete="current-password">
|
||||
<a id="recoverPassword" href="#" onclick="return false;">Forgot password?</a>
|
||||
<div class="flex-container">
|
||||
<div id="loginButton" class="menu_button">Login</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="passwordRecoveryBlock" style="display:none;"
|
||||
class="flex-container flexFlowColumn alignItemsCenter">
|
||||
<div id="recoverMessage">
|
||||
Recovery code has been posted to the server console.
|
||||
</div>
|
||||
<input id="recoveryCode" class="text_pole" type="text" placeholder="Recovery code">
|
||||
<input id="newPassword" class="text_pole" type="password" placeholder="New password" autocomplete="new-password">
|
||||
<div class="flex-container flexGap10">
|
||||
<div id="sendRecovery" class="menu_button">Send</div>
|
||||
<div id="cancelRecovery" class="menu_button">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="neutral_warning" id="errorMessage">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -202,7 +202,7 @@ import {
|
||||
instruct_presets,
|
||||
selectContextPreset,
|
||||
} from './scripts/instruct-mode.js';
|
||||
import { applyLocale, initLocales } from './scripts/i18n.js';
|
||||
import { initLocales } from './scripts/i18n.js';
|
||||
import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, getTokenizerModel, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';
|
||||
import { createPersona, initPersonas, selectCurrentPersona, setPersonaDescription, updatePersonaNameIfExists } from './scripts/personas.js';
|
||||
import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
|
||||
@@ -212,8 +212,10 @@ import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermati
|
||||
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
|
||||
import { initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros } from './scripts/macros.js';
|
||||
import { currentUser, setUserControls } from './scripts/user.js';
|
||||
import { callGenericPopup } from './scripts/popup.js';
|
||||
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
|
||||
import { ScraperManager } from './scripts/scrapers.js';
|
||||
|
||||
//exporting functions and vars for mods
|
||||
export {
|
||||
@@ -446,6 +448,7 @@ export const event_types = {
|
||||
CHARACTER_DELETED: 'characterDeleted',
|
||||
CHARACTER_DUPLICATED: 'character_duplicated',
|
||||
SMOOTH_STREAM_TOKEN_RECEIVED: 'smooth_stream_token_received',
|
||||
FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
|
||||
};
|
||||
|
||||
export const eventSource = new EventEmitter();
|
||||
@@ -666,13 +669,15 @@ async function getSystemMessages() {
|
||||
registerPromptManagerMigration();
|
||||
|
||||
$(document).ajaxError(function myErrorHandler(_, xhr) {
|
||||
// Cohee: CSRF doesn't error out in multiple tabs anymore, so this is unnecessary
|
||||
/*
|
||||
if (xhr.status == 403) {
|
||||
toastr.warning(
|
||||
'doubleCsrf errors in console are NORMAL in this case. If you want to run ST in multiple tabs, start the server with --disableCsrf option.',
|
||||
'Looks like you\'ve opened SillyTavern in another browser tab',
|
||||
{ timeOut: 0, extendedTimeOut: 0, preventDuplicates: true },
|
||||
);
|
||||
}
|
||||
} */
|
||||
});
|
||||
|
||||
async function getClientVersion() {
|
||||
@@ -1501,7 +1506,7 @@ function getCharacterSource(chId = this_chid) {
|
||||
}
|
||||
|
||||
async function getCharacters() {
|
||||
var response = await fetch('/api/characters/all', {
|
||||
const response = await fetch('/api/characters/all', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
@@ -1509,11 +1514,9 @@ async function getCharacters() {
|
||||
}),
|
||||
});
|
||||
if (response.ok === true) {
|
||||
var getData = ''; //RossAscends: reset to force array to update to account for deleted character.
|
||||
getData = await response.json();
|
||||
const load_ch_count = Object.getOwnPropertyNames(getData);
|
||||
for (var i = 0; i < load_ch_count.length; i++) {
|
||||
characters[i] = [];
|
||||
characters.splice(0, characters.length);
|
||||
const getData = await response.json();
|
||||
for (let i = 0; i < getData.length; i++) {
|
||||
characters[i] = getData[i];
|
||||
characters[i]['name'] = DOMPurify.sanitize(characters[i]['name']);
|
||||
|
||||
@@ -4129,8 +4132,12 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
// regenerate with character speech reenforced
|
||||
// to make sure we leave on swipe type while also adding the name2 appendage
|
||||
await delay(1000);
|
||||
// A message was already deleted on regeneration, so instead treat is as a normal gen
|
||||
if (type === 'regenerate') {
|
||||
type = 'normal';
|
||||
}
|
||||
// The first await is for waiting for the generate to start. The second one is waiting for it to finish
|
||||
const result = await await Generate(type, { automatic_trigger, force_name2: true, quiet_prompt, skipWIAN, force_chid, maxLoops: maxLoops - 1 });
|
||||
const result = await await Generate(type, { automatic_trigger, force_name2: true, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, maxLoops: maxLoops - 1 });
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -5654,7 +5661,7 @@ async function getChat() {
|
||||
contentType: 'application/json',
|
||||
});
|
||||
if (response[0] !== undefined) {
|
||||
chat.push(...response);
|
||||
chat.splice(0, chat.length, ...response);
|
||||
chat_create_date = chat[0]['create_date'];
|
||||
chat_metadata = chat[0]['chat_metadata'] ?? {};
|
||||
|
||||
@@ -6075,7 +6082,7 @@ async function doOnboarding(avatarId) {
|
||||
template.find('input[name="enable_simple_mode"]').on('input', function () {
|
||||
simpleUiMode = $(this).is(':checked');
|
||||
});
|
||||
var userName = await callPopup(template, 'input', name1);
|
||||
let userName = await callPopup(template, 'input', currentUser?.name || name1);
|
||||
|
||||
if (userName) {
|
||||
userName = userName.replace('\n', ' ');
|
||||
@@ -6129,6 +6136,8 @@ async function getSettings() {
|
||||
$('#your_name').val(name1);
|
||||
}
|
||||
|
||||
await setUserControls(data.enable_accounts);
|
||||
|
||||
// Allow subscribers to mutate settings
|
||||
eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
|
||||
|
||||
@@ -6764,8 +6773,9 @@ function select_rm_info(type, charId, previousCharId = null) {
|
||||
importFlashTimeout = setTimeout(function () {
|
||||
if (type === 'char_import' || type === 'char_create') {
|
||||
// Find the page at which the character is located
|
||||
const avatarFileName = `${charId}.png`;
|
||||
const charData = getEntitiesList({ doFilter: true });
|
||||
const charIndex = charData.findIndex((x) => x?.item?.avatar?.startsWith(charId));
|
||||
const charIndex = charData.findIndex((x) => x?.item?.avatar?.startsWith(avatarFileName));
|
||||
|
||||
if (charIndex === -1) {
|
||||
console.log(`Could not find character ${charId} in the list`);
|
||||
@@ -6775,7 +6785,7 @@ function select_rm_info(type, charId, previousCharId = null) {
|
||||
try {
|
||||
const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;
|
||||
const page = Math.floor(charIndex / perPage) + 1;
|
||||
const selector = `#rm_print_characters_block [title^="${charId}"]`;
|
||||
const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
|
||||
$('#rm_print_characters_pagination').pagination('go', page);
|
||||
|
||||
waitUntilCondition(() => document.querySelector(selector) !== null).then(() => {
|
||||
@@ -7085,10 +7095,10 @@ function onScenarioOverrideRemoveClick() {
|
||||
* @param {string} type
|
||||
* @param {string} inputValue - Value to set the input to.
|
||||
* @param {PopupOptions} options - Options for the popup.
|
||||
* @typedef {{okButton?: string, rows?: number, wide?: boolean, wider?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup.
|
||||
* @typedef {{okButton?: string, rows?: number, wide?: boolean, wider?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup.
|
||||
* @returns
|
||||
*/
|
||||
function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) {
|
||||
function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
|
||||
dialogueCloseStop = true;
|
||||
if (type) {
|
||||
popup_type = type;
|
||||
@@ -7146,7 +7156,7 @@ function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, l
|
||||
crop_data = undefined;
|
||||
|
||||
$('#avatarToCrop').cropper({
|
||||
aspectRatio: 2 / 3,
|
||||
aspectRatio: cropAspect ?? 2 / 3,
|
||||
autoCropArea: 1,
|
||||
viewMode: 2,
|
||||
rotatable: false,
|
||||
@@ -7358,47 +7368,6 @@ export function cancelTtsPlay() {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMessageImage() {
|
||||
const value = await callPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', 'confirm');
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mesBlock = $(this).closest('.mes');
|
||||
const mesId = mesBlock.attr('mesid');
|
||||
const message = chat[mesId];
|
||||
delete message.extra.image;
|
||||
delete message.extra.inline_image;
|
||||
mesBlock.find('.mes_img_container').removeClass('img_extra');
|
||||
mesBlock.find('.mes_img').attr('src', '');
|
||||
await saveChatConditional();
|
||||
}
|
||||
|
||||
function enlargeMessageImage() {
|
||||
const mesBlock = $(this).closest('.mes');
|
||||
const mesId = mesBlock.attr('mesid');
|
||||
const message = chat[mesId];
|
||||
const imgSrc = message?.extra?.image;
|
||||
const title = message?.extra?.title;
|
||||
|
||||
if (!imgSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.classList.add('img_enlarged');
|
||||
img.src = imgSrc;
|
||||
const imgContainer = $('<div><pre><code></code></pre></div>');
|
||||
imgContainer.prepend(img);
|
||||
imgContainer.addClass('img_enlarged_container');
|
||||
imgContainer.find('code').addClass('txt').text(title);
|
||||
const titleEmpty = !title || title.trim().length === 0;
|
||||
imgContainer.find('pre').toggle(!titleEmpty);
|
||||
addCopyToCodeBlocks(imgContainer);
|
||||
callPopup(imgContainer, 'text', '', { wide: true, large: true });
|
||||
}
|
||||
|
||||
function updateAlternateGreetingsHintVisibility(root) {
|
||||
const numberOfGreetings = root.find('.alternate_greetings_list .alternate_greeting').length;
|
||||
$(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
|
||||
@@ -7794,6 +7763,7 @@ window['SillyTavern'].getContext = function () {
|
||||
*/
|
||||
renderExtensionTemplate: renderExtensionTemplate,
|
||||
renderExtensionTemplateAsync: renderExtensionTemplateAsync,
|
||||
registerDataBankScraper: ScraperManager.registerDataBankScraper,
|
||||
callPopup: callPopup,
|
||||
callGenericPopup: callGenericPopup,
|
||||
mainApi: main_api,
|
||||
@@ -8244,10 +8214,15 @@ const CONNECT_API_MAP = {
|
||||
source: chat_completion_sources.CUSTOM,
|
||||
},
|
||||
'cohere': {
|
||||
selected: 'cohere',
|
||||
selected: 'openai',
|
||||
button: '#api_button_openai',
|
||||
source: chat_completion_sources.COHERE,
|
||||
},
|
||||
'perplexity': {
|
||||
selected: 'openai',
|
||||
button: '#api_button_openai',
|
||||
source: chat_completion_sources.PERPLEXITY,
|
||||
},
|
||||
'infermaticai': {
|
||||
selected: 'textgenerationwebui',
|
||||
button: '#api_button_textgenerationwebui',
|
||||
@@ -10153,6 +10128,7 @@ jQuery(async function () {
|
||||
'#character_cross',
|
||||
'#avatar-and-name-block',
|
||||
'#shadow_popup',
|
||||
'.shadow_popup',
|
||||
'#world_popup',
|
||||
'.ui-widget',
|
||||
'.text_pole',
|
||||
@@ -10388,9 +10364,6 @@ jQuery(async function () {
|
||||
$('#char-management-dropdown').prop('selectedIndex', 0);
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
|
||||
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
||||
|
||||
$(window).on('beforeunload', () => {
|
||||
cancelTtsPlay();
|
||||
if (streamingProcessor) {
|
||||
|
@@ -1398,7 +1398,8 @@ class PromptManager {
|
||||
`;
|
||||
|
||||
const rangeBlockDiv = promptManagerDiv.querySelector('.range-block');
|
||||
rangeBlockDiv.insertAdjacentHTML('beforeend', footerHtml);
|
||||
const headerDiv = promptManagerDiv.querySelector('.completion_prompt_manager_header');
|
||||
headerDiv.insertAdjacentHTML('afterend', footerHtml);
|
||||
rangeBlockDiv.querySelector('#prompt-manager-reset-character').addEventListener('click', this.handleCharacterReset);
|
||||
|
||||
const footerDiv = rangeBlockDiv.querySelector(`.${this.configuration.prefix}prompt_manager_footer`);
|
||||
@@ -1427,7 +1428,12 @@ class PromptManager {
|
||||
|
||||
rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup);
|
||||
|
||||
let exportPopper = Popper.createPopper(
|
||||
// Destroy previous popper instance if it exists
|
||||
if (this.exportPopper) {
|
||||
this.exportPopper.destroy();
|
||||
}
|
||||
|
||||
this.exportPopper = Popper.createPopper(
|
||||
document.getElementById('prompt-manager-export'),
|
||||
document.getElementById('prompt-manager-export-format-popup'),
|
||||
{ placement: 'bottom' },
|
||||
@@ -1440,7 +1446,7 @@ class PromptManager {
|
||||
if (show) popup.removeAttribute('data-show');
|
||||
else popup.setAttribute('data-show', '');
|
||||
|
||||
exportPopper.update();
|
||||
this.exportPopper.update();
|
||||
};
|
||||
|
||||
footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport);
|
||||
|
@@ -32,7 +32,7 @@ import {
|
||||
SECRET_KEYS,
|
||||
secret_state,
|
||||
} from './secrets.js';
|
||||
import { debounce, delay, getStringHash, isValidUrl } from './utils.js';
|
||||
import { debounce, getStringHash, isValidUrl } from './utils.js';
|
||||
import { chat_completion_sources, oai_settings } from './openai.js';
|
||||
import { getTokenCountAsync } from './tokenizers.js';
|
||||
import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
|
||||
@@ -377,6 +377,7 @@ function RA_autoconnect(PrevApi) {
|
||||
|| (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)
|
||||
|| (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI)
|
||||
|| (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
|
||||
|| (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
|
||||
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
|
||||
) {
|
||||
$('#api_button_openai').trigger('click');
|
||||
@@ -1202,7 +1203,7 @@ export function initRossMods() {
|
||||
|
||||
if (event.ctrlKey && /^[1-9]$/.test(event.key)) {
|
||||
// This will eventually be to trigger quick replies
|
||||
event.preventDefault();
|
||||
// event.preventDefault();
|
||||
console.log('Ctrl +' + event.key + ' pressed!');
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { characters, getCharacters, handleDeleteCharacter, callPopup, characterGroupOverlay } from '../script.js';
|
||||
import { characterGroupOverlay } from '../script.js';
|
||||
import { BulkEditOverlay, BulkEditOverlayState } from './BulkEditOverlay.js';
|
||||
|
||||
|
||||
@@ -69,15 +69,6 @@ function onSelectAllButtonClick() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the character with the given chid.
|
||||
*
|
||||
* @param {string} this_chid - The chid of the character to delete.
|
||||
*/
|
||||
async function deleteCharacter(this_chid) {
|
||||
await handleDeleteCharacter('del_ch', this_chid, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all characters that have been selected via the bulk checkboxes.
|
||||
*/
|
||||
|
@@ -18,6 +18,8 @@ import {
|
||||
saveSettingsDebounced,
|
||||
showSwipeButtons,
|
||||
this_chid,
|
||||
saveChatConditional,
|
||||
chat_metadata,
|
||||
} from '../script.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { power_user } from './power-user.js';
|
||||
@@ -25,22 +27,93 @@ import {
|
||||
extractTextFromHTML,
|
||||
extractTextFromMarkdown,
|
||||
extractTextFromPDF,
|
||||
extractTextFromEpub,
|
||||
getBase64Async,
|
||||
getStringHash,
|
||||
humanFileSize,
|
||||
saveBase64AsFile,
|
||||
extractTextFromOffice,
|
||||
} from './utils.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
import { ScraperManager } from './scrapers.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} FileAttachment
|
||||
* @property {string} url File URL
|
||||
* @property {number} size File size
|
||||
* @property {string} name File name
|
||||
* @property {number} created Timestamp
|
||||
* @property {string} [text] File text
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {function} ConverterFunction
|
||||
* @param {File} file File object
|
||||
* @returns {Promise<string>} Converted file text
|
||||
*/
|
||||
|
||||
const fileSizeLimit = 1024 * 1024 * 10; // 10 MB
|
||||
const ATTACHMENT_SOURCE = {
|
||||
GLOBAL: 'global',
|
||||
CHAT: 'chat',
|
||||
CHARACTER: 'character',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {Record<string, ConverterFunction>} File converters
|
||||
*/
|
||||
const converters = {
|
||||
'application/pdf': extractTextFromPDF,
|
||||
'text/html': extractTextFromHTML,
|
||||
'text/markdown': extractTextFromMarkdown,
|
||||
'application/epub+zip': extractTextFromEpub,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': extractTextFromOffice,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': extractTextFromOffice,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.text': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.presentation': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.spreadsheet': extractTextFromOffice,
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a matching key in the converters object.
|
||||
* @param {string} type MIME type
|
||||
* @returns {string} Matching key
|
||||
*/
|
||||
function findConverterKey(type) {
|
||||
return Object.keys(converters).find((key) => {
|
||||
// Match exact type
|
||||
if (type === key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Match wildcards
|
||||
if (key.endsWith('*')) {
|
||||
return type.startsWith(key.substring(0, key.length - 1));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the file type has a converter function.
|
||||
* @param {string} type MIME type
|
||||
* @returns {boolean} True if the file type is convertible, false otherwise.
|
||||
*/
|
||||
function isConvertible(type) {
|
||||
return Object.keys(converters).includes(type);
|
||||
return Boolean(findConverterKey(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the converter function for a file type.
|
||||
* @param {string} type MIME type
|
||||
* @returns {ConverterFunction} Converter function
|
||||
*/
|
||||
function getConverter(type) {
|
||||
const key = findConverterKey(type);
|
||||
return key && converters[key];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +199,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
|
||||
|
||||
if (isConvertible(file.type)) {
|
||||
try {
|
||||
const converter = converters[file.type];
|
||||
const converter = getConverter(file.type);
|
||||
const fileText = await converter(file);
|
||||
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||
} catch (error) {
|
||||
@@ -145,6 +218,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
|
||||
url: fileUrl,
|
||||
size: file.size,
|
||||
name: file.name,
|
||||
created: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -275,9 +349,9 @@ async function onFileAttach() {
|
||||
* @param {number} messageId Message ID
|
||||
*/
|
||||
async function deleteMessageFile(messageId) {
|
||||
const confirm = await callPopup('Are you sure you want to delete this file?', 'confirm');
|
||||
const confirm = await callGenericPopup('Are you sure you want to delete this file?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (!confirm) {
|
||||
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
console.debug('Delete file cancelled');
|
||||
return;
|
||||
}
|
||||
@@ -289,11 +363,15 @@ async function deleteMessageFile(messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = message.extra.file.url;
|
||||
|
||||
delete message.extra.file;
|
||||
$(`.mes[mesid="${messageId}"] .mes_file_container`).remove();
|
||||
saveChatDebounced();
|
||||
await saveChatConditional();
|
||||
await deleteFileFromServer(url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens file from message in a modal.
|
||||
* @param {number} messageId Message ID
|
||||
@@ -306,14 +384,7 @@ async function viewMessageFile(messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileText = messageFile.text || (await getFileAttachment(messageFile.url));
|
||||
|
||||
const modalTemplate = $('<div><pre><code></code></pre></div>');
|
||||
modalTemplate.find('code').addClass('txt').text(fileText);
|
||||
modalTemplate.addClass('file_modal');
|
||||
addCopyToCodeBlocks(modalTemplate);
|
||||
|
||||
callPopup(modalTemplate, 'text', '', { wide: true, large: true });
|
||||
await openFilePopup(messageFile);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,7 +419,7 @@ function embedMessageFile(messageId, messageBlock) {
|
||||
|
||||
await populateFileAttachment(message, 'embed_file_input');
|
||||
appendMediaToMessage(message, messageBlock);
|
||||
saveChatDebounced();
|
||||
await saveChatConditional();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +434,7 @@ export async function appendFileContent(message, messageText) {
|
||||
const fileText = message.extra.file.text || (await getFileAttachment(message.extra.file.url));
|
||||
|
||||
if (fileText) {
|
||||
const fileWrapped = `\`\`\`\n${fileText}\n\`\`\`\n\n`;
|
||||
const fileWrapped = `${fileText}\n\n`;
|
||||
message.extra.fileLength = fileWrapped.length;
|
||||
messageText = fileWrapped + messageText;
|
||||
}
|
||||
@@ -395,7 +466,7 @@ export function decodeStyleTags(text) {
|
||||
|
||||
return text.replaceAll(styleDecodeRegex, (_, style) => {
|
||||
try {
|
||||
let styleCleaned = unescape(style).replaceAll(/<br\/>/g, '');
|
||||
let styleCleaned = unescape(style).replaceAll(/<br\/>/g, '');
|
||||
const ast = css.parse(styleCleaned);
|
||||
const rules = ast?.stylesheet?.rules;
|
||||
if (rules) {
|
||||
@@ -436,8 +507,8 @@ async function openExternalMediaOverridesDialog() {
|
||||
}
|
||||
|
||||
const template = $('#forbid_media_override_template > .forbid_media_override').clone();
|
||||
template.find('.forbid_media_global_state_forbidden').toggle(power_user.forbid_external_images);
|
||||
template.find('.forbid_media_global_state_allowed').toggle(!power_user.forbid_external_images);
|
||||
template.find('.forbid_media_global_state_forbidden').toggle(power_user.forbid_external_media);
|
||||
template.find('.forbid_media_global_state_allowed').toggle(!power_user.forbid_external_media);
|
||||
|
||||
if (power_user.external_media_allowed_overrides.includes(entityId)) {
|
||||
template.find('#forbid_media_override_allowed').prop('checked', true);
|
||||
@@ -463,7 +534,7 @@ export function getCurrentEntityId() {
|
||||
export function isExternalMediaAllowed() {
|
||||
const entityId = getCurrentEntityId();
|
||||
if (!entityId) {
|
||||
return !power_user.forbid_external_images;
|
||||
return !power_user.forbid_external_media;
|
||||
}
|
||||
|
||||
if (power_user.external_media_allowed_overrides.includes(entityId)) {
|
||||
@@ -474,7 +545,518 @@ export function isExternalMediaAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !power_user.forbid_external_images;
|
||||
return !power_user.forbid_external_media;
|
||||
}
|
||||
|
||||
function enlargeMessageImage() {
|
||||
const mesBlock = $(this).closest('.mes');
|
||||
const mesId = mesBlock.attr('mesid');
|
||||
const message = chat[mesId];
|
||||
const imgSrc = message?.extra?.image;
|
||||
const title = message?.extra?.title;
|
||||
|
||||
if (!imgSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.classList.add('img_enlarged');
|
||||
img.src = imgSrc;
|
||||
const imgContainer = $('<div><pre><code></code></pre></div>');
|
||||
imgContainer.prepend(img);
|
||||
imgContainer.addClass('img_enlarged_container');
|
||||
imgContainer.find('code').addClass('txt').text(title);
|
||||
const titleEmpty = !title || title.trim().length === 0;
|
||||
imgContainer.find('pre').toggle(!titleEmpty);
|
||||
addCopyToCodeBlocks(imgContainer);
|
||||
callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
}
|
||||
|
||||
async function deleteMessageImage() {
|
||||
const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (value !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mesBlock = $(this).closest('.mes');
|
||||
const mesId = mesBlock.attr('mesid');
|
||||
const message = chat[mesId];
|
||||
delete message.extra.image;
|
||||
delete message.extra.inline_image;
|
||||
mesBlock.find('.mes_img_container').removeClass('img_extra');
|
||||
mesBlock.find('.mes_img').attr('src', '');
|
||||
await saveChatConditional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes file from the server.
|
||||
* @param {string} url Path to the file on the server
|
||||
* @returns {Promise<boolean>} True if file was deleted, false otherwise.
|
||||
*/
|
||||
async function deleteFileFromServer(url) {
|
||||
try {
|
||||
const result = await fetch('/api/files/delete', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ path: url }),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const error = await result.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
await eventSource.emit(event_types.FILE_ATTACHMENT_DELETED, url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
toastr.error(String(error), 'Could not delete file');
|
||||
console.error('Could not delete file', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens file attachment in a modal.
|
||||
* @param {FileAttachment} attachment File attachment
|
||||
*/
|
||||
async function openFilePopup(attachment) {
|
||||
const fileText = attachment.text || (await getFileAttachment(attachment.url));
|
||||
|
||||
const modalTemplate = $('<div><pre><code></code></pre></div>');
|
||||
modalTemplate.find('code').addClass('txt').text(fileText);
|
||||
modalTemplate.addClass('file_modal').addClass('textarea_compact').addClass('fontsize90p');
|
||||
addCopyToCodeBlocks(modalTemplate);
|
||||
|
||||
callGenericPopup(modalTemplate, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a file attachment in a notepad-like modal.
|
||||
* @param {FileAttachment} attachment Attachment to edit
|
||||
* @param {string} source Attachment source
|
||||
* @param {function} callback Callback function
|
||||
*/
|
||||
async function editAttachment(attachment, source, callback) {
|
||||
const originalFileText = attachment.text || (await getFileAttachment(attachment.url));
|
||||
const template = $(await renderExtensionTemplateAsync('attachments', 'notepad'));
|
||||
|
||||
let editedFileText = originalFileText;
|
||||
template.find('[name="notepadFileContent"]').val(editedFileText).on('input', function () {
|
||||
editedFileText = String($(this).val());
|
||||
});
|
||||
|
||||
let editedFileName = attachment.name;
|
||||
template.find('[name="notepadFileName"]').val(editedFileName).on('input', function () {
|
||||
editedFileName = String($(this).val());
|
||||
});
|
||||
|
||||
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { wide: true, large: true, okButton: 'Save', cancelButton: 'Cancel' });
|
||||
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editedFileText === originalFileText && editedFileName === attachment.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nullCallback = () => { };
|
||||
await deleteAttachment(attachment, source, nullCallback, false);
|
||||
const file = new File([editedFileText], editedFileName, { type: 'text/plain' });
|
||||
await uploadFileAttachmentToServer(file, source);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an attachment from the server and the chat.
|
||||
* @param {FileAttachment} attachment Attachment to delete
|
||||
* @param {string} source Source of the attachment
|
||||
* @param {function} callback Callback function
|
||||
* @param {boolean} [confirm=true] If true, show a confirmation dialog
|
||||
* @returns {Promise<void>} A promise that resolves when the attachment is deleted.
|
||||
*/
|
||||
async function deleteAttachment(attachment, source, callback, confirm = true) {
|
||||
if (confirm) {
|
||||
const result = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ensureAttachmentsExist();
|
||||
|
||||
switch (source) {
|
||||
case 'global':
|
||||
extension_settings.attachments = extension_settings.attachments.filter((a) => a.url !== attachment.url);
|
||||
saveSettingsDebounced();
|
||||
break;
|
||||
case 'chat':
|
||||
chat_metadata.attachments = chat_metadata.attachments.filter((a) => a.url !== attachment.url);
|
||||
saveMetadataDebounced();
|
||||
break;
|
||||
case 'character':
|
||||
extension_settings.character_attachments[characters[this_chid]?.avatar] = extension_settings.character_attachments[characters[this_chid]?.avatar].filter((a) => a.url !== attachment.url);
|
||||
break;
|
||||
}
|
||||
|
||||
await deleteFileFromServer(attachment.url);
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the attachment manager.
|
||||
*/
|
||||
async function openAttachmentManager() {
|
||||
/**
|
||||
* Renders a list of attachments.
|
||||
* @param {FileAttachment[]} attachments List of attachments
|
||||
* @param {string} source Source of the attachments
|
||||
*/
|
||||
async function renderList(attachments, source) {
|
||||
/**
|
||||
* Sorts attachments by sortField and sortOrder.
|
||||
* @param {FileAttachment} a First attachment
|
||||
* @param {FileAttachment} b Second attachment
|
||||
* @returns {number} Sort order
|
||||
*/
|
||||
function sortFn(a, b) {
|
||||
const sortValueA = a[sortField];
|
||||
const sortValueB = b[sortField];
|
||||
if (typeof sortValueA === 'string' && typeof sortValueB === 'string') {
|
||||
return sortValueA.localeCompare(sortValueB) * (sortOrder === 'asc' ? 1 : -1);
|
||||
}
|
||||
return (sortValueA - sortValueB) * (sortOrder === 'asc' ? 1 : -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters attachments by name.
|
||||
* @param {FileAttachment} a Attachment
|
||||
* @returns {boolean} True if attachment matches the filter, false otherwise.
|
||||
*/
|
||||
function filterFn(a) {
|
||||
if (!filterString) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return a.name.toLowerCase().includes(filterString.toLowerCase());
|
||||
}
|
||||
const sources = {
|
||||
[ATTACHMENT_SOURCE.GLOBAL]: '.globalAttachmentsList',
|
||||
[ATTACHMENT_SOURCE.CHARACTER]: '.characterAttachmentsList',
|
||||
[ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsList',
|
||||
};
|
||||
|
||||
template.find(sources[source]).empty();
|
||||
|
||||
// Sort attachments by sortField and sortOrder, and apply filter
|
||||
const sortedAttachmentList = attachments.slice().filter(filterFn).sort(sortFn);
|
||||
|
||||
for (const attachment of sortedAttachmentList) {
|
||||
const attachmentTemplate = template.find('.attachmentListItemTemplate .attachmentListItem').clone();
|
||||
attachmentTemplate.find('.attachmentListItemName').text(attachment.name);
|
||||
attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size));
|
||||
attachmentTemplate.find('.attachmentListItemCreated').text(new Date(attachment.created).toLocaleString());
|
||||
attachmentTemplate.find('.viewAttachmentButton').on('click', () => openFilePopup(attachment));
|
||||
attachmentTemplate.find('.editAttachmentButton').on('click', () => editAttachment(attachment, source, renderAttachments));
|
||||
attachmentTemplate.find('.deleteAttachmentButton').on('click', () => deleteAttachment(attachment, source, renderAttachments));
|
||||
template.find(sources[source]).append(attachmentTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders buttons for the attachment manager.
|
||||
*/
|
||||
async function renderButtons() {
|
||||
const sources = {
|
||||
[ATTACHMENT_SOURCE.GLOBAL]: '.globalAttachmentsTitle',
|
||||
[ATTACHMENT_SOURCE.CHARACTER]: '.characterAttachmentsTitle',
|
||||
[ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsTitle',
|
||||
};
|
||||
|
||||
const modal = template.find('.actionButtonsModal').hide();
|
||||
const scrapers = ScraperManager.getDataBankScrapers();
|
||||
|
||||
for (const scraper of scrapers) {
|
||||
const isAvailable = await ScraperManager.isScraperAvailable(scraper.id);
|
||||
if (!isAvailable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const buttonTemplate = template.find('.actionButtonTemplate .actionButton').clone();
|
||||
buttonTemplate.find('.actionButtonIcon').addClass(scraper.iconClass);
|
||||
buttonTemplate.find('.actionButtonText').text(scraper.name);
|
||||
buttonTemplate.attr('title', scraper.description);
|
||||
buttonTemplate.on('click', () => {
|
||||
const target = modal.attr('data-attachment-manager-target');
|
||||
runScraper(scraper.id, target, renderAttachments);
|
||||
});
|
||||
modal.append(buttonTemplate);
|
||||
}
|
||||
|
||||
const modalButtonData = Object.entries(sources).map(entry => {
|
||||
const [source, selector] = entry;
|
||||
const button = template.find(selector).find('.openActionModalButton').get(0);
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bodyListener = (e) => {
|
||||
if (modal.is(':visible') && (!$(e.target).closest('.openActionModalButton').length)) {
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
// Replay a click if the modal was already open by another button
|
||||
if ($(e.target).closest('.openActionModalButton').length && !modal.is(':visible')) {
|
||||
modal.show();
|
||||
}
|
||||
};
|
||||
document.body.addEventListener('click', bodyListener);
|
||||
|
||||
const popper = Popper.createPopper(button, modal.get(0), { placement: 'bottom-end' });
|
||||
button.addEventListener('click', () => {
|
||||
modal.attr('data-attachment-manager-target', source);
|
||||
modal.toggle();
|
||||
popper.update();
|
||||
});
|
||||
|
||||
return [popper, bodyListener];
|
||||
}).filter(Boolean);
|
||||
|
||||
return () => {
|
||||
modalButtonData.forEach(p => {
|
||||
const [popper, bodyListener] = p;
|
||||
popper.destroy();
|
||||
document.body.removeEventListener('click', bodyListener);
|
||||
});
|
||||
modal.remove();
|
||||
};
|
||||
}
|
||||
|
||||
async function renderAttachments() {
|
||||
/** @type {FileAttachment[]} */
|
||||
const globalAttachments = extension_settings.attachments ?? [];
|
||||
/** @type {FileAttachment[]} */
|
||||
const chatAttachments = chat_metadata.attachments ?? [];
|
||||
/** @type {FileAttachment[]} */
|
||||
const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
|
||||
await renderList(globalAttachments, ATTACHMENT_SOURCE.GLOBAL);
|
||||
await renderList(chatAttachments, ATTACHMENT_SOURCE.CHAT);
|
||||
await renderList(characterAttachments, ATTACHMENT_SOURCE.CHARACTER);
|
||||
|
||||
const isNotCharacter = this_chid === undefined || selected_group;
|
||||
const isNotInChat = getCurrentChatId() === undefined;
|
||||
template.find('.characterAttachmentsBlock').toggle(!isNotCharacter);
|
||||
template.find('.chatAttachmentsBlock').toggle(!isNotInChat);
|
||||
|
||||
const characterName = characters[this_chid]?.name || 'Anonymous';
|
||||
template.find('.characterAttachmentsName').text(characterName);
|
||||
|
||||
const chatName = getCurrentChatId() || 'Unnamed chat';
|
||||
template.find('.chatAttachmentsName').text(chatName);
|
||||
}
|
||||
|
||||
let sortField = localStorage.getItem('DataBank_sortField') || 'created';
|
||||
let sortOrder = localStorage.getItem('DataBank_sortOrder') || 'desc';
|
||||
let filterString = '';
|
||||
|
||||
const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
|
||||
|
||||
template.find('.attachmentSearch').on('input', function () {
|
||||
filterString = String($(this).val());
|
||||
renderAttachments();
|
||||
});
|
||||
template.find('.attachmentSort').on('change', function () {
|
||||
if (!(this instanceof HTMLSelectElement) || this.selectedOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sortField = this.selectedOptions[0].dataset.sortField;
|
||||
sortOrder = this.selectedOptions[0].dataset.sortOrder;
|
||||
localStorage.setItem('DataBank_sortField', sortField);
|
||||
localStorage.setItem('DataBank_sortOrder', sortOrder);
|
||||
renderAttachments();
|
||||
});
|
||||
|
||||
const cleanupFn = await renderButtons();
|
||||
await renderAttachments();
|
||||
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
|
||||
|
||||
cleanupFn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a known scraper on a source and saves the result as an attachment.
|
||||
* @param {string} scraperId Id of the scraper
|
||||
* @param {string} target Target for the attachment
|
||||
* @param {function} callback Callback function
|
||||
* @returns {Promise<void>} A promise that resolves when the source is scraped.
|
||||
*/
|
||||
async function runScraper(scraperId, target, callback) {
|
||||
try {
|
||||
console.log(`Running scraper ${scraperId} for ${target}`);
|
||||
const files = await ScraperManager.runDataBankScraper(scraperId);
|
||||
|
||||
if (!Array.isArray(files)) {
|
||||
console.warn('Scraping returned nothing');
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
console.warn('Scraping returned no files');
|
||||
toastr.info('No files were scraped.', 'Data Bank');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
await uploadFileAttachmentToServer(file, target);
|
||||
}
|
||||
|
||||
toastr.success(`Scraped ${files.length} files from ${scraperId} to ${target}.`, 'Data Bank');
|
||||
callback();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Scraping failed', error);
|
||||
toastr.error('Check browser console for details.', 'Scraping failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file attachment to the server.
|
||||
* @param {File} file File to upload
|
||||
* @param {string} target Target for the attachment
|
||||
* @returns
|
||||
*/
|
||||
export async function uploadFileAttachmentToServer(file, target) {
|
||||
const isValid = await validateFile(file);
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
let base64Data = await getBase64Async(file);
|
||||
const slug = getStringHash(file.name);
|
||||
const uniqueFileName = `${Date.now()}_${slug}.txt`;
|
||||
|
||||
if (isConvertible(file.type)) {
|
||||
try {
|
||||
const converter = getConverter(file.type);
|
||||
const fileText = await converter(file);
|
||||
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||
} catch (error) {
|
||||
toastr.error(String(error), 'Could not convert file');
|
||||
console.error('Could not convert file', error);
|
||||
}
|
||||
} else {
|
||||
const fileText = await file.text();
|
||||
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||
}
|
||||
|
||||
const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data);
|
||||
const convertedSize = Math.round(base64Data.length * 0.75);
|
||||
|
||||
if (!fileUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = {
|
||||
url: fileUrl,
|
||||
size: convertedSize,
|
||||
name: file.name,
|
||||
created: Date.now(),
|
||||
};
|
||||
|
||||
ensureAttachmentsExist();
|
||||
|
||||
switch (target) {
|
||||
case ATTACHMENT_SOURCE.GLOBAL:
|
||||
extension_settings.attachments.push(attachment);
|
||||
saveSettingsDebounced();
|
||||
break;
|
||||
case ATTACHMENT_SOURCE.CHAT:
|
||||
chat_metadata.attachments.push(attachment);
|
||||
saveMetadataDebounced();
|
||||
break;
|
||||
case ATTACHMENT_SOURCE.CHARACTER:
|
||||
extension_settings.character_attachments[characters[this_chid]?.avatar].push(attachment);
|
||||
saveSettingsDebounced();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAttachmentsExist() {
|
||||
if (!Array.isArray(extension_settings.attachments)) {
|
||||
extension_settings.attachments = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(chat_metadata.attachments)) {
|
||||
chat_metadata.attachments = [];
|
||||
}
|
||||
|
||||
if (this_chid !== undefined && characters[this_chid]) {
|
||||
if (!extension_settings.character_attachments) {
|
||||
extension_settings.character_attachments = {};
|
||||
}
|
||||
|
||||
if (!Array.isArray(extension_settings.character_attachments[characters[this_chid].avatar])) {
|
||||
extension_settings.character_attachments[characters[this_chid].avatar] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all currently available attachments.
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachments() {
|
||||
ensureAttachmentsExist();
|
||||
const globalAttachments = extension_settings.attachments ?? [];
|
||||
const chatAttachments = chat_metadata.attachments ?? [];
|
||||
const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
|
||||
return [...globalAttachments, ...chatAttachments, ...characterAttachments];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all attachments for a specific source.
|
||||
* @param {string} source Attachment source
|
||||
* @returns {FileAttachment[]} List of attachments
|
||||
*/
|
||||
export function getDataBankAttachmentsForSource(source) {
|
||||
ensureAttachmentsExist();
|
||||
|
||||
switch (source) {
|
||||
case ATTACHMENT_SOURCE.GLOBAL:
|
||||
return extension_settings.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHAT:
|
||||
return chat_metadata.attachments ?? [];
|
||||
case ATTACHMENT_SOURCE.CHARACTER:
|
||||
return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a file converter function.
|
||||
* @param {string} mimeType MIME type
|
||||
* @param {ConverterFunction} converter Function to convert file
|
||||
* @returns {void}
|
||||
*/
|
||||
export function registerFileConverter(mimeType, converter) {
|
||||
if (typeof mimeType !== 'string' || typeof converter !== 'function') {
|
||||
console.error('Invalid converter registration');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(converters).includes(mimeType)) {
|
||||
console.error('Converter already registered');
|
||||
return;
|
||||
}
|
||||
|
||||
converters[mimeType] = converter;
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
@@ -507,6 +1089,11 @@ jQuery(function () {
|
||||
$('#file_form_input').trigger('click');
|
||||
});
|
||||
|
||||
// Do not change. #manageAttachments is added by extension.
|
||||
$(document).on('click', '#manageAttachments', function () {
|
||||
openAttachmentManager();
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_embed', function () {
|
||||
const messageBlock = $(this).closest('.mes');
|
||||
const messageId = Number(messageBlock.attr('mesid'));
|
||||
@@ -598,6 +1185,9 @@ jQuery(function () {
|
||||
reloadCurrentChat();
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
|
||||
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
||||
|
||||
$('#file_form_input').on('change', onFileAttach);
|
||||
$('#file_form').on('reset', function () {
|
||||
$('#file_form').addClass('displayNone');
|
||||
|
@@ -145,6 +145,8 @@ const extension_settings = {
|
||||
variables: {
|
||||
global: {},
|
||||
},
|
||||
attachments: [],
|
||||
character_attachments: {},
|
||||
};
|
||||
|
||||
let modules = [];
|
||||
|
9
public/scripts/extensions/attachments/buttons.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div id="attachFile" class="list-group-item flex-container flexGap5" title="Attach a file or image to a current chat.">
|
||||
<div class="fa-fw fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
|
||||
<span data-i18n="Attach a File">Attach a File</span>
|
||||
</div>
|
||||
|
||||
<div id="manageAttachments" class="list-group-item flex-container flexGap5" title="View global, character, or data files.">
|
||||
<div class="fa-fw fa-solid fa-book-open-reader extensionsMenuExtensionButton"></div>
|
||||
<span data-i18n="Open Data Bank">Open Data Bank</span>
|
||||
</div>
|
51
public/scripts/extensions/attachments/fandom-scrape.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="fandomScrapeInput" data-i18n="Enter a URL or the ID of a Fandom wiki page to scrape:">
|
||||
Enter a URL or the ID of a Fandom wiki page to scrape:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<code>https://harrypotter.fandom.com/</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>harrypotter</code>
|
||||
</small>
|
||||
<input type="text" id="fandomScrapeInput" name="fandomScrapeInput" class="text_pole" placeholder="">
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="fandomScrapeFilter">
|
||||
Optional regex to pick the content by its title:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n="Example:">Example:</span>
|
||||
<code>/(Azkaban|Weasley)/gi</code>
|
||||
</small>
|
||||
<input type="text" id="fandomScrapeFilter" name="fandomScrapeFilter" class="text_pole" placeholder="">
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label>
|
||||
Output format:
|
||||
</label>
|
||||
<label class="checkbox_label justifyLeft" for="fandomScrapeOutputSingle">
|
||||
<input id="fandomScrapeOutputSingle" type="radio" name="fandomScrapeOutput" value="single" checked>
|
||||
<div class="flex-container flexFlowColumn flexNoGap">
|
||||
<span data-i18n="Single file">
|
||||
Single file
|
||||
</span>
|
||||
<small data-i18n="All articles will be concatenated into a single file.">
|
||||
All articles will be concatenated into a single file.
|
||||
</small>
|
||||
</div>
|
||||
</label>
|
||||
<label class="checkbox_label justifyLeft" for="fandomScrapeOutputMulti">
|
||||
<input id="fandomScrapeOutputMulti" type="radio" name="fandomScrapeOutput" value="multi">
|
||||
<div class="flex-container flexFlowColumn flexNoGap">
|
||||
<span data-i18n="File per article">
|
||||
File per article
|
||||
</span>
|
||||
<small data-i18n="Each article will be saved as a separate file.">
|
||||
Not recommended. Each article will be saved as a separate file.
|
||||
</small>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
6
public/scripts/extensions/attachments/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
|
||||
jQuery(async () => {
|
||||
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
||||
$('#extensionsMenu').prepend(buttons);
|
||||
});
|
118
public/scripts/extensions/attachments/manager.html
Normal file
@@ -0,0 +1,118 @@
|
||||
<div class="wide100p padding5">
|
||||
<h2 class="marginBot5">
|
||||
<span data-i18n="Data Bank">
|
||||
Data Bank
|
||||
</span>
|
||||
</h2>
|
||||
<div data-i18n="These files will be available for extensions that support attachments (e.g. Vector Storage).">
|
||||
These files will be available for extensions that support attachments (e.g. Vector Storage).
|
||||
</div>
|
||||
<div data-i18n="Supported file types: Plain Text, PDF, Markdown, HTML, EPUB." class="marginTopBot5">
|
||||
Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.
|
||||
</div>
|
||||
<div class="flex-container marginTopBot5">
|
||||
<input type="search" id="attachmentSearch" class="attachmentSearch text_pole margin0 flex1" placeholder="Search...">
|
||||
<select id="attachmentSort" class="attachmentSort text_pole margin0 flex1 textarea_compact">
|
||||
<option data-sort-field="created" data-sort-order="desc" data-i18n="Date (Newest First)">
|
||||
Date (Newest First)
|
||||
</option>
|
||||
<option data-sort-field="created" data-sort-order="asc" data-i18n="Date (Oldest First)">
|
||||
Date (Oldest First)
|
||||
</option>
|
||||
<option data-sort-field="name" data-sort-order="asc" data-i18n="Name (A-Z)">
|
||||
Name (A-Z)
|
||||
</option>
|
||||
<option data-sort-field="name" data-sort-order="desc" data-i18n="Name (Z-A)">
|
||||
Name (Z-A)
|
||||
</option>
|
||||
<option data-sort-field="size" data-sort-order="asc" data-i18n="Size (Smallest First)">
|
||||
Size (Smallest First)
|
||||
</option>
|
||||
<option data-sort-field="size" data-sort-order="desc" data-i18n="Size (Largest First)">
|
||||
Size (Largest First)
|
||||
</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="justifyLeft globalAttachmentsBlock marginBot10">
|
||||
<h3 class="globalAttachmentsTitle margin0 title_restorable">
|
||||
<span data-i18n="Global Attachments">
|
||||
Global Attachments
|
||||
</span>
|
||||
<div class="openActionModalButton menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span data-i18n="Add">Add</span>
|
||||
</div>
|
||||
</h3>
|
||||
<small data-i18n="These files are available for all characters in all chats.">
|
||||
These files are available for all characters in all chats.
|
||||
</small>
|
||||
<div class="globalAttachmentsList attachmentsList"></div>
|
||||
<hr>
|
||||
</div>
|
||||
<div class="justifyLeft characterAttachmentsBlock marginBot10">
|
||||
<h3 class="characterAttachmentsTitle margin0 title_restorable">
|
||||
<span data-i18n="Character Attachments">
|
||||
Character Attachments
|
||||
</span>
|
||||
<div class="openActionModalButton menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span data-i18n="Add">Add</span>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<strong><small class="characterAttachmentsName"></small></strong>
|
||||
<small>
|
||||
<span data-i18n="These files are available the current character in all chats they are in.">
|
||||
These files are available the current character in all chats they are in.
|
||||
</span>
|
||||
<span>
|
||||
<span data-i18n="Saved locally. Not exported.">
|
||||
Saved locally. Not exported.
|
||||
</span>
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
<div class="characterAttachmentsList attachmentsList"></div>
|
||||
<hr>
|
||||
</div>
|
||||
<div class="justifyLeft chatAttachmentsBlock marginBot10">
|
||||
<h3 class="chatAttachmentsTitle margin0 title_restorable">
|
||||
<span data-i18n="Chat Attachments">
|
||||
Chat Attachments
|
||||
</span>
|
||||
<div class="openActionModalButton menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span data-i18n="Add">Add</span>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<strong><small class="chatAttachmentsName"></small></strong>
|
||||
<small data-i18n="These files are available to all characters in the current chat.">
|
||||
These files are available to all characters in the current chat.
|
||||
</small>
|
||||
</div>
|
||||
<div class="chatAttachmentsList attachmentsList"></div>
|
||||
</div>
|
||||
|
||||
<div class="attachmentListItemTemplate template_element">
|
||||
<div class="attachmentListItem flex-container alignItemsCenter flexGap10">
|
||||
<div class="attachmentFileIcon fa-solid fa-file-alt"></div>
|
||||
<div class="attachmentListItemName flex1"></div>
|
||||
<small class="attachmentListItemCreated"></small>
|
||||
<small class="attachmentListItemSize"></small>
|
||||
<div class="viewAttachmentButton right_menu_button fa-solid fa-magnifying-glass" title="View attachment content"></div>
|
||||
<div class="editAttachmentButton right_menu_button fa-solid fa-pencil" title="Edit attachment"></div>
|
||||
<div class="deleteAttachmentButton right_menu_button fa-solid fa-trash" title="Delete attachment"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actionButtonTemplate">
|
||||
<div class="actionButton list-group-item flex-container flexGap5" title="">
|
||||
<i class="actionButtonIcon"></i>
|
||||
<span class="actionButtonText"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actionButtonsModal popper-modal options-content list-group"></div>
|
||||
</div>
|
11
public/scripts/extensions/attachments/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Chat Attachments",
|
||||
"loading_order": 3,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Cohee1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
10
public/scripts/extensions/attachments/notepad.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="flex-container flexFlowColumn height100p">
|
||||
<label for="notepadFileName">
|
||||
File Name
|
||||
</label>
|
||||
<input type="text" class="text_pole" id="notepadFileName" name="notepadFileName" value="" />
|
||||
<labels>
|
||||
File Content
|
||||
</label>
|
||||
<textarea id="notepadFileContent" name="notepadFileContent" class="text_pole textarea_compact monospace flex1" placeholder="Enter your notes here."></textarea>
|
||||
</div>
|
29
public/scripts/extensions/attachments/style.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.attachmentsList:empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.attachmentsList:empty::before {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
content: "No data";
|
||||
font-weight: bolder;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.8;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.attachmentListItem {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.attachmentListItemSize {
|
||||
min-width: 4em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.attachmentListItemCreated {
|
||||
text-align: right;
|
||||
}
|
3
public/scripts/extensions/attachments/web-scrape.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div data-i18n="Enter web URLs to scrape (one per line):">
|
||||
Enter web URLs to scrape (one per line):
|
||||
</div>
|
20
public/scripts/extensions/attachments/youtube-scrape.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div>
|
||||
<strong data-i18n="Enter a video URL to download its transcript.">
|
||||
Enter a video URL or ID to download its transcript.
|
||||
</strong>
|
||||
<div data-i18n="Examples:" class="m-t-1">
|
||||
Examples:
|
||||
</div>
|
||||
<ul class="justifyLeft">
|
||||
<li>https://www.youtube.com/watch?v=jV1vkHv4zq8</li>
|
||||
<li>https://youtu.be/nlLhw1mtCFA</li>
|
||||
<li>TDpxx5UqrVU</li>
|
||||
</ul>
|
||||
<label>
|
||||
Language code (optional 2-letter ISO code):
|
||||
</label>
|
||||
<input type="text" class="text_pole" name="youtubeLanguageCode" placeholder="e.g. en">
|
||||
<label>
|
||||
Video ID:
|
||||
</label>
|
||||
</div>
|
@@ -310,14 +310,8 @@ jQuery(function () {
|
||||
<div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
|
||||
Generate Caption
|
||||
</div>`);
|
||||
const attachFileButton = $(`
|
||||
<div id="attachFile" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
|
||||
Attach a File
|
||||
</div>`);
|
||||
|
||||
$('#extensionsMenu').prepend(sendButton);
|
||||
$('#extensionsMenu').prepend(attachFileButton);
|
||||
$(sendButton).on('click', () => {
|
||||
const hasCaptionModule =
|
||||
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
||||
|
@@ -507,6 +507,10 @@ async function loadTalkingHead() {
|
||||
},
|
||||
body: JSON.stringify(emotionsSettings),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
throw new Error(apiResult.statusText);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// it's ok if not supported
|
||||
@@ -539,6 +543,10 @@ async function loadTalkingHead() {
|
||||
},
|
||||
body: JSON.stringify(animatorSettings),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
throw new Error(apiResult.statusText);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// it's ok if not supported
|
||||
@@ -1076,6 +1084,7 @@ async function getExpressionLabel(text) {
|
||||
case EXPRESSION_API.llm: {
|
||||
const expressionsList = await getExpressionsList();
|
||||
const prompt = await getLlmPrompt(expressionsList);
|
||||
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
|
||||
const emotionResponse = await generateQuietPrompt(prompt, false, false);
|
||||
return parseLlmResponse(emotionResponse, expressionsList);
|
||||
}
|
||||
@@ -1956,7 +1965,6 @@ function migrateSettings() {
|
||||
});
|
||||
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||
eventSource.on(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
|
||||
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> – force sets the sprite for the current character', true, true);
|
||||
registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> – sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
|
||||
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> – Returns the last set sprite / expression for the named character.', true, true);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { getStringHash, debounce, waitUntilCondition, extractAllWords, delay } from '../../utils.js';
|
||||
import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js';
|
||||
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import {
|
||||
activateSendButtons,
|
||||
|
@@ -23,7 +23,6 @@ export async function getMultimodalCaption(base64Img, prompt) {
|
||||
// OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.
|
||||
// Ooba requires all images to be JPEGs. Koboldcpp just asked nicely.
|
||||
const isGoogle = extension_settings.caption.multimodal_api === 'google';
|
||||
const isClaude = extension_settings.caption.multimodal_api === 'anthropic';
|
||||
const isOllama = extension_settings.caption.multimodal_api === 'ollama';
|
||||
const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp';
|
||||
const isCustom = extension_settings.caption.multimodal_api === 'custom';
|
||||
|
@@ -37,6 +37,8 @@ const p = a => `<p>${a}</p>`;
|
||||
|
||||
const MODULE_NAME = 'sd';
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
// This is a 1x1 transparent PNG
|
||||
const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
|
||||
const sources = {
|
||||
extras: 'extras',
|
||||
@@ -2650,6 +2652,8 @@ async function generateComfyImage(prompt, negativePrompt) {
|
||||
const avatarBlob = await response.blob();
|
||||
const avatarBase64 = await getBase64Async(avatarBlob);
|
||||
workflow = workflow.replace('"%user_avatar%"', JSON.stringify(avatarBase64));
|
||||
} else {
|
||||
workflow = workflow.replace('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
|
||||
}
|
||||
}
|
||||
if (/%char_avatar%/gi.test(workflow)) {
|
||||
@@ -2658,6 +2662,8 @@ async function generateComfyImage(prompt, negativePrompt) {
|
||||
const avatarBlob = await response.blob();
|
||||
const avatarBase64 = await getBase64Async(avatarBlob);
|
||||
workflow = workflow.replace('"%char_avatar%"', JSON.stringify(avatarBase64));
|
||||
} else {
|
||||
workflow = workflow.replace('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
|
||||
}
|
||||
}
|
||||
console.log(`{
|
||||
|
@@ -433,8 +433,8 @@ class AllTalkTtsProvider {
|
||||
updateLanguageDropdown() {
|
||||
const languageSelect = document.getElementById('language_options');
|
||||
if (languageSelect) {
|
||||
// Ensure default language is set
|
||||
this.settings.language = this.settings.language;
|
||||
// Ensure default language is set (??? whatever that means)
|
||||
// this.settings.language = this.settings.language;
|
||||
|
||||
languageSelect.innerHTML = '';
|
||||
for (let language in this.languageLabels) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js';
|
||||
import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js';
|
||||
import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
|
||||
import { EdgeTtsProvider } from './edge.js';
|
||||
@@ -425,6 +425,9 @@ async function processTtsQueue() {
|
||||
currentTtsJob = ttsJobQueue.shift();
|
||||
let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes;
|
||||
|
||||
// Substitute macros
|
||||
text = substituteParams(text);
|
||||
|
||||
if (extension_settings.tts.skip_codeblocks) {
|
||||
text = text.replace(/^\s{4}.*$/gm, '').trim();
|
||||
text = text.replace(/```.*?```/gs, '').trim();
|
||||
|
@@ -2,6 +2,7 @@ import {
|
||||
eventSource,
|
||||
event_types,
|
||||
extension_prompt_types,
|
||||
extension_prompt_roles,
|
||||
getCurrentChatId,
|
||||
getRequestHeaders,
|
||||
is_send_press,
|
||||
@@ -20,11 +21,13 @@ import {
|
||||
} from '../../extensions.js';
|
||||
import { collapseNewlines } from '../../power-user.js';
|
||||
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { getDataBankAttachments, getFileAttachment } from '../../chats.js';
|
||||
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
|
||||
|
||||
const MODULE_NAME = 'vectors';
|
||||
|
||||
export const EXTENSION_PROMPT_TAG = '3_vectors';
|
||||
export const EXTENSION_PROMPT_TAG_DB = '4_vectors_data_bank';
|
||||
|
||||
const settings = {
|
||||
// For both
|
||||
@@ -32,6 +35,7 @@ const settings = {
|
||||
include_wi: false,
|
||||
togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',
|
||||
openai_model: 'text-embedding-ada-002',
|
||||
cohere_model: 'embed-english-v3.0',
|
||||
summarize: false,
|
||||
summarize_sent: false,
|
||||
summary_source: 'main',
|
||||
@@ -39,7 +43,7 @@ const settings = {
|
||||
|
||||
// For chats
|
||||
enabled_chats: false,
|
||||
template: 'Past events: {{text}}',
|
||||
template: 'Past events:\n{{text}}',
|
||||
depth: 2,
|
||||
position: extension_prompt_types.IN_PROMPT,
|
||||
protect: 5,
|
||||
@@ -49,13 +53,32 @@ const settings = {
|
||||
|
||||
// For files
|
||||
enabled_files: false,
|
||||
translate_files: false,
|
||||
size_threshold: 10,
|
||||
chunk_size: 5000,
|
||||
chunk_count: 2,
|
||||
|
||||
// For Data Bank
|
||||
size_threshold_db: 5,
|
||||
chunk_size_db: 2500,
|
||||
chunk_count_db: 5,
|
||||
file_template_db: 'Related information:\n{{text}}',
|
||||
file_position_db: extension_prompt_types.IN_PROMPT,
|
||||
file_depth_db: 4,
|
||||
file_depth_role_db: extension_prompt_roles.SYSTEM,
|
||||
};
|
||||
|
||||
const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
|
||||
|
||||
/**
|
||||
* Gets the Collection ID for a file embedded in the chat.
|
||||
* @param {string} fileUrl URL of the file
|
||||
* @returns {string} Collection ID
|
||||
*/
|
||||
function getFileCollectionId(fileUrl) {
|
||||
return `file_${getStringHash(fileUrl)}`;
|
||||
}
|
||||
|
||||
async function onVectorizeAllClick() {
|
||||
try {
|
||||
if (!settings.enabled_chats) {
|
||||
@@ -292,6 +315,34 @@ async function processFiles(chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataBank = getDataBankAttachments();
|
||||
const dataBankCollectionIds = [];
|
||||
|
||||
for (const file of dataBank) {
|
||||
const collectionId = getFileCollectionId(file.url);
|
||||
const hashesInCollection = await getSavedHashes(collectionId);
|
||||
dataBankCollectionIds.push(collectionId);
|
||||
|
||||
// File is already in the collection
|
||||
if (hashesInCollection.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download and process the file
|
||||
file.text = await getFileAttachment(file.url);
|
||||
console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
|
||||
// Convert kilobytes to string length
|
||||
const thresholdLength = settings.size_threshold_db * 1024;
|
||||
// Use chunk size from settings if file is larger than threshold
|
||||
const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
|
||||
await vectorizeFile(file.text, file.name, collectionId, chunkSize);
|
||||
}
|
||||
|
||||
if (dataBankCollectionIds.length) {
|
||||
const queryText = await getQueryText(chat);
|
||||
await injectDataBankChunks(queryText, dataBankCollectionIds);
|
||||
}
|
||||
|
||||
for (const message of chat) {
|
||||
// Message has no file
|
||||
if (!message?.extra?.file) {
|
||||
@@ -300,8 +351,7 @@ async function processFiles(chat) {
|
||||
|
||||
// Trim file inserted by the script
|
||||
const fileText = String(message.mes)
|
||||
.substring(0, message.extra.fileLength).trim()
|
||||
.replace(/^```/, '').replace(/```$/, '').trim();
|
||||
.substring(0, message.extra.fileLength).trim();
|
||||
|
||||
// Convert kilobytes to string length
|
||||
const thresholdLength = settings.size_threshold * 1024;
|
||||
@@ -314,25 +364,55 @@ async function processFiles(chat) {
|
||||
message.mes = message.mes.substring(message.extra.fileLength);
|
||||
|
||||
const fileName = message.extra.file.name;
|
||||
const collectionId = `file_${getStringHash(fileName)}`;
|
||||
const fileUrl = message.extra.file.url;
|
||||
const collectionId = getFileCollectionId(fileUrl);
|
||||
const hashesInCollection = await getSavedHashes(collectionId);
|
||||
|
||||
// File is already in the collection
|
||||
if (!hashesInCollection.length) {
|
||||
await vectorizeFile(fileText, fileName, collectionId);
|
||||
await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size);
|
||||
}
|
||||
|
||||
const queryText = await getQueryText(chat);
|
||||
const fileChunks = await retrieveFileChunks(queryText, collectionId);
|
||||
|
||||
// Wrap it back in a code block
|
||||
message.mes = `\`\`\`\n${fileChunks}\n\`\`\`\n\n${message.mes}`;
|
||||
message.mes = `${fileChunks}\n\n${message.mes}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to retrieve files', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts file chunks from the Data Bank into the prompt.
|
||||
* @param {string} queryText Text to query
|
||||
* @param {string[]} collectionIds File collection IDs
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function injectDataBankChunks(queryText, collectionIds) {
|
||||
try {
|
||||
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.chunk_count_db);
|
||||
console.debug(`Vectors: Retrieved ${collectionIds.length} Data Bank collections`, queryResults);
|
||||
let textResult = '';
|
||||
|
||||
for (const collectionId in queryResults) {
|
||||
console.debug(`Vectors: Processing Data Bank collection ${collectionId}`, queryResults[collectionId]);
|
||||
const metadata = queryResults[collectionId].metadata?.filter(x => x.text)?.sort((a, b) => a.index - b.index)?.map(x => x.text)?.filter(onlyUnique) || [];
|
||||
textResult += metadata.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
if (!textResult) {
|
||||
console.debug('Vectors: No Data Bank chunks found');
|
||||
return;
|
||||
}
|
||||
|
||||
const insertedText = substituteParams(settings.file_template_db.replace(/{{text}}/i, textResult));
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG_DB, insertedText, settings.file_position_db, settings.file_depth_db, settings.include_wi, settings.file_depth_role_db);
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to insert Data Bank chunks', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves file chunks from the vector index and inserts them into the chat.
|
||||
* @param {string} queryText Text to query
|
||||
@@ -354,16 +434,24 @@ async function retrieveFileChunks(queryText, collectionId) {
|
||||
* @param {string} fileText File text
|
||||
* @param {string} fileName File name
|
||||
* @param {string} collectionId File collection ID
|
||||
* @param {number} chunkSize Chunk size
|
||||
*/
|
||||
async function vectorizeFile(fileText, fileName, collectionId) {
|
||||
async function vectorizeFile(fileText, fileName, collectionId, chunkSize) {
|
||||
try {
|
||||
toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`);
|
||||
const chunks = splitRecursive(fileText, settings.chunk_size);
|
||||
if (settings.translate_files && typeof window['translate'] === 'function') {
|
||||
console.log(`Vectors: Translating file ${fileName} to English...`);
|
||||
const translatedText = await window['translate'](fileText, 'en');
|
||||
fileText = translatedText;
|
||||
}
|
||||
|
||||
const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`);
|
||||
const chunks = splitRecursive(fileText, chunkSize);
|
||||
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks`, chunks);
|
||||
|
||||
const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index }));
|
||||
await insertVectorItems(collectionId, items);
|
||||
|
||||
toastr.clear(toast);
|
||||
console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`);
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to vectorize file', error);
|
||||
@@ -377,7 +465,8 @@ async function vectorizeFile(fileText, fileName, collectionId) {
|
||||
async function rearrangeChat(chat) {
|
||||
try {
|
||||
// Clear the extension prompt
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, '', extension_prompt_types.IN_PROMPT, 0, settings.include_wi);
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, '', settings.position, settings.depth, settings.include_wi);
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG_DB, '', settings.file_position_db, settings.file_depth_db, settings.include_wi, settings.file_depth_role_db);
|
||||
|
||||
if (settings.enabled_files) {
|
||||
await processFiles(chat);
|
||||
@@ -526,6 +615,9 @@ function getVectorHeaders() {
|
||||
case 'openai':
|
||||
addOpenAiHeaders(headers);
|
||||
break;
|
||||
case 'cohere':
|
||||
addCohereHeaders(headers);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -564,6 +656,16 @@ function addOpenAiHeaders(headers) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for the Cohere API source.
|
||||
* @param {object} headers Header object
|
||||
*/
|
||||
function addCohereHeaders(headers) {
|
||||
Object.assign(headers, {
|
||||
'X-Cohere-Model': extension_settings.vectors.cohere_model,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts vector items into a collection
|
||||
* @param {string} collectionId - The collection to insert into
|
||||
@@ -575,7 +677,8 @@ async function insertVectorItems(collectionId, items) {
|
||||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
|
||||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] ||
|
||||
settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] ||
|
||||
settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI]) {
|
||||
settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] ||
|
||||
settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE]) {
|
||||
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
|
||||
}
|
||||
|
||||
@@ -649,6 +752,65 @@ async function queryCollection(collectionId, searchText, topK) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries multiple collections for a given text.
|
||||
* @param {string[]} collectionIds - Collection IDs to query
|
||||
* @param {string} searchText - Text to query
|
||||
* @param {number} topK - Number of results to return
|
||||
* @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
|
||||
*/
|
||||
async function queryMultipleCollections(collectionIds, searchText, topK) {
|
||||
const headers = getVectorHeaders();
|
||||
|
||||
const response = await fetch('/api/vector/query-multi', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
collectionIds: collectionIds,
|
||||
searchText: searchText,
|
||||
topK: topK,
|
||||
source: settings.source,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to query multiple collections');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges the vector index for a file.
|
||||
* @param {string} fileUrl File URL to purge
|
||||
*/
|
||||
async function purgeFileVectorIndex(fileUrl) {
|
||||
try {
|
||||
if (!settings.enabled_files) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Vectors: Purging file vector index for ${fileUrl}`);
|
||||
const collectionId = getFileCollectionId(fileUrl);
|
||||
|
||||
const response = await fetch('/api/vector/purge', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
collectionId: collectionId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not delete vector index for collection ${collectionId}`);
|
||||
}
|
||||
|
||||
console.log(`Vectors: Purged vector index for collection ${collectionId}`);
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to purge file', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges the vector index for a collection.
|
||||
* @param {string} collectionId Collection ID to purge
|
||||
@@ -685,6 +847,7 @@ function toggleSettings() {
|
||||
$('#vectors_chats_settings').toggle(!!settings.enabled_chats);
|
||||
$('#together_vectorsModel').toggle(settings.source === 'togetherai');
|
||||
$('#openai_vectorsModel').toggle(settings.source === 'openai');
|
||||
$('#cohere_vectorsModel').toggle(settings.source === 'cohere');
|
||||
$('#nomicai_apiKey').toggle(settings.source === 'nomicai');
|
||||
}
|
||||
|
||||
@@ -728,6 +891,49 @@ async function onViewStatsClick() {
|
||||
|
||||
}
|
||||
|
||||
async function onVectorizeAllFilesClick() {
|
||||
try {
|
||||
const dataBank = getDataBankAttachments();
|
||||
const chatAttachments = getContext().chat.filter(x => x.extra?.file).map(x => x.extra.file);
|
||||
const allFiles = [...dataBank, ...chatAttachments];
|
||||
|
||||
for (const file of allFiles) {
|
||||
const text = await getFileAttachment(file.url);
|
||||
const collectionId = getFileCollectionId(file.url);
|
||||
const hashes = await getSavedHashes(collectionId);
|
||||
|
||||
if (hashes.length) {
|
||||
console.log(`Vectors: File ${file.name} is already vectorized`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await vectorizeFile(text, file.name, collectionId, settings.chunk_size);
|
||||
}
|
||||
|
||||
toastr.success('All files vectorized', 'Vectorization successful');
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to vectorize all files', error);
|
||||
toastr.error('Failed to vectorize all files', 'Vectorization failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function onPurgeFilesClick() {
|
||||
try {
|
||||
const dataBank = getDataBankAttachments();
|
||||
const chatAttachments = getContext().chat.filter(x => x.extra?.file).map(x => x.extra.file);
|
||||
const allFiles = [...dataBank, ...chatAttachments];
|
||||
|
||||
for (const file of allFiles) {
|
||||
await purgeFileVectorIndex(file.url);
|
||||
}
|
||||
|
||||
toastr.success('All files purged', 'Purge successful');
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to purge all files', error);
|
||||
toastr.error('Failed to purge all files', 'Purge failed');
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
if (!extension_settings.vectors) {
|
||||
extension_settings.vectors = settings;
|
||||
@@ -782,6 +988,12 @@ jQuery(async () => {
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_cohere_model').val(settings.cohere_model).on('change', () => {
|
||||
$('#vectors_modelWarning').show();
|
||||
settings.cohere_model = String($('#vectors_cohere_model').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_template').val(settings.template).on('input', () => {
|
||||
settings.template = String($('#vectors_template').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
@@ -816,6 +1028,8 @@ jQuery(async () => {
|
||||
$('#vectors_vectorize_all').on('click', onVectorizeAllClick);
|
||||
$('#vectors_purge').on('click', onPurgeClick);
|
||||
$('#vectors_view_stats').on('click', onViewStatsClick);
|
||||
$('#vectors_files_vectorize_all').on('click', onVectorizeAllFilesClick);
|
||||
$('#vectors_files_purge').on('click', onPurgeFilesClick);
|
||||
|
||||
$('#vectors_size_threshold').val(settings.size_threshold).on('input', () => {
|
||||
settings.size_threshold = Number($('#vectors_size_threshold').val());
|
||||
@@ -871,6 +1085,55 @@ jQuery(async () => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_size_threshold_db').val(settings.size_threshold_db).on('input', () => {
|
||||
settings.size_threshold_db = Number($('#vectors_size_threshold_db').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_chunk_size_db').val(settings.chunk_size_db).on('input', () => {
|
||||
settings.chunk_size_db = Number($('#vectors_chunk_size_db').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_chunk_count_db').val(settings.chunk_count_db).on('input', () => {
|
||||
settings.chunk_count_db = Number($('#vectors_chunk_count_db').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_file_template_db').val(settings.file_template_db).on('input', () => {
|
||||
settings.file_template_db = String($('#vectors_file_template_db').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(`input[name="vectors_file_position_db"][value="${settings.file_position_db}"]`).prop('checked', true);
|
||||
$('input[name="vectors_file_position_db"]').on('change', () => {
|
||||
settings.file_position_db = Number($('input[name="vectors_file_position_db"]:checked').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_file_depth_db').val(settings.file_depth_db).on('input', () => {
|
||||
settings.file_depth_db = Number($('#vectors_file_depth_db').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_file_depth_role_db').val(settings.file_depth_role_db).on('input', () => {
|
||||
settings.file_depth_role_db = Number($('#vectors_file_depth_role_db').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_translate_files').prop('checked', settings.translate_files).on('input', () => {
|
||||
settings.translate_files = !!$('#vectors_translate_files').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
const validSecret = !!secret_state[SECRET_KEYS.NOMICAI];
|
||||
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
|
||||
$('#api_key_nomicai').attr('placeholder', placeholder);
|
||||
@@ -883,4 +1146,5 @@ jQuery(async () => {
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent);
|
||||
eventSource.on(event_types.CHAT_DELETED, purgeVectorIndex);
|
||||
eventSource.on(event_types.GROUP_CHAT_DELETED, purgeVectorIndex);
|
||||
eventSource.on(event_types.FILE_ATTACHMENT_DELETED, purgeFileVectorIndex);
|
||||
});
|
||||
|
@@ -10,13 +10,14 @@
|
||||
Vectorization Source
|
||||
</label>
|
||||
<select id="vectors_source" class="text_pole">
|
||||
<option value="transformers">Local (Transformers)</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
<option value="extras">Extras</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="palm">Google MakerSuite (PaLM)</option>
|
||||
<option value="transformers">Local (Transformers)</option>
|
||||
<option value="mistral">MistralAI</option>
|
||||
<option value="togetherai">TogetherAI</option>
|
||||
<option value="nomicai">NomicAI</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="togetherai">TogetherAI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="openai_vectorsModel">
|
||||
@@ -29,6 +30,20 @@
|
||||
<option value="text-embedding-3-large">text-embedding-3-large</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="cohere_vectorsModel">
|
||||
<label for="vectors_cohere_model">
|
||||
Vectorization Model
|
||||
</label>
|
||||
<select id="vectors_cohere_model" class="text_pole">
|
||||
<option value="embed-english-v3.0">embed-english-v3.0</option>
|
||||
<option value="embed-multilingual-v3.0">embed-multilingual-v3.0</option>
|
||||
<option value="embed-english-light-v3.0">embed-english-light-v3.0</option>
|
||||
<option value="embed-multilingual-light-v3.0">embed-multilingual-light-v3.0</option>
|
||||
<option value="embed-english-v2.0">embed-english-v2.0</option>
|
||||
<option value="embed-english-light-v2.0">embed-english-light-v2.0</option>
|
||||
<option value="embed-multilingual-v2.0">embed-multilingual-v2.0</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="together_vectorsModel">
|
||||
<label for="vectors_togetherai_model">
|
||||
Vectorization Model
|
||||
@@ -91,7 +106,17 @@
|
||||
Enabled for files
|
||||
</label>
|
||||
|
||||
<div id="vectors_files_settings">
|
||||
<div id="vectors_files_settings" class="marginTopBot5">
|
||||
<label class="checkbox_label" for="vectors_translate_files" title="This can help with retrieval accuracy if using embedding models that are trained on English data. Uses the selected API from Chat Translation extension settings.">
|
||||
<input id="vectors_translate_files" type="checkbox" class="checkbox">
|
||||
<span data-i18n="Translate files into English before processing">
|
||||
Translate files into English before processing
|
||||
</span>
|
||||
<i class="fa-solid fa-flask" title="Experimental feature"></i>
|
||||
</label>
|
||||
<div class="flex justifyCenter" title="These settings apply to files attached directly to messages.">
|
||||
<span>Message attachments</span>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="flex1" title="Only files past this size will be vectorized.">
|
||||
<label for="vectors_size_threshold">
|
||||
@@ -112,6 +137,66 @@
|
||||
<input id="vectors_chunk_count" type="number" class="text_pole widthUnset" min="1" max="99999" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justifyCenter" title="These settings apply to files stored in the Data Bank.">
|
||||
<span>Data Bank files</span>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="flex1" title="Only files past this size will be vectorized.">
|
||||
<label for="vectors_size_threshold_db">
|
||||
<small>Size threshold (KB)</small>
|
||||
</label>
|
||||
<input id="vectors_size_threshold_db" type="number" class="text_pole widthUnset" min="1" max="99999" />
|
||||
</div>
|
||||
<div class="flex1" title="Chunk size for file splitting.">
|
||||
<label for="vectors_chunk_size_db">
|
||||
<small>Chunk size (chars)</small>
|
||||
</label>
|
||||
<input id="vectors_chunk_size_db" type="number" class="text_pole widthUnset" min="1" max="99999" />
|
||||
</div>
|
||||
<div class="flex1" title="How many chunks to retrieve when querying.">
|
||||
<label for="vectors_chunk_count_db">
|
||||
<small>Retrieve chunks</small>
|
||||
</label>
|
||||
<input id="vectors_chunk_count_db" type="number" class="text_pole widthUnset" min="1" max="99999" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="vectors_file_template_db">
|
||||
<span>Injection Template</span>
|
||||
</label>
|
||||
<textarea id="vectors_file_template_db" class="margin0 text_pole textarea_compact" rows="3" placeholder="Use {{text}} macro to specify the position of retrieved text."></textarea>
|
||||
<label for="vectors_file_position_db">Injection Position</label>
|
||||
<div class="radio_group">
|
||||
<label>
|
||||
<input type="radio" name="vectors_file_position_db" value="2" />
|
||||
<span>Before Main Prompt / Story String</span>
|
||||
</label>
|
||||
<!--Keep these as 0 and 1 to interface with the setExtensionPrompt function-->
|
||||
<label>
|
||||
<input type="radio" name="vectors_file_position_db" value="0" />
|
||||
<span>After Main Prompt / Story String</span>
|
||||
</label>
|
||||
<label for="vectors_file_depth_db" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
|
||||
<input type="radio" name="vectors_file_position_db" value="1" />
|
||||
<span>In-chat @ Depth</span>
|
||||
<input id="vectors_file_depth_db" class="text_pole widthUnset" type="number" min="0" max="999" />
|
||||
<span>as</span>
|
||||
<select id="vectors_file_depth_role_db" class="text_pole widthNatural">
|
||||
<option value="0">System</option>
|
||||
<option value="1">User</option>
|
||||
<option value="2">Assistant</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div id="vectors_files_vectorize_all" class="menu_button menu_button_icon" title="Vectorize all files in the Data Bank and current chat.">
|
||||
Vectorize All
|
||||
</div>
|
||||
<div id="vectors_files_purge" class="menu_button menu_button_icon" title="Purge all file vectors in the Data Bank and current chat.">
|
||||
Purge Vectors
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
@@ -129,9 +214,9 @@
|
||||
<div id="vectors_chats_settings">
|
||||
<div id="vectors_advanced_settings">
|
||||
<label for="vectors_template">
|
||||
Insertion Template
|
||||
Injection Template
|
||||
</label>
|
||||
<textarea id="vectors_template" class="text_pole textarea_compact" rows="3" placeholder="Use {{text}} macro to specify the position of retrieved text."></textarea>
|
||||
<textarea id="vectors_template" class="text_pole textarea_compact" rows="3" placeholder="Use {{text}} macro to specify the position of retrieved text."></textarea>
|
||||
<label for="vectors_position">Injection Position</label>
|
||||
<div class="radio_group">
|
||||
<label>
|
||||
|
@@ -188,9 +188,7 @@ export async function getGroupChat(groupId, reload = false) {
|
||||
|
||||
if (Array.isArray(data) && data.length) {
|
||||
data[0].is_group = true;
|
||||
for (let key of data) {
|
||||
chat.push(key);
|
||||
}
|
||||
chat.splice(0, chat.length, ...data);
|
||||
await printMessages();
|
||||
} else {
|
||||
sendSystemMessage(system_message_types.GROUP, '', { isSmallSys: true });
|
||||
|
@@ -5,21 +5,18 @@ export function showLoader() {
|
||||
const loader = $('<div></div>').attr('id', 'load-spinner').addClass('fa-solid fa-gear fa-spin fa-3x');
|
||||
container.append(loader);
|
||||
$('body').append(container);
|
||||
|
||||
}
|
||||
|
||||
export function hideLoader() {
|
||||
export async function hideLoader() {
|
||||
//Sets up a 2-step animation. Spinner blurs/fades out, and then the loader shadow does the same.
|
||||
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
|
||||
//console.log('FADING BLUR SCREEN')
|
||||
$(`#${ELEMENT_ID}`)
|
||||
//only fade out the spinner and replace with login screen
|
||||
.animate({ opacity: 0 }, 300, function () {
|
||||
//console.log('REMOVING LOADER')
|
||||
$(`#${ELEMENT_ID}`).remove();
|
||||
});
|
||||
});
|
||||
|
||||
//console.log('BLURRING SPINNER')
|
||||
$('#load-spinner')
|
||||
.css({
|
||||
'filter': 'blur(15px)',
|
||||
|
276
public/scripts/login.js
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* CRSF token for requests.
|
||||
*/
|
||||
let csrfToken = '';
|
||||
let discreetLogin = false;
|
||||
|
||||
/**
|
||||
* Gets a CSRF token from the server.
|
||||
* @returns {Promise<string>} CSRF token
|
||||
*/
|
||||
async function getCsrfToken() {
|
||||
const response = await fetch('/csrf-token');
|
||||
const data = await response.json();
|
||||
return data.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of users from the server.
|
||||
* @returns {Promise<object>} List of users
|
||||
*/
|
||||
async function getUserList() {
|
||||
const response = await fetch('/api/users/list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
return displayError(errorData.error || 'An error occurred');
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
discreetLogin = true;
|
||||
return [];
|
||||
}
|
||||
|
||||
const userListObj = await response.json();
|
||||
console.log(userListObj);
|
||||
return userListObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a recovery code for the user.
|
||||
* @param {string} handle User handle
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendRecoveryPart1(handle) {
|
||||
const response = await fetch('/api/users/recover-step1', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({ handle }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
return displayError(errorData.error || 'An error occurred');
|
||||
}
|
||||
|
||||
showRecoveryBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new password for the user using the recovery code.
|
||||
* @param {string} handle User handle
|
||||
* @param {string} code Recovery code
|
||||
* @param {string} newPassword New password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendRecoveryPart2(handle, code, newPassword) {
|
||||
const recoveryData = {
|
||||
handle,
|
||||
code,
|
||||
newPassword,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/users/recover-step2', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(recoveryData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
return displayError(errorData.error || 'An error occurred');
|
||||
}
|
||||
|
||||
console.log(`Successfully recovered password for ${handle}!`);
|
||||
await performLogin(handle, newPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to log in the user.
|
||||
* @param {string} handle User's handle
|
||||
* @param {string} password User's password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function performLogin(handle, password) {
|
||||
const userInfo = {
|
||||
handle: handle,
|
||||
password: password,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(userInfo),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
return displayError(errorData.error || 'An error occurred');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.handle) {
|
||||
console.log(`Successfully logged in as ${handle}!`);
|
||||
redirectToHome();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error logging in:', error);
|
||||
displayError(String(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the user selection event.
|
||||
* @param {object} user User object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function onUserSelected(user) {
|
||||
// No password, just log in
|
||||
if (!user.password) {
|
||||
return await performLogin(user.handle, '');
|
||||
}
|
||||
|
||||
$('#passwordRecoveryBlock').hide();
|
||||
$('#passwordEntryBlock').show();
|
||||
$('#loginButton').off('click').on('click', async () => {
|
||||
const password = String($('#userPassword').val());
|
||||
await performLogin(user.handle, password);
|
||||
});
|
||||
|
||||
$('#recoverPassword').off('click').on('click', async () => {
|
||||
await sendRecoveryPart1(user.handle);
|
||||
});
|
||||
|
||||
$('#sendRecovery').off('click').on('click', async () => {
|
||||
const code = String($('#recoveryCode').val());
|
||||
const newPassword = String($('#newPassword').val());
|
||||
await sendRecoveryPart2(user.handle, code, newPassword);
|
||||
});
|
||||
|
||||
displayError('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message to the user.
|
||||
* @param {string} message Error message
|
||||
*/
|
||||
function displayError(message) {
|
||||
$('#errorMessage').text(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the user to the home page.
|
||||
* Preserves the query string.
|
||||
*/
|
||||
function redirectToHome() {
|
||||
window.location.href = '/' + window.location.search;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the password entry block and shows the password recovery block.
|
||||
*/
|
||||
function showRecoveryBlock() {
|
||||
$('#passwordEntryBlock').hide();
|
||||
$('#passwordRecoveryBlock').show();
|
||||
displayError('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the password recovery block and shows the password entry block.
|
||||
*/
|
||||
function onCancelRecoveryClick() {
|
||||
$('#passwordRecoveryBlock').hide();
|
||||
$('#passwordEntryBlock').show();
|
||||
displayError('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the login page for normal login.
|
||||
* @param {import('../../src/users').UserViewModel[]} userList List of users
|
||||
*/
|
||||
function configureNormalLogin(userList) {
|
||||
console.log('Discreet login is disabled');
|
||||
$('#handleEntryBlock').hide();
|
||||
$('#normalLoginPrompt').show();
|
||||
$('#discreetLoginPrompt').hide();
|
||||
console.log(userList);
|
||||
for (const user of userList) {
|
||||
const userBlock = $('<div></div>').addClass('userSelect');
|
||||
const avatarBlock = $('<div></div>').addClass('avatar');
|
||||
avatarBlock.append($('<img>').attr('src', user.avatar));
|
||||
userBlock.append(avatarBlock);
|
||||
userBlock.append($('<span></span>').addClass('userName').text(user.name));
|
||||
userBlock.append($('<small></small>').addClass('userHandle').text(user.handle));
|
||||
userBlock.on('click', () => onUserSelected(user));
|
||||
$('#userList').append(userBlock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the login page for discreet login.
|
||||
*/
|
||||
function configureDiscreetLogin() {
|
||||
console.log('Discreet login is enabled');
|
||||
$('#handleEntryBlock').show();
|
||||
$('#normalLoginPrompt').hide();
|
||||
$('#discreetLoginPrompt').show();
|
||||
$('#userList').hide();
|
||||
$('#passwordRecoveryBlock').hide();
|
||||
$('#passwordEntryBlock').show();
|
||||
$('#loginButton').off('click').on('click', async () => {
|
||||
const handle = String($('#userHandle').val());
|
||||
const password = String($('#userPassword').val());
|
||||
await performLogin(handle, password);
|
||||
});
|
||||
|
||||
$('#recoverPassword').off('click').on('click', async () => {
|
||||
const handle = String($('#userHandle').val());
|
||||
await sendRecoveryPart1(handle);
|
||||
});
|
||||
|
||||
$('#sendRecovery').off('click').on('click', async () => {
|
||||
const handle = String($('#userHandle').val());
|
||||
const code = String($('#recoveryCode').val());
|
||||
const newPassword = String($('#newPassword').val());
|
||||
await sendRecoveryPart2(handle, code, newPassword);
|
||||
});
|
||||
}
|
||||
|
||||
(async function () {
|
||||
csrfToken = await getCsrfToken();
|
||||
const userList = await getUserList();
|
||||
|
||||
if (discreetLogin) {
|
||||
configureDiscreetLogin();
|
||||
} else {
|
||||
configureNormalLogin(userList);
|
||||
}
|
||||
document.getElementById('shadow_popup').style.opacity = '';
|
||||
$('#cancelRecovery').on('click', onCancelRecoveryClick);
|
||||
$(document).on('keydown', (evt) => {
|
||||
if (evt.key === 'Enter' && document.activeElement.tagName === 'INPUT') {
|
||||
if ($('#passwordRecoveryBlock').is(':visible')) {
|
||||
$('#sendRecovery').trigger('click');
|
||||
} else {
|
||||
$('#loginButton').trigger('click');
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
@@ -129,8 +129,8 @@ function addKeyboardProps(element) {
|
||||
* selected token highlighted. If no token is selected, the subview is hidden.
|
||||
*/
|
||||
function renderTopLogprobs() {
|
||||
$('#logprobs_top_logprobs_hint').hide();
|
||||
const view = $('.logprobs_candidate_list');
|
||||
const hint = $('#logprobs_top_logprobs_hint').hide();
|
||||
view.empty();
|
||||
|
||||
if (!state.selectedTokenLogprobs) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId } from '../script.js';
|
||||
import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js';
|
||||
import { timestampToMoment, isDigitsOnly, getStringHash } from './utils.js';
|
||||
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
|
||||
import { replaceInstructMacros } from './instruct-mode.js';
|
||||
@@ -6,6 +6,12 @@ import { replaceVariableMacros } from './variables.js';
|
||||
|
||||
// Register any macro that you want to leave in the compiled story string
|
||||
Handlebars.registerHelper('trim', () => '{{trim}}');
|
||||
// Catch-all helper for any macro that is not defined for story strings
|
||||
Handlebars.registerHelper('helperMissing', function () {
|
||||
const options = arguments[arguments.length - 1];
|
||||
const macroName = options.name;
|
||||
return substituteParams(`{{${macroName}}}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets a hashed id of the current chat from the metadata.
|
||||
|
@@ -31,7 +31,7 @@ import {
|
||||
system_message_types,
|
||||
this_chid,
|
||||
} from '../script.js';
|
||||
import { groups, selected_group } from './group-chats.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
|
||||
import {
|
||||
@@ -172,6 +172,7 @@ export const chat_completion_sources = {
|
||||
MISTRALAI: 'mistralai',
|
||||
CUSTOM: 'custom',
|
||||
COHERE: 'cohere',
|
||||
PERPLEXITY: 'perplexity',
|
||||
};
|
||||
|
||||
const character_names_behavior = {
|
||||
@@ -238,6 +239,7 @@ const default_settings = {
|
||||
ai21_model: 'j2-ultra',
|
||||
mistralai_model: 'mistral-medium-latest',
|
||||
cohere_model: 'command-r',
|
||||
perplexity_model: 'llama-3-70b-instruct',
|
||||
custom_model: '',
|
||||
custom_url: '',
|
||||
custom_include_body: '',
|
||||
@@ -310,6 +312,7 @@ const oai_settings = {
|
||||
ai21_model: 'j2-ultra',
|
||||
mistralai_model: 'mistral-medium-latest',
|
||||
cohere_model: 'command-r',
|
||||
perplexity_model: 'llama-3-70b-instruct',
|
||||
custom_model: '',
|
||||
custom_url: '',
|
||||
custom_include_body: '',
|
||||
@@ -1006,6 +1009,15 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
|
||||
}
|
||||
}
|
||||
|
||||
// Vectors Data Bank
|
||||
if (prompts.has('vectorsDataBank')) {
|
||||
const vectorsDataBank = prompts.get('vectorsDataBank');
|
||||
|
||||
if (vectorsDataBank.position) {
|
||||
chatCompletion.insert(Message.fromPrompt(vectorsDataBank), 'main', vectorsDataBank.position);
|
||||
}
|
||||
}
|
||||
|
||||
// Smart Context (ChromaDB)
|
||||
if (prompts.has('smartContext')) {
|
||||
const smartContext = prompts.get('smartContext');
|
||||
@@ -1097,6 +1109,14 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
|
||||
position: getPromptPosition(vectorsMemory.position),
|
||||
});
|
||||
|
||||
const vectorsDataBank = extensionPrompts['4_vectors_data_bank'];
|
||||
if (vectorsDataBank && vectorsDataBank.value) systemPrompts.push({
|
||||
role: getPromptRole(vectorsDataBank.role),
|
||||
content: vectorsDataBank.value,
|
||||
identifier: 'vectorsDataBank',
|
||||
position: getPromptPosition(vectorsDataBank.position),
|
||||
});
|
||||
|
||||
// Smart Context (ChromaDB)
|
||||
const smartContext = extensionPrompts['chromadb'];
|
||||
if (smartContext && smartContext.value) systemPrompts.push({
|
||||
@@ -1410,6 +1430,8 @@ function getChatCompletionModel() {
|
||||
return oai_settings.custom_model;
|
||||
case chat_completion_sources.COHERE:
|
||||
return oai_settings.cohere_model;
|
||||
case chat_completion_sources.PERPLEXITY:
|
||||
return oai_settings.perplexity_model;
|
||||
default:
|
||||
throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`);
|
||||
}
|
||||
@@ -1602,6 +1624,11 @@ async function sendAltScaleRequest(messages, logit_bias, signal, type) {
|
||||
signal: signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
tryParseStreamingError(response, await response.text());
|
||||
throw new Error('Scale response does not indicate success.');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.output;
|
||||
}
|
||||
@@ -1630,6 +1657,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
const isMistral = oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI;
|
||||
const isCustom = oai_settings.chat_completion_source == chat_completion_sources.CUSTOM;
|
||||
const isCohere = oai_settings.chat_completion_source == chat_completion_sources.COHERE;
|
||||
const isPerplexity = oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY;
|
||||
const isTextCompletion = (isOAI && textCompletionModels.includes(oai_settings.openai_model)) || (isOpenRouter && oai_settings.openrouter_force_instruct && power_user.instruct.enabled);
|
||||
const isQuiet = type === 'quiet';
|
||||
const isImpersonate = type === 'impersonate';
|
||||
@@ -1777,6 +1805,16 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
generate_data['websearch'] = oai_settings.websearch_cohere;
|
||||
}
|
||||
|
||||
if (isPerplexity) {
|
||||
generate_data['top_k'] = Number(oai_settings.top_k_openai);
|
||||
// Normalize values. 1 == disabled. 0 == is usual disabled state in OpenAI.
|
||||
generate_data['frequency_penalty'] = Math.max(0, Number(oai_settings.freq_pen_openai)) + 1;
|
||||
generate_data['presence_penalty'] = Number(oai_settings.pres_pen_openai);
|
||||
|
||||
// YEAH BRO JUST USE OPENAI CLIENT BRO
|
||||
delete generate_data['stop'];
|
||||
}
|
||||
|
||||
if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere) && oai_settings.seed >= 0) {
|
||||
generate_data['seed'] = oai_settings.seed;
|
||||
}
|
||||
@@ -1847,7 +1885,7 @@ function getStreamingReply(data) {
|
||||
} else if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
|
||||
return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||
} else {
|
||||
return data.choices[0]?.delta?.content || data.choices[0]?.message?.content || data.choices[0]?.text || '';
|
||||
return data.choices[0]?.delta?.content ?? data.choices[0]?.message?.content ?? data.choices[0]?.text ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2643,6 +2681,7 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model;
|
||||
oai_settings.mistralai_model = settings.mistralai_model ?? default_settings.mistralai_model;
|
||||
oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;
|
||||
oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model;
|
||||
oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
|
||||
oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
|
||||
oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;
|
||||
@@ -2708,6 +2747,8 @@ function loadOpenAISettings(data, settings) {
|
||||
$(`#model_mistralai_select option[value="${oai_settings.mistralai_model}"`).attr('selected', true);
|
||||
$('#model_cohere_select').val(oai_settings.cohere_model);
|
||||
$(`#model_cohere_select option[value="${oai_settings.cohere_model}"`).attr('selected', true);
|
||||
$('#model_perplexity_select').val(oai_settings.perplexity_model);
|
||||
$(`#model_perplexity_select option[value="${oai_settings.perplexity_model}"`).attr('selected', true);
|
||||
$('#custom_model_id').val(oai_settings.custom_model);
|
||||
$('#custom_api_url_text').val(oai_settings.custom_url);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context);
|
||||
@@ -2857,7 +2898,7 @@ async function getStatusOpen() {
|
||||
return resultCheckStatus();
|
||||
}
|
||||
|
||||
const noValidateSources = [chat_completion_sources.SCALE, chat_completion_sources.CLAUDE, chat_completion_sources.AI21, chat_completion_sources.MAKERSUITE];
|
||||
const noValidateSources = [chat_completion_sources.SCALE, chat_completion_sources.CLAUDE, chat_completion_sources.AI21, chat_completion_sources.MAKERSUITE, chat_completion_sources.PERPLEXITY];
|
||||
if (noValidateSources.includes(oai_settings.chat_completion_source)) {
|
||||
let status = 'Unable to verify key; press "Test Message" to validate.';
|
||||
setOnlineStatus(status);
|
||||
@@ -2948,6 +2989,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
ai21_model: settings.ai21_model,
|
||||
mistralai_model: settings.mistralai_model,
|
||||
cohere_model: settings.cohere_model,
|
||||
perplexity_model: settings.perplexity_model,
|
||||
custom_model: settings.custom_model,
|
||||
custom_url: settings.custom_url,
|
||||
custom_include_body: settings.custom_include_body,
|
||||
@@ -3340,6 +3382,7 @@ function onSettingsPresetChange() {
|
||||
ai21_model: ['#model_ai21_select', 'ai21_model', false],
|
||||
mistralai_model: ['#model_mistralai_select', 'mistralai_model', false],
|
||||
cohere_model: ['#model_cohere_select', 'cohere_model', false],
|
||||
perplexity_model: ['#model_perplexity_select', 'perplexity_model', false],
|
||||
custom_model: ['#custom_model_id', 'custom_model', false],
|
||||
custom_url: ['#custom_api_url_text', 'custom_url', false],
|
||||
custom_include_body: ['#custom_include_body', 'custom_include_body', false],
|
||||
@@ -3563,6 +3606,11 @@ async function onModelChange() {
|
||||
oai_settings.cohere_model = value;
|
||||
}
|
||||
|
||||
if ($(this).is('#model_perplexity_select')) {
|
||||
console.log('Perplexity model changed to', value);
|
||||
oai_settings.perplexity_model = value;
|
||||
}
|
||||
|
||||
if (value && $(this).is('#model_custom_select')) {
|
||||
console.log('Custom model changed to', value);
|
||||
oai_settings.custom_model = value;
|
||||
@@ -3577,6 +3625,7 @@ async function onModelChange() {
|
||||
}
|
||||
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
|
||||
@@ -3704,6 +3753,29 @@ async function onModelChange() {
|
||||
}
|
||||
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
}
|
||||
else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', max_16k);
|
||||
}
|
||||
else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
}
|
||||
else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', 12000);
|
||||
}
|
||||
else {
|
||||
$('#openai_max_context').attr('max', max_4k);
|
||||
}
|
||||
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
|
||||
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
|
||||
@@ -3742,6 +3814,7 @@ async function onModelChange() {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
|
||||
$('#openai_max_context_counter').attr('max', Number($('#openai_max_context').attr('max')));
|
||||
@@ -3912,6 +3985,19 @@ async function onConnectButtonClick(e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) {
|
||||
const api_key_perplexity = String($('#api_key_perplexity').val()).trim();
|
||||
|
||||
if (api_key_perplexity.length) {
|
||||
await writeSecret(SECRET_KEYS.PERPLEXITY, api_key_perplexity);
|
||||
}
|
||||
|
||||
if (!secret_state[SECRET_KEYS.PERPLEXITY]) {
|
||||
console.log('No secret key saved for Perplexity');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
startStatusLoading();
|
||||
saveSettingsDebounced();
|
||||
await getStatusOpen();
|
||||
@@ -3950,6 +4036,9 @@ function toggleChatCompletionForms() {
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
|
||||
$('#model_cohere_select').trigger('change');
|
||||
}
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) {
|
||||
$('#model_perplexity_select').trigger('change');
|
||||
}
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
|
||||
$('#model_custom_select').trigger('change');
|
||||
}
|
||||
@@ -4619,6 +4708,7 @@ $(document).ready(async function () {
|
||||
$('#model_ai21_select').on('change', onModelChange);
|
||||
$('#model_mistralai_select').on('change', onModelChange);
|
||||
$('#model_cohere_select').on('change', onModelChange);
|
||||
$('#model_perplexity_select').on('change', onModelChange);
|
||||
$('#model_custom_select').on('change', onModelChange);
|
||||
$('#settings_preset_openai').on('change', onSettingsPresetChange);
|
||||
$('#new_oai_preset').on('click', onNewPresetClick);
|
||||
|