mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-28 01:40:12 +01:00
commit
736223a86a
@ -12,7 +12,7 @@ module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
// Server-side files (plus this configuration file)
|
||||
files: ['src/**/*.js', './*.js'],
|
||||
files: ['src/**/*.js', './*.js', 'plugins/**/*.js'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -27,7 +27,8 @@ public/stats.json
|
||||
public/settings.json
|
||||
/thumbnails
|
||||
whitelist.txt
|
||||
.vscode
|
||||
.vscode/**
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
secrets.json
|
||||
/dist
|
||||
@ -41,3 +42,4 @@ access.log
|
||||
/vectors/
|
||||
/cache/
|
||||
public/css/user.css
|
||||
/plugins/
|
||||
|
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig"
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": []
|
||||
}
|
@ -31,10 +31,9 @@ RUN \
|
||||
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" "public/css/bg_load.css" || true && \
|
||||
rm -f "config.yaml" "public/settings.json" || true && \
|
||||
ln -s "./config/config.yaml" "config.yaml" || true && \
|
||||
ln -s "../config/settings.json" "public/settings.json" || true && \
|
||||
ln -s "../../config/bg_load.css" "public/css/bg_load.css" || true && \
|
||||
mkdir "config" || true
|
||||
|
||||
# Cleanup unnecessary files
|
||||
|
@ -1 +0,0 @@
|
||||
#bg1 {background-image: url(../backgrounds/__transparent.png);}
|
@ -25,6 +25,9 @@ autorun: true
|
||||
disableThumbnails: false
|
||||
# Thumbnail quality (0-100)
|
||||
thumbnailsQuality: 95
|
||||
# Generate avatar thumbnails as PNG instead of JPG (preserves transparency but increases filesize by about 100%)
|
||||
# Changing this only affects new thumbnails. To recreate the old ones, clear out your ST/thumbnails/ folder.
|
||||
avatarThumbnailsPng: false
|
||||
# Allow secret keys exposure via API
|
||||
allowKeysExposure: false
|
||||
# Skip new default content checks
|
||||
@ -54,7 +57,13 @@ extras:
|
||||
openai:
|
||||
# Will send a random user ID to OpenAI completion API
|
||||
randomizeUserId: false
|
||||
# If not empty, will add this as a system message to the start of every caption completion prompt
|
||||
# Example: "Perform the instructions to the best of your ability.\n" (for LLaVA)
|
||||
# Not used in image inlining mode
|
||||
captionSystemPrompt: ""
|
||||
# -- DEEPL TRANSLATION CONFIGURATION --
|
||||
deepl:
|
||||
# Available options: default, more, less, prefer_more, prefer_less
|
||||
formality: default
|
||||
# -- SERVER PLUGIN CONFIGURATION --
|
||||
enableServerPlugins: false
|
||||
|
@ -2,8 +2,6 @@
|
||||
"firstRun": true,
|
||||
"username": "User",
|
||||
"api_server": "http://127.0.0.1:5000/api",
|
||||
"api_server_textgenerationwebui": "http://127.0.0.1:5000/api",
|
||||
"api_use_mancer_webui": false,
|
||||
"preset_settings": "RecoveredRuins",
|
||||
"user_avatar": "user-default.png",
|
||||
"amount_gen": 250,
|
||||
@ -596,7 +594,6 @@
|
||||
"openrouter_model": "OR_Website",
|
||||
"jailbreak_system": true,
|
||||
"reverse_proxy": "",
|
||||
"legacy_streaming": false,
|
||||
"chat_completion_source": "openai",
|
||||
"max_context_unlocked": false,
|
||||
"api_url_scale": "",
|
||||
|
@ -19,11 +19,6 @@ if [ ! -e "config/settings.json" ]; then
|
||||
cp -r "default/settings.json" "config/settings.json"
|
||||
fi
|
||||
|
||||
if [ ! -e "config/bg_load.css" ]; then
|
||||
echo "Resource not found, copying from defaults: bg_load.css"
|
||||
cp -r "default/bg_load.css" "config/bg_load.css"
|
||||
fi
|
||||
|
||||
CONFIG_FILE="config.yaml"
|
||||
|
||||
echo "Starting with the following config:"
|
||||
|
13
package-lock.json
generated
13
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sillytavern",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sillytavern",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@ -20,7 +20,6 @@
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csrf-csrf": "^2.2.3",
|
||||
"device-detector-js": "^3.0.3",
|
||||
"express": "^4.18.2",
|
||||
"form-data": "^4.0.0",
|
||||
"google-translate-api-browser": "^3.0.1",
|
||||
@ -1777,14 +1776,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/device-detector-js": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/device-detector-js/-/device-detector-js-3.0.3.tgz",
|
||||
"integrity": "sha512-jM89LJAvP6uOd84at8OlD9dWP8KeYCCHUde0RT0HQo/stdoRH4b54Xl/fntx2nEXCmqiFhmo+/cJetS2VGUHPw==",
|
||||
"engines": {
|
||||
"node": ">= 8.11.4"
|
||||
}
|
||||
},
|
||||
"node_modules/digest-fetch": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz",
|
||||
|
@ -10,7 +10,6 @@
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csrf-csrf": "^2.2.3",
|
||||
"device-detector-js": "^3.0.3",
|
||||
"express": "^4.18.2",
|
||||
"form-data": "^4.0.0",
|
||||
"google-translate-api-browser": "^3.0.1",
|
||||
@ -52,7 +51,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/SillyTavern/SillyTavern.git"
|
||||
},
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"start-multi": "node server.js --disableCsrf",
|
||||
|
0
plugins/.gitkeep
Normal file
0
plugins/.gitkeep
Normal file
@ -107,7 +107,6 @@ function addMissingConfigValues() {
|
||||
function createDefaultFiles() {
|
||||
const files = {
|
||||
settings: './public/settings.json',
|
||||
bg_load: './public/css/bg_load.css',
|
||||
config: './config.yaml',
|
||||
user: './public/css/user.css',
|
||||
};
|
||||
@ -168,6 +167,29 @@ 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();
|
||||
@ -177,6 +199,8 @@ try {
|
||||
copyWasmFiles();
|
||||
// 3. Add missing config values
|
||||
addMissingConfigValues();
|
||||
// 4. Migrate bg_load.css to settings.json
|
||||
migrateBackground();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
#loader {
|
||||
#loader, #preloader {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@ -22,4 +22,4 @@
|
||||
#load-spinner {
|
||||
transition: all 300ms ease-out;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
/*will apply to anything 1000px or less. this catches ipads, horizontal phones, and vertical phones)*/
|
||||
@media screen and (max-width: 1000px) {
|
||||
#send_form.compact #leftSendForm, #send_form.compact #rightSendForm {
|
||||
flex-wrap: nowrap;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.bg_button {
|
||||
font-size: 15px;
|
||||
|
@ -121,7 +121,7 @@
|
||||
}
|
||||
|
||||
/* Add the custom checkbox */
|
||||
.select2-results__option:before {
|
||||
.select2-results__option::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
@ -141,11 +141,19 @@
|
||||
}
|
||||
|
||||
/* Add the custom checkbox checkmark */
|
||||
.select2-results__option--selected.select2-results__option:before {
|
||||
.select2-results__option--selected.select2-results__option::before {
|
||||
content: '\2713';
|
||||
font-weight: bold;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--SmartThemeBlurTintColor);
|
||||
text-align: center;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.select2-results__option.select2-results__message {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.select2-results__option.select2-results__message::before {
|
||||
display: none;
|
||||
}
|
||||
|
39
public/img/llamacpp.svg
Normal file
39
public/img/llamacpp.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.0"
|
||||
width="350.95343pt"
|
||||
height="433.92468pt"
|
||||
viewBox="0 0 350.95343 433.92468"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="svg3"
|
||||
sodipodi:docname="llamacpp.svg"
|
||||
inkscape:version="1.3 (0e150ed, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs3" />
|
||||
<sodipodi:namedview
|
||||
id="namedview3"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="pt"
|
||||
inkscape:zoom="0.61795062"
|
||||
inkscape:cx="360.87026"
|
||||
inkscape:cy="319.60482"
|
||||
inkscape:window-width="1280"
|
||||
inkscape:window-height="688"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3" />
|
||||
<path
|
||||
id="path15"
|
||||
d="M 115.66411,7.7769089e-4 C 108.03646,-0.04647231 97.66356,2.0614588 89.22605,5.7471588 51.629188,22.170371 29.279858,72.255744 26.302778,146.75404 l -1.08171,27.05939 10.19027,-9.11895 c 16.68028,-14.92501 43.7359,-30.80127 65.967952,-38.71307 3.63963,-1.29525 7.39727,-3.00875 8.34819,-3.80665 1.00889,-0.84654 -0.84203,-6.76797 -4.44134,-14.21878 -5.75466,-11.912432 -6.12062,-13.824142 -5.45304,-28.480056 0.68369,-15.00947 1.27807,-16.84384 13.33674,-41.2326 C 128.87131,6.4869918 129.50802,4.3066778 123.92323,1.4548327 122.03009,0.48812169 119.13122,0.02222669 115.66411,7.7769089e-4 Z M 204.3319,24.868452 c -7.90831,-0.07627 -17.36177,1.199451 -23.54292,3.870384 -18.58511,8.030767 -38.06958,36.609918 -47.25132,69.305902 -2.22908,7.937702 -4.5161,15.970742 -5.08401,17.852392 -0.86974,2.88178 -0.32873,3.22525 3.43601,2.17653 2.45813,-0.68477 18.29522,-1.73488 35.1935,-2.33437 16.89826,-0.59952 30.72354,-1.40131 30.72354,-1.78192 0,-0.38061 -1.78758,-5.74168 -3.97051,-11.9117 -6.54342,-18.495036 -4.8829,-25.966506 11.1988,-50.400166 7.46265,-11.33831 13.56896,-21.480943 13.56896,-22.542378 0,-2.73047 -6.36368,-4.158497 -14.27205,-4.234674 z M 168.50212,145.23018 c -45.12449,0.0128 -76.75805,10.98462 -110.460932,38.31236 -22.62195,18.34285 -45.99259,54.10069 -54.3650997,83.1786 -4.94441,17.17201 -4.88874,65.42308 0.0924,79.37804 16.4963297,46.21663 57.3528097,79.08349 107.4639617,86.44794 32.21284,4.73407 74.8601,-2.95259 109.24245,-19.68893 l 7.20925,-3.50917 -4.64502,-17.64293 c -2.55479,-9.70397 -5.46337,-20.62804 -6.46485,-24.27571 l -1.82292,-6.63282 -14.30391,6.30496 c -22.86829,10.08133 -41.37356,13.8047 -63.89044,12.8558 -13.70887,-0.57772 -22.19455,-1.94878 -30.04268,-4.85697 -14.96555,-5.54563 -31.436082,-20.30658 -37.827792,-33.90468 -16.63575,-35.39192 -7.26602,-83.4333 21.984032,-112.712 34.5434,-34.57726 78.91103,-41.04325 127.6377,-18.6022 9.71534,4.47445 18.40283,7.701 19.30836,7.16708 1.84426,-1.08761 26.365,-41.92583 26.365,-43.91001 0,-1.77105 -17.98211,-11.91179 -29.15193,-16.43783 -20.81281,-8.43331 -38.421,-11.4793 -66.32745,-11.47153 z m -4.7277,92.6254 v 17.13902 17.13905 h -17.96261 -17.96264 v 15.33281 15.33588 h 17.96264 17.96261 v 17.13903 17.13599 h 16.06964 16.07283 v -17.13599 -17.13903 h 17.01451 17.0178 V 287.46646 272.13365 H 212.9314 195.91689 V 254.9946 237.85558 h -16.07283 z m 121.00426,0 v 17.13902 17.13905 h -17.95945 -17.96254 v 15.33281 15.33588 h 17.96254 17.95945 v 17.13903 17.13599 h 15.12793 15.12482 v -17.13599 -17.13903 h 17.96254 17.95945 V 287.46646 272.13365 H 332.99397 315.03143 V 254.9946 237.85558 h -15.12482 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
38
public/img/makersuite.svg
Normal file
38
public/img/makersuite.svg
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="590.000000pt" height="589.000000pt" viewBox="0 0 590.000000 589.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,589.000000) scale(0.100000,-0.100000)" stroke="none">
|
||||
<path d="M4436 5263 c-22 -2 -49 -11 -60 -19 -11 -8 -30 -14 -43 -14 -12 0
|
||||
-32 -4 -45 -9 -42 -16 -113 -43 -133 -51 -61 -23 -191 -92 -267 -143 -181
|
||||
-122 -327 -258 -472 -441 -71 -88 -216 -297 -216 -309 0 -4 -15 -30 -33 -59
|
||||
-73 -115 -212 -419 -256 -558 -6 -19 -21 -64 -32 -100 -12 -36 -25 -81 -30
|
||||
-100 -22 -96 -28 -122 -47 -215 -46 -228 -66 -517 -52 -743 15 -221 48 -473
|
||||
71 -530 5 -13 9 -33 9 -44 0 -12 4 -35 9 -52 6 -17 17 -58 27 -91 73 -263 189
|
||||
-540 299 -714 31 -50 136 -145 217 -197 38 -24 70 -44 73 -44 3 0 22 -10 43
|
||||
-23 42 -26 202 -92 247 -102 16 -4 33 -12 37 -18 5 -8 58 -12 170 -12 l163 0
|
||||
97 48 c76 37 111 62 169 120 71 70 179 221 179 249 0 8 4 18 9 23 30 33 61
|
||||
177 61 282 0 95 -4 121 -41 241 -60 197 -199 365 -390 475 -24 14 -80 45 -124
|
||||
70 -327 184 -411 248 -422 319 -9 60 2 80 81 145 148 124 337 231 541 308 55
|
||||
20 108 41 117 46 10 5 27 9 37 9 11 0 23 5 26 10 3 6 14 10 22 10 9 0 55 12
|
||||
102 26 47 14 102 29 121 34 19 4 62 15 95 24 69 19 117 32 178 47 82 20 263
|
||||
81 317 107 187 88 391 273 468 427 41 81 63 136 78 197 9 35 21 66 27 70 15 9
|
||||
14 341 -1 346 -6 2 -14 20 -18 40 -7 40 -28 113 -44 150 -6 12 -15 34 -21 50
|
||||
-118 283 -358 521 -639 634 -82 32 -117 45 -155 53 -16 4 -33 12 -37 18 -7 12
|
||||
-425 20 -512 10z"/>
|
||||
<path d="M740 4521 c-14 -5 -50 -14 -80 -21 -157 -36 -350 -175 -454 -325 -70
|
||||
-102 -145 -276 -166 -385 -12 -59 -12 -365 -1 -410 18 -69 57 -166 96 -235 40
|
||||
-71 183 -225 209 -225 7 0 19 -6 25 -14 15 -17 110 -64 171 -83 59 -19 136
|
||||
-33 235 -43 44 -5 114 -13 155 -19 41 -6 109 -16 150 -21 121 -17 191 -30 250
|
||||
-46 30 -8 73 -20 95 -26 98 -25 297 -115 335 -150 25 -23 34 -74 27 -154 -6
|
||||
-81 -77 -230 -173 -363 -86 -121 -93 -129 -191 -231 -84 -88 -139 -172 -162
|
||||
-250 -7 -25 -17 -55 -22 -68 -13 -32 -11 -208 2 -251 50 -164 174 -284 347
|
||||
-337 122 -37 290 6 442 112 127 89 252 228 352 396 15 24 57 143 97 273 17 55
|
||||
40 159 51 235 6 41 16 107 23 145 16 94 16 485 -1 600 -46 318 -109 525 -241
|
||||
795 -55 114 -184 322 -243 395 -168 206 -299 341 -413 425 -33 25 -62 48 -65
|
||||
51 -3 3 -30 21 -60 41 -139 88 -288 150 -453 187 -54 12 -299 13 -337 2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
14
public/img/mistralai.svg
Normal file
14
public/img/mistralai.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="64.000000pt" height="64.000000pt" viewBox="0 0 53.000000 60.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,63.000000) scale(0.100000,-0.100000)" stroke="none">
|
||||
<path d="M40 320 l0 -240 70 0 70 0 0 95 c0 95 0 95 25 95 23 0 25 -3 25 -50
|
||||
l0 -50 70 0 70 0 0 50 c0 47 2 50 25 50 25 0 25 0 25 -95 l0 -95 70 0 70 0 0
|
||||
240 0 240 -70 0 -70 0 0 -44 0 -45 -47 -3 -48 -3 -3 -47 c-3 -43 -5 -48 -28
|
||||
-48 -22 0 -24 4 -24 50 l0 50 -45 0 -45 0 0 45 0 45 -70 0 -70 0 0 -240z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 716 B |
56
public/img/ollama.svg
Normal file
56
public/img/ollama.svg
Normal file
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.0"
|
||||
width="467.388pt"
|
||||
height="618.89093pt"
|
||||
viewBox="0 0 467.388 618.89093"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="svg5"
|
||||
sodipodi:docname="ollama.svg"
|
||||
inkscape:version="1.3 (0e150ed, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs5" />
|
||||
<sodipodi:namedview
|
||||
id="namedview5"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="false"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="pt"
|
||||
inkscape:zoom="0.20971564"
|
||||
inkscape:cx="309.9435"
|
||||
inkscape:cy="278.94915"
|
||||
inkscape:window-width="1280"
|
||||
inkscape:window-height="688"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg5" />
|
||||
<g
|
||||
transform="matrix(0.1,0,0,-0.1,-188.01849,632.89095)"
|
||||
stroke="none"
|
||||
id="g5">
|
||||
<path
|
||||
d="m 2849,6312 c -219,-73 -378,-347 -444,-768 -34,-213 -29,-629 9,-774 l 13,-49 -105,-103 c -143,-140 -201,-210 -265,-320 -85,-145 -143,-312 -167,-477 -20,-135 -8,-404 23,-522 34,-129 78,-237 138,-337 l 50,-83 -50,-117 c -96,-227 -130,-376 -138,-618 -12,-345 48,-589 208,-854 l 21,-35 -35,-57 c -43,-72 -100,-243 -122,-368 -26,-149 -31,-393 -11,-523 10,-59 22,-121 28,-138 l 10,-29 177,2 176,3 -1,40 c 0,22 -11,76 -23,120 -42,149 -26,433 34,610 13,39 51,120 84,179 33,60 63,122 67,138 10,46 -4,109 -34,154 -15,22 -46,69 -69,103 -171,254 -206,664 -88,1017 27,80 77,185 130,274 63,105 56,178 -25,260 -138,138 -221,394 -207,634 21,357 227,680 532,833 130,66 183,77 375,78 96,0 183,4 193,9 10,5 36,45 58,90 121,242 304,391 594,484 72,23 96,26 235,26 148,0 160,-1 250,-32 281,-94 469,-249 577,-478 50,-105 54,-107 215,-99 153,8 244,-6 365,-57 143,-59 293,-181 389,-314 62,-87 130,-236 161,-351 22,-84 26,-119 26,-243 0,-124 -4,-159 -26,-242 -31,-118 -101,-257 -167,-332 -83,-95 -88,-166 -19,-277 128,-206 190,-431 191,-689 1,-277 -53,-446 -217,-684 -36,-52 -51,-114 -41,-164 4,-16 34,-78 67,-138 33,-59 71,-140 84,-178 60,-182 76,-461 34,-611 -12,-44 -23,-98 -23,-120 l -1,-40 176,-3 177,-2 11,31 c 46,134 52,474 11,683 -25,129 -78,281 -121,351 l -31,50 21,35 c 159,261 219,507 208,848 -8,252 -53,444 -155,663 l -40,86 31,49 c 59,94 119,235 150,352 29,112 31,126 31,317 1,224 -9,294 -70,472 -19,55 -34,106 -34,113 0,21 -109,198 -159,257 -26,32 -98,107 -159,167 -61,60 -109,113 -106,118 16,25 35,205 41,368 8,260 -15,478 -72,675 -88,303 -214,474 -393,534 -207,70 -405,-47 -542,-318 -75,-151 -139,-379 -156,-558 l -7,-72 -99,50 c -189,95 -399,149 -578,149 -173,0 -383,-52 -560,-138 -52,-26 -98,-48 -101,-50 -3,-1 -9,28 -13,65 -29,288 -146,595 -282,742 -121,130 -274,179 -415,133 z m 153,-374 c 119,-127 208,-471 208,-804 0,-85 -4,-112 -20,-144 -17,-34 -25,-40 -53,-40 -51,0 -267,-30 -326,-45 -30,-8 -56,-13 -58,-12 -1,2 -7,67 -14,145 -16,215 7,467 62,657 39,133 121,275 159,275 7,0 25,-14 42,-32 z m 2529,1 c 124,-133 208,-558 179,-909 -6,-74 -13,-136 -15,-138 -2,-2 -25,3 -52,11 -39,12 -122,24 -352,50 -7,1 -22,18 -33,37 -18,32 -19,50 -15,200 8,255 53,468 132,635 34,71 93,145 115,145 7,0 25,-14 41,-31 z"
|
||||
id="path1" />
|
||||
<path
|
||||
d="m 4115,3729 c -390,-29 -735,-284 -824,-609 -26,-93 -28,-244 -5,-334 38,-149 171,-324 306,-404 85,-50 204,-99 288,-117 99,-22 453,-32 584,-16 350,41 626,253 700,538 20,78 21,240 1,318 -36,140 -144,303 -266,401 -218,174 -474,247 -784,223 z m 329,-258 c 291,-76 497,-291 500,-521 3,-227 -192,-414 -479,-460 -80,-13 -403,-13 -485,1 -212,34 -390,160 -452,319 -29,77 -29,194 1,272 79,206 278,353 544,404 97,18 269,11 371,-15 z"
|
||||
id="path2" />
|
||||
<path
|
||||
d="m 4038,3151 c -58,-52 -40,-123 47,-177 43,-27 45,-31 40,-64 -19,-120 -19,-127 8,-154 22,-22 35,-26 85,-26 91,0 123,41 103,130 -17,74 -15,83 33,113 56,35 76,66 76,116 0,32 -6,44 -31,65 -39,33 -81,33 -136,1 l -43,-24 -42,24 c -58,33 -100,32 -140,-4 z"
|
||||
id="path3" />
|
||||
<path
|
||||
d="m 2932,3664 c -107,-53 -169,-209 -128,-319 44,-115 194,-177 303,-124 89,43 153,148 153,250 0,171 -171,271 -328,193 z"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 5320,3675 c -119,-54 -165,-193 -104,-320 27,-58 88,-118 141,-141 68,-29 162,-10 227,47 86,76 97,174 35,297 -45,89 -101,125 -198,129 -44,2 -78,-2 -101,-12 z"
|
||||
id="path5" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
23
public/img/tabby.svg
Normal file
23
public/img/tabby.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="176.000000pt" height="176.000000pt" viewBox="0 0 176.000000 176.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,176.000000) scale(0.100000,-0.100000)" stroke="none">
|
||||
<path d="M197 1670 c-16 -19 -32 -58 -43 -107 -19 -87 -16 -222 11 -422 21
|
||||
-162 19 -218 -10 -306 -49 -144 -43 -332 14 -443 54 -106 160 -180 297 -207
|
||||
164 -33 202 -44 270 -77 59 -28 80 -33 144 -33 66 0 84 4 154 38 53 25 110 43
|
||||
170 53 122 21 177 38 241 74 158 90 225 282 180 515 -8 42 -21 90 -30 107 -20
|
||||
41 -19 144 1 284 9 60 17 177 17 259 1 134 -1 156 -21 206 -31 77 -50 93 -104
|
||||
85 -84 -13 -183 -89 -319 -243 l-54 -62 -75 19 c-100 26 -224 26 -321 0 l-74
|
||||
-20 -54 63 c-95 109 -182 186 -244 217 -79 39 -117 39 -150 0z m1121 -897 c2
|
||||
-18 -5 -52 -16 -76 -25 -55 -61 -73 -171 -83 l-84 -7 5 51 c7 74 45 114 138
|
||||
146 8 3 40 4 70 3 54 -2 55 -2 58 -34z m-693 16 c24 -7 55 -27 78 -51 33 -34
|
||||
37 -45 37 -88 0 -57 5 -56 -119 -40 -96 13 -136 48 -141 125 -5 64 -4 65 53
|
||||
65 28 0 70 -5 92 -11z m391 -384 c21 -28 18 -33 -31 -63 -32 -19 -48 -36 -53
|
||||
-57 -6 -23 -14 -30 -32 -30 -18 0 -26 7 -32 32 -6 24 -19 38 -48 53 -31 16
|
||||
-40 26 -40 46 0 34 27 42 134 40 73 -2 91 -6 102 -21z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
55
public/img/togetherai.svg
Normal file
55
public/img/togetherai.svg
Normal file
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="togetherai.svg"
|
||||
inkscape:version="1.3 (0e150ed, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="7.375"
|
||||
inkscape:cx="15.932203"
|
||||
inkscape:cy="15.932203"
|
||||
inkscape:window-width="1280"
|
||||
inkscape:window-height="688"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g4" />
|
||||
<g
|
||||
clip-path="url(#clip0_542_18748)"
|
||||
id="g4">
|
||||
<path
|
||||
id="rect1"
|
||||
d="M 5.6464844 0 C 2.5180256 -1.1842331e-15 3.5527101e-15 2.5180256 0 5.6464844 L 0 26.353516 C -1.1842331e-15 29.481971 2.5180256 32 5.6464844 32 L 26.353516 32 C 29.481971 32 32 29.481971 32 26.353516 L 32 5.6464844 C 32 2.5180256 29.481971 3.5527101e-15 26.353516 0 L 5.6464844 0 z M 9.6464844 4 A 5.6470599 5.6470599 0 0 1 15.294922 9.6464844 A 5.6470599 5.6470599 0 0 1 9.6464844 15.294922 A 5.6470599 5.6470599 0 0 1 4 9.6464844 A 5.6470599 5.6470599 0 0 1 9.6464844 4 z M 22.824219 4 A 5.6470599 5.6470599 0 0 1 28.470703 9.6464844 A 5.6470599 5.6470599 0 0 1 22.824219 15.294922 A 5.6470599 5.6470599 0 0 1 17.175781 9.6464844 A 5.6470599 5.6470599 0 0 1 22.824219 4 z M 9.6464844 17.175781 A 5.6470599 5.6470599 0 0 1 15.294922 22.824219 A 5.6470599 5.6470599 0 0 1 9.6464844 28.470703 A 5.6470599 5.6470599 0 0 1 4 22.824219 A 5.6470599 5.6470599 0 0 1 9.6464844 17.175781 z M 22.824219 17.175781 A 5.6470599 5.6470599 0 0 1 28.470703 22.824219 A 5.6470599 5.6470599 0 0 1 22.824219 28.470703 A 5.6470599 5.6470599 0 0 1 17.175781 22.824219 A 5.6470599 5.6470599 0 0 1 22.824219 17.175781 z " />
|
||||
<circle
|
||||
cx="9.64706"
|
||||
cy="9.64706"
|
||||
r="5.64706"
|
||||
opacity="0.45"
|
||||
id="circle9" />
|
||||
</g>
|
||||
<defs
|
||||
id="defs4">
|
||||
<clipPath
|
||||
id="clip0_542_18748">
|
||||
<rect
|
||||
width="32"
|
||||
height="32"
|
||||
id="rect4" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
@ -61,7 +61,6 @@
|
||||
<link rel="stylesheet" type="text/css" href="css/extensions-panel.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/select2-overrides.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/mobile-styles.css">
|
||||
<link rel="stylesheet" href="css/bg_load.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/user.css">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<script type="module" src="scripts/i18n.js"></script>
|
||||
@ -70,7 +69,7 @@
|
||||
<script type="module" src="scripts/group-chats.js"></script>
|
||||
<script type="module" src="scripts/kai-settings.js"></script>
|
||||
<script type="module" src="scripts/textgen-settings.js"></script>
|
||||
<script type="module" src="scripts/mancer-settings.js"></script>
|
||||
<script type="module" src="scripts/textgen-models.js"></script>
|
||||
<script type="module" src="scripts/bookmarks.js"></script>
|
||||
<script type="module" src="scripts/horde.js"></script>
|
||||
<script type="module" src="scripts/RossAscends-mods.js"></script>
|
||||
@ -91,6 +90,7 @@
|
||||
</head>
|
||||
|
||||
<body class="no-blur">
|
||||
<div id="preloader"></div>
|
||||
<div id="bg_custom"></div>
|
||||
<div id="bg1"></div>
|
||||
<div id="character_context_menu" class="hidden">
|
||||
@ -177,7 +177,7 @@
|
||||
<div>
|
||||
<h4 class="margin0"><span data-i18n="openaipresets">Chat Completion Presets</span></h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_openai" class="flex1 text_pole">
|
||||
<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">
|
||||
@ -437,14 +437,15 @@
|
||||
Streaming</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft">
|
||||
<span data-i18n="Display the response bit by bit as it is generated.">Display
|
||||
the response bit by bit as it is generated.</span><br>
|
||||
<span data-i18n="When this is off, responses will be displayed all at once when they are complete.">When
|
||||
this is off, responses will be displayed all at once when they are
|
||||
complete.</span>
|
||||
<span data-i18n="Display the response bit by bit as it is generated.">
|
||||
Display the response bit by bit as it is generated.
|
||||
</span><br>
|
||||
<span data-i18n="When this is off, responses will be displayed all at once when they are complete.">
|
||||
When this is off, responses will be displayed all at once when they are complete.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,palm">
|
||||
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom">
|
||||
<div class="range-block-title" data-i18n="Temperature">
|
||||
Temperature
|
||||
</div>
|
||||
@ -457,7 +458,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21">
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom">
|
||||
<div class="range-block-title" data-i18n="Frequency Penalty">
|
||||
Frequency Penalty
|
||||
</div>
|
||||
@ -470,7 +471,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21">
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom">
|
||||
<div class="range-block-title" data-i18n="Presence Penalty">
|
||||
Presence Penalty
|
||||
</div>
|
||||
@ -496,7 +497,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="claude,openrouter,ai21,palm">
|
||||
<div data-newbie-hidden class="range-block" data-source="claude,openrouter,ai21,makersuite">
|
||||
<div class="range-block-title" data-i18n="Top K">
|
||||
Top K
|
||||
</div>
|
||||
@ -509,7 +510,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,palm">
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom">
|
||||
<div class="range-block-title" data-i18n="Top-p">
|
||||
Top P
|
||||
</div>
|
||||
@ -546,10 +547,6 @@
|
||||
<textarea id="jailbreak_prompt_quick_edit_textarea" class="text_pole textarea_compact autoSetHeight" rows="6" placeholder="—" data-pm-prompt="jailbreak"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="claude_assistant_prefill_block" data-source="claude" class="range-block">
|
||||
<span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
|
||||
<textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="inline-drawer wide100p">
|
||||
@ -581,7 +578,7 @@
|
||||
</div>
|
||||
<div class="toggle-description justifyLeft" data-i18n="Wraps activated World Info entries before inserting into the prompt.">
|
||||
Wraps activated World Info entries before inserting into the prompt. Use
|
||||
<tt>{0}</tt> to mark a place where the content is inserted.
|
||||
<code>{0}</code> to mark a place where the content is inserted.
|
||||
</div>
|
||||
<div class="wide100p">
|
||||
<textarea id="wi_format_textarea" class="text_pole textarea_compact autoSetHeight" rows="3" placeholder="—"></textarea>
|
||||
@ -595,7 +592,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="toggle-description justifyLeft" data-i18n="Use scenario to mark a place where the content is inserted.">
|
||||
Use <tt>{{scenario}}</tt> to mark a place where the content is inserted.
|
||||
Use <code>{{scenario}}</code> to mark a place where the content is inserted.
|
||||
</div>
|
||||
<div class="wide100p">
|
||||
<textarea id="scenario_format_textarea" class="text_pole textarea_compact autoSetHeight" rows="3" placeholder="—"></textarea>
|
||||
@ -609,7 +606,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="toggle-description justifyLeft" data-i18n="Use personality to mark a place where the content is inserted.">
|
||||
Use <tt>{{personality}}</tt> to mark a place where the content is inserted.
|
||||
Use <code>{{personality}}</code> to mark a place where the content is inserted.
|
||||
</div>
|
||||
<div class="wide100p">
|
||||
<textarea id="personality_format_textarea" class="text_pole textarea_compact autoSetHeight" rows="3" placeholder="—"></textarea>
|
||||
@ -733,6 +730,9 @@
|
||||
</div>
|
||||
<div class="wide100p">
|
||||
<input id="openai_reverse_proxy" type="text" class="text_pole" placeholder="https://api.openai.com/v1" maxlength="500" />
|
||||
<small class="reverse_proxy_warning">
|
||||
Doesn't work? Try adding <code>/v1</code> at the end!
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,claude">
|
||||
@ -749,7 +749,7 @@
|
||||
<div id="openai_proxy_password_show" title="Peek a password" class="menu_button fa-solid fa-eye-slash fa-fw"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter">
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,mistralai,custom">
|
||||
<div class="range-block-title justifyLeft" data-i18n="Seed">
|
||||
Seed
|
||||
</div>
|
||||
@ -760,19 +760,6 @@
|
||||
<input type="number" id="seed_openai" name="seed_openai" class="text_pole" min="-1" max="2147483647" value="-1">
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openai,claude">
|
||||
<div class="range-block-title justifyLeft">
|
||||
<label for="legacy_streaming" class="checkbox_label">
|
||||
<input id="legacy_streaming" type="checkbox" />
|
||||
<span data-i18n="Legacy Streaming Processing">
|
||||
Legacy Streaming Processing
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="toggle-description justifyLeft" data-i18n="Enable this if the streaming doesn't work with your proxy">
|
||||
Enable this if the streaming doesn't work with your proxy.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="advanced-ai-config-block" class="width100p">
|
||||
@ -991,7 +978,7 @@
|
||||
Helps to ban or reinforce the usage of certain tokens.
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn wide100p">
|
||||
<div class="novelai_logit_bias_list"></div>
|
||||
<div class="logit_bias_list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
@ -1396,6 +1383,21 @@
|
||||
<textarea id="banned_tokens_textgenerationwebui" class="text_pole textarea_compact" name="banned_tokens_textgenerationwebui" rows="3" placeholder="Example: some text [42, 69, 1337]"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block wide100p">
|
||||
<div class="range-block-title title_restorable">
|
||||
<span data-i18n="Logit Bias">Logit Bias</span>
|
||||
<div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon">
|
||||
<i class="fa-xs fa-solid fa-plus"></i>
|
||||
<small data-i18n="Add">Add</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toggle-description justifyLeft" data-i18n="Helps to ban or reenforce the usage of certain words">
|
||||
Helps to ban or reinforce the usage of certain tokens.
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn wide100p">
|
||||
<div class="logit_bias_list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-forAphro=False class="wide100p">
|
||||
<hr class="width100p">
|
||||
<h4 data-i18n="CFG" class="textAlignCenter">CFG
|
||||
@ -1493,7 +1495,18 @@
|
||||
<input id="names_in_completion" type="checkbox" /><span data-i18n="Add character names">Add character names</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft">
|
||||
<span data-i18n="Send names in the ChatML objects.">Send names in the ChatML objects. Helps the model to associate messages with characters.</span>
|
||||
<span data-i18n="Send names in the message objects.">Send names in the message objects. Helps the model to associate messages with characters.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<label for="continue_prefill" class="checkbox_label widthFreeExpand">
|
||||
<input id="continue_prefill" type="checkbox" />
|
||||
<span data-i18n="Continue prefill">Continue prefill</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft">
|
||||
<span data-i18n="Continue sends the last message.">
|
||||
Continue sends the last message as assistant role instead of system message with instruction.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
@ -1510,7 +1523,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,openrouter">
|
||||
<div class="range-block" data-source="openai,openrouter,makersuite,custom">
|
||||
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_image_inlining" type="checkbox" />
|
||||
<span data-i18n="Send inline images">Send inline images</span>
|
||||
@ -1529,12 +1542,48 @@
|
||||
<span data-i18n="Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.">Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="claude">
|
||||
<label for="exclude_assistant" title="Exclude Assistant suffix" class="checkbox_label widthFreeExpand">
|
||||
<input id="exclude_assistant" type="checkbox" /><span data-i18n="Exclude Assistant suffix">Exclude Assistant suffix</span>
|
||||
<div class="range-block" data-source="makersuite">
|
||||
<label for="use_google_tokenizer" title="Use Google Tokenizer" class="checkbox_label widthFreeExpand">
|
||||
<input id="use_google_tokenizer" type="checkbox" /><span data-i18n="Use Google Tokenizer">Use Google Tokenizer</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft">
|
||||
<span data-i18n="Exclude the assistant suffix from being added to the end of prompt.">Exclude the assistant suffix from being added to the end of prompt (Requires jailbreak with 'Assistant:' in it).</span>
|
||||
<span data-i18n="Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.">Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="claude">
|
||||
<label for="exclude_assistant" title="Exclude Assistant suffix" class="checkbox_label widthFreeExpand">
|
||||
<input id="exclude_assistant" type="checkbox" />
|
||||
<span data-i18n="Exclude Assistant suffix">Exclude Assistant suffix</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft">
|
||||
<span data-i18n="Exclude the assistant suffix from being added to the end of prompt.">
|
||||
Exclude the assistant suffix from being added to the end of prompt (Requires jailbreak with 'Assistant:' in it).
|
||||
</span>
|
||||
</div>
|
||||
<div id="claude_assistant_prefill_block" class="wide100p">
|
||||
<span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
|
||||
<textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
|
||||
</div>
|
||||
<label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
|
||||
<input id="claude_use_sysprompt" type="checkbox" />
|
||||
<span data-i18n="Use system prompt (Claude 2.1+ only)">
|
||||
Use system prompt (Claude 2.1+ only)
|
||||
</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft">
|
||||
<span data-i18n="Exclude the 'Human: ' prefix from being added to the beginning of the prompt.">
|
||||
Exclude the 'Human: ' prefix from being added to the beginning of the prompt.
|
||||
Instead, place it between the system prompt and the first message with the role 'assistant' (right before 'Chat History' by default).
|
||||
</span>
|
||||
</div>
|
||||
<div id="claude_human_sysprompt_message_block" class="wide100p">
|
||||
<div class="range-block-title openai_restorable">
|
||||
<span data-i18n="Human: first message">Human: first message</span>
|
||||
<div id="claude_human_sysprompt_message_restore" title="Restore Human: first message" class="right_menu_button">
|
||||
<div class="fa-solid fa-clock-rotate-left"></div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="claude_human_sysprompt_textarea" class="text_pole textarea_compact" rows="4" maxlength="10000" data-i18n="[placeholder]Human message" placeholder="Human message, instruction, etc. Adds nothing when empty, i.e. requires a new prompt with the role 'user' or manually adding the 'Human: ' prefix."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1597,8 +1646,8 @@
|
||||
<option value="kobold"><span data-i18n="KoboldAI">KoboldAI Classic</span></option>
|
||||
<option value="koboldhorde"><span data-i18n="KoboldAI Horde">KoboldAI Horde</span></option>
|
||||
<option value="novel"><span data-i18n="NovelAI">NovelAI</span></option>
|
||||
<option value="textgenerationwebui"><span data-i18n="Text Completion">Text Completion (ooba, Mancer, Aphrodite, TabbyAPI, KoboldCpp)</span></option>
|
||||
<option value="openai"><span data-i18n="Chat Completion (OpenAI, Claude, Window/OpenRouter, Scale, AI21)">Chat Completion (OpenAI, Claude, Window, OpenRouter, Scale, AI21, PaLM)</span></option>
|
||||
<option value="textgenerationwebui"><span data-i18n="Text Completion">Text Completion</span></option>
|
||||
<option value="openai"><span data-i18n="Chat Completion">Chat Completion</span></option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="kobold_horde" style="position: relative;"> <!-- shows the kobold settings -->
|
||||
@ -1728,8 +1777,29 @@
|
||||
<option value="aphrodite">Aphrodite</option>
|
||||
<option value="tabby">TabbyAPI</option>
|
||||
<option value="koboldcpp">KoboldCpp</option>
|
||||
<option value="llamacpp">llama.cpp</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="togetherai">TogetherAI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div data-tg-type="togetherai" class="flex-container flexFlowColumn">
|
||||
<h4 data-i18n="TogetherAI API Key">TogetherAI API Key</h4>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_togetherai" name="api_key_togetherai" 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_togetherai"></div>
|
||||
</div>
|
||||
<div data-for="api_key_togetherai" class="neutral_warning">
|
||||
For privacy reasons, your API key will be hidden after you reload the page.
|
||||
</div>
|
||||
<div>
|
||||
<h4 data-i18n="TogetherAI Model">TogetherAI Model</h4>
|
||||
<select id="model_togetherai_select">
|
||||
<option>
|
||||
-- Connect to the API --
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div data-tg-type="mancer" class="flex-container flexFlowColumn">
|
||||
<div class="flex-container flexFlowColumn">
|
||||
</div>
|
||||
@ -1749,7 +1819,11 @@
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4>Mancer Model</h4>
|
||||
<select id="mancer_model"></select>
|
||||
<select id="mancer_model">
|
||||
<option>
|
||||
-- Connect to the API --
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div data-tg-type="ooba" class="flex-container flexFlowColumn">
|
||||
@ -1758,7 +1832,7 @@
|
||||
oobabooga/text-generation-webui
|
||||
</a>
|
||||
<span data-i18n="Make sure you run it with">
|
||||
Make sure you run it with <tt>--api</tt> flag
|
||||
Make sure you run it with <code>--api</code> flag
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
@ -1788,6 +1862,44 @@
|
||||
<input id="aphrodite_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="aphrodite">
|
||||
</div>
|
||||
</div>
|
||||
<div data-tg-type="llamacpp">
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<a href="https://github.com/ggerganov/llama.cpp" target="_blank">
|
||||
ggerganov/llama.cpp (inference server)
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4 data-i18n="API url">API URL</h4>
|
||||
<small data-i18n="Example: http://127.0.0.1:8080">Example: http://127.0.0.1:8080</small>
|
||||
<input id="llamacpp_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="llamacpp">
|
||||
</div>
|
||||
</div>
|
||||
<div data-tg-type="ollama">
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<a href="https://github.com/jmorganca/ollama" target="_blank">
|
||||
jmorganca/ollama
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4 data-i18n="API url">API URL</h4>
|
||||
<small data-i18n="Example: http://127.0.0.1:11434">Example: http://127.0.0.1:11434</small>
|
||||
<input id="ollama_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="ollama">
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4>
|
||||
<span data-i18n="Ollama Model">Ollama Model</h4>
|
||||
</h4>
|
||||
<select id="ollama_model">
|
||||
<option>
|
||||
-- Connect to the API --
|
||||
</option>
|
||||
</select>
|
||||
<div id="ollama_download_model" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
<span data-i18n="Download">Download</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-tg-type="tabby">
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<a href="https://github.com/theroyallab/tabbyAPI" target="_blank">
|
||||
@ -1825,7 +1937,7 @@
|
||||
<div id="api_button_textgenerationwebui" class="api_button menu_button" type="submit" data-i18n="Connect" data-server-connect="ooba_blocking,aphrodite,tabby,koboldcpp">Connect</div>
|
||||
<div class="api_loading menu_button" data-i18n="Cancel">Cancel</div>
|
||||
</div>
|
||||
<label class="checkbox_label margin-bot-10px" for="legacy_api_textgenerationwebui">
|
||||
<label data-tg-type="ooba,aphrodite" class="checkbox_label margin-bot-10px" for="legacy_api_textgenerationwebui">
|
||||
<input type="checkbox" id="legacy_api_textgenerationwebui" />
|
||||
<span data-i18n="Legacy API (pre-OAI, no streaming)">Legacy API (pre-OAI, no streaming)</span>
|
||||
</label>
|
||||
@ -1846,7 +1958,9 @@
|
||||
<option value="claude">Claude</option>
|
||||
<option value="scale">Scale</option>
|
||||
<option value="ai21">AI21</option>
|
||||
<option value="palm">Google PaLM 2</option>
|
||||
<option value="makersuite">Google MakerSuite</option>
|
||||
<option value="mistralai">MistralAI</option>
|
||||
<option value="custom">Custom (OpenAI-compatible)</option>
|
||||
</select>
|
||||
<form id="openai_form" data-source="openai" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
<h4><span data-i18n="OpenAI API key">OpenAI API key</span></h4>
|
||||
@ -2112,21 +2226,79 @@
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<form id="palm_form" data-source="palm" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
<h4 data-i18n="PaLM API Key">PaLM API Key</h4>
|
||||
<form id="makersuite_form" data-source="makersuite" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
<h4 data-i18n="MakerSuite API Key">MakerSuite API Key</h4>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_palm" name="api_key_palm" 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_palm"></div>
|
||||
<input id="api_key_makersuite" name="api_key_makersuite" 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_makersuite"></div>
|
||||
</div>
|
||||
<div data-for="api_key_palm" class="neutral_warning">
|
||||
<div data-for="api_key_makersuite" class="neutral_warning">
|
||||
For privacy reasons, your API key will be hidden after you reload the page.
|
||||
</div>
|
||||
<!-- Its only purpose is to trigger max context size check -->
|
||||
<select id="model_palm_select" class="displayNone"></select>
|
||||
<div>
|
||||
<h4 data-i18n="Google Model">Google Model</h4>
|
||||
<select id="model_google_select">
|
||||
<option value="gemini-pro">Gemini Pro</option>
|
||||
<option value="gemini-pro-vision">Gemini Pro Vision</option>
|
||||
<option value="text-bison-001">Bison Text</option>
|
||||
<option value="chat-bison-001">Bison Chat</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<form id="mistralai_form" data-source="mistralai" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
<h4 data-i18n="MistralAI API Key">MistralAI API Key</h4>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_mistralai" name="api_key_mistralai" 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_mistralai"></div>
|
||||
</div>
|
||||
<div data-for="api_key_mistralai" class="neutral_warning">
|
||||
For privacy reasons, your API key will be hidden after you reload the page.
|
||||
</div>
|
||||
<div>
|
||||
<h4 data-i18n="MistralAI Model">MistralAI Model</h4>
|
||||
<select id="model_mistralai_select">
|
||||
<optgroup label="Latest">
|
||||
<option value="mistral-tiny">mistral-tiny</option>
|
||||
<option value="mistral-small">mistral-small</option>
|
||||
<option value="mistral-medium">mistral-medium</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<form id="custom_form" data-source="custom">
|
||||
<h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
|
||||
<div class="flex-container">
|
||||
<input id="custom_api_url_text" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" placeholder="Example: http://localhost:1234/v1">
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
Doesn't work? Try adding <code>/v1</code> at the end of the URL!
|
||||
</small>
|
||||
</div>
|
||||
<h4>
|
||||
<span data-i18n="Custom API Key">Custom API Key</span>
|
||||
<small>(Optional)</small>
|
||||
</h4>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_custom" name="api_key_custom" 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_custom"></div>
|
||||
</div>
|
||||
<div data-for="api_key_custom" class="neutral_warning">
|
||||
For privacy reasons, your API key will be hidden after you reload the page.
|
||||
</div>
|
||||
<h4>Enter a Model ID</h4>
|
||||
<div class="flex-container">
|
||||
<input id="custom_model_id" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" placeholder="Example: gpt-3.5-turbo">
|
||||
</div>
|
||||
<h4 data-i18n="Available Models">Available Models</h4>
|
||||
<div class="flex-container">
|
||||
<select id="model_custom_select" class="text_pole"></select>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex-container flex">
|
||||
<div id="api_button_openai" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect">Connect</div>
|
||||
<div class="api_loading menu_button" data-i18n="Cancel">Cancel</div>
|
||||
<div data-source="custom" id="customize_additional_parameters" class="menu_button menu_button_icon">Additional Parameters</div>
|
||||
<div data-source="openrouter" id="openrouter_authorize" class="menu_button menu_button_icon" title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai" data-i18n="[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">Authorize</div>
|
||||
<div id="test_api_button" class="menu_button menu_button_icon" title="Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!" data-i18n="[title]Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!">Test Message</div>
|
||||
</div>
|
||||
@ -2243,11 +2415,15 @@
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</h4>
|
||||
<div>
|
||||
<label for="instruct_enabled" class="checkbox_label">
|
||||
<div class="flex-container">
|
||||
<label for="instruct_enabled" class="checkbox_label flex1">
|
||||
<input id="instruct_enabled" type="checkbox" />
|
||||
<span data-i18n="Enabled">Enabled</span>
|
||||
</label>
|
||||
<label for="instruct_bind_to_context" class="checkbox_label flex1" title="If enabled, Context templates will be automatically selected based on selected Instruct template name or by preference.">
|
||||
<input id="instruct_bind_to_context" type="checkbox" />
|
||||
<span data-i18n="Bind to Context">Bind to Context</span>
|
||||
</label>
|
||||
</div>
|
||||
<label for="instruct_presets">
|
||||
<span data-i18n="Presets">Presets</span>
|
||||
@ -2774,9 +2950,12 @@
|
||||
<div class="flex-container flexnowrap alignitemscenter">
|
||||
<select id="themes" class="margin0">
|
||||
</select>
|
||||
<div id="ui-preset-save-button" title="Save changes to a new theme file" data-i18n="[title]Save changes to a new theme file" class="menu_button margin0">
|
||||
<div id="ui-preset-update-button" title="Update a theme file" data-i18n="[title]Update a theme file" class="menu_button margin0">
|
||||
<i class="fa-solid fa-save"></i>
|
||||
</div>
|
||||
<div id="ui-preset-save-button" title="Save as a new theme" data-i18n="[title]Save as a new theme" class="menu_button margin0">
|
||||
<i class="fa-solid fa-paste"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div name="themeElements" data-newbie-hidden class="flex-container flexFlowColumn flexNoGap">
|
||||
@ -2903,12 +3082,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="streaming-fps" class="range-block">
|
||||
<div class="range-block-title" data-i18n="Streaming FPS">
|
||||
Streaming FPS
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="streaming_fps" name="streaming_fps" min="5" max="100" step="5">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<input type="number" min="5" max="100" step="1" data-for="streaming_fps" id="streaming_fps_counter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div name="UserSettingsSecondColumn" id="UI-Customization" class="flex-container flexFlowColumn wide100p flexNoGap">
|
||||
<div name="themeToggles">
|
||||
<h4 data-i18n="Theme Toggles">Theme Toggles</h4>
|
||||
<label for="reduced_motion" class="checkbox_label" title="Disables animations and transitions" data-i18n="[title]Disables animations and transitions">
|
||||
<input id="reduced_motion" type="checkbox" />
|
||||
<span data-i18n="Reduced Motion">Reduced Motion</span>
|
||||
</label>
|
||||
<label data-newbie-hidden for="fast_ui_mode" class="checkbox_label" title="removes blur from window backgrounds" data-i18n="[title]removes blur from window backgrounds">
|
||||
<input id="fast_ui_mode" type="checkbox" />
|
||||
<span data-i18n="No Blur Effect">No Blur Effect</span>
|
||||
@ -2953,6 +3149,10 @@
|
||||
<input id="messageTokensEnabled" type="checkbox" />
|
||||
<span data-i18n="Show Message Token Count">Message Token Count</span>
|
||||
</label>
|
||||
<label for="compact_input_area" class="checkbox_label">
|
||||
<input id="compact_input_area" type="checkbox" />
|
||||
<span data-i18n="Compact Input Area (Mobile)">Compact Input Area <i class="fa-solid fa-mobile-screen-button"></i></span>
|
||||
</label>
|
||||
<label data-newbie-hidden for="hotswapEnabled" class="checkbox_label">
|
||||
<input id="hotswapEnabled" type="checkbox" />
|
||||
<span data-i18n="Characters Hotswap">Characters Hotswap</span>
|
||||
@ -3604,6 +3804,7 @@
|
||||
<label id="rm_group_automode_label" class="checkbox_label whitespacenowrap">
|
||||
<input id="rm_group_automode" type="checkbox" />
|
||||
<span data-i18n="Auto Mode">Auto Mode</span>
|
||||
<input id="rm_group_automode_delay" class="text_pole textarea_compact widthUnset" type="number" min="1" max="999" step="1" value="5" title="Auto Mode delay" />
|
||||
</label>
|
||||
<label id="rm_group_hidemutedsprites_label" class="checkbox_label whitespacenowrap">
|
||||
<input id="rm_group_hidemutedsprites" type="checkbox" />
|
||||
@ -3648,7 +3849,7 @@
|
||||
</div>
|
||||
<div id="rm_character_import" class="right_menu" style="display: none;">
|
||||
<form id="form_import" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
<input multiple type="file" id="character_import_file" accept=".json, image/png" name="avatar">
|
||||
<input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml" name="avatar">
|
||||
<input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
@ -3776,7 +3977,7 @@
|
||||
<span data-i18n="Personality summary">Personality summary</span>
|
||||
<a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#personality-summary" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a>
|
||||
</h4>
|
||||
<textarea id="personality_textarea" name="personality" data-i18n="[placeholder](A brief description of the personality)" placeholder="(A brief description of the personality)" form="form_create" class="text_pole" autocomplete="off" rows="1" maxlength="50000"></textarea>
|
||||
<textarea id="personality_textarea" name="personality" data-i18n="[placeholder](A brief description of the personality)" placeholder="(A brief description of the personality)" form="form_create" class="text_pole" autocomplete="off" rows="4" maxlength="50000"></textarea>
|
||||
<div class="extension_token_counter">
|
||||
Tokens: <span data-token-counter="personality_textarea" data-token-permanent="true">counting...</span>
|
||||
</div>
|
||||
@ -3788,7 +3989,7 @@
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</h4>
|
||||
<textarea id="scenario_pole" name="scenario" data-i18n="[placeholder](Circumstances and context of the interaction)" placeholder="(Circumstances and context of the interaction)" class="text_pole" maxlength="50000" value="" autocomplete="off" form="form_create" rows="1"></textarea>
|
||||
<textarea id="scenario_pole" name="scenario" data-i18n="[placeholder](Circumstances and context of the interaction)" placeholder="(Circumstances and context of the interaction)" class="text_pole" maxlength="50000" value="" autocomplete="off" form="form_create" rows="4"></textarea>
|
||||
<div class="extension_token_counter">
|
||||
Tokens: <span data-token-counter="scenario_pole" data-token-permanent="true">counting...</span>
|
||||
</div>
|
||||
@ -4205,11 +4406,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="novelai_logit_bias_template" class="template_element">
|
||||
<div class="novelai_logit_bias_form">
|
||||
<input class="novelai_logit_bias_text text_pole" data-i18n="[placeholder]Type here..." placeholder="type here..." />
|
||||
<input class="novelai_logit_bias_value text_pole" type="number" min="-2" value="0" max="2" step="0.01" />
|
||||
<i class="menu_button fa-solid fa-xmark novelai_logit_bias_remove"></i>
|
||||
<div id="logit_bias_template" class="template_element">
|
||||
<div class="logit_bias_form">
|
||||
<input class="logit_bias_text text_pole" data-i18n="[placeholder]Type here..." placeholder="type here..." />
|
||||
<input class="logit_bias_value text_pole" type="number" min="-2" value="0" max="2" step="0.01" />
|
||||
<i class="menu_button fa-solid fa-xmark logit_bias_remove"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="completion_prompt_manager_popup" class="drawer-content" style="display:none;">
|
||||
|
@ -8,7 +8,7 @@
|
||||
"system_sequence_prefix": "[INST] <<SYS>>\n",
|
||||
"system_sequence_suffix": "\n<</SYS>>\n",
|
||||
"stop_sequence": "",
|
||||
"separator_sequence": "\n",
|
||||
"separator_sequence": " ",
|
||||
"wrap": false,
|
||||
"macro": true,
|
||||
"names": false,
|
||||
|
14
public/lib/bowser.min.js
vendored
Normal file
14
public/lib/bowser.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/lib/css-parser.map
Normal file
1
public/lib/css-parser.map
Normal file
File diff suppressed because one or more lines are too long
765
public/lib/css-parser.mjs
Normal file
765
public/lib/css-parser.mjs
Normal file
@ -0,0 +1,765 @@
|
||||
|
||||
function $parcel$defineInteropFlag(a) {
|
||||
Object.defineProperty(a, '__esModule', {value: true, configurable: true});
|
||||
}
|
||||
|
||||
function $parcel$export(e, n, v, s) {
|
||||
Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
|
||||
}
|
||||
var $009ddb00d3ec72b8$exports = {};
|
||||
|
||||
$parcel$defineInteropFlag($009ddb00d3ec72b8$exports);
|
||||
|
||||
$parcel$export($009ddb00d3ec72b8$exports, "default", () => $009ddb00d3ec72b8$export$2e2bcd8739ae039);
|
||||
class $009ddb00d3ec72b8$export$2e2bcd8739ae039 extends Error {
|
||||
constructor(filename, msg, lineno, column, css){
|
||||
super(filename + ":" + lineno + ":" + column + ": " + msg);
|
||||
this.reason = msg;
|
||||
this.filename = filename;
|
||||
this.line = lineno;
|
||||
this.column = column;
|
||||
this.source = css;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var $0865a9fb4cc365fe$exports = {};
|
||||
|
||||
$parcel$defineInteropFlag($0865a9fb4cc365fe$exports);
|
||||
|
||||
$parcel$export($0865a9fb4cc365fe$exports, "default", () => $0865a9fb4cc365fe$export$2e2bcd8739ae039);
|
||||
/**
|
||||
* Store position information for a node
|
||||
*/ class $0865a9fb4cc365fe$export$2e2bcd8739ae039 {
|
||||
constructor(start, end, source){
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var $b2e137848b48cf4f$exports = {};
|
||||
|
||||
$parcel$export($b2e137848b48cf4f$exports, "CssTypes", () => $b2e137848b48cf4f$export$9be5dd6e61d5d73a);
|
||||
var $b2e137848b48cf4f$export$9be5dd6e61d5d73a;
|
||||
(function(CssTypes) {
|
||||
CssTypes["stylesheet"] = "stylesheet";
|
||||
CssTypes["rule"] = "rule";
|
||||
CssTypes["declaration"] = "declaration";
|
||||
CssTypes["comment"] = "comment";
|
||||
CssTypes["container"] = "container";
|
||||
CssTypes["charset"] = "charset";
|
||||
CssTypes["document"] = "document";
|
||||
CssTypes["customMedia"] = "custom-media";
|
||||
CssTypes["fontFace"] = "font-face";
|
||||
CssTypes["host"] = "host";
|
||||
CssTypes["import"] = "import";
|
||||
CssTypes["keyframes"] = "keyframes";
|
||||
CssTypes["keyframe"] = "keyframe";
|
||||
CssTypes["layer"] = "layer";
|
||||
CssTypes["media"] = "media";
|
||||
CssTypes["namespace"] = "namespace";
|
||||
CssTypes["page"] = "page";
|
||||
CssTypes["supports"] = "supports";
|
||||
})($b2e137848b48cf4f$export$9be5dd6e61d5d73a || ($b2e137848b48cf4f$export$9be5dd6e61d5d73a = {}));
|
||||
|
||||
|
||||
// http://www.w3.org/TR/CSS21/grammar.html
|
||||
// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
|
||||
// New rule => https://www.w3.org/TR/CSS22/syndata.html#comments
|
||||
// [^] is equivalent to [.\n\r]
|
||||
const $d708735ed1303b43$var$commentre = /\/\*[^]*?(?:\*\/|$)/g;
|
||||
const $d708735ed1303b43$export$98e6a39c04603d36 = (css, options)=>{
|
||||
options = options || {};
|
||||
/**
|
||||
* Positional.
|
||||
*/ let lineno = 1;
|
||||
let column = 1;
|
||||
/**
|
||||
* Update lineno and column based on `str`.
|
||||
*/ function updatePosition(str) {
|
||||
const lines = str.match(/\n/g);
|
||||
if (lines) lineno += lines.length;
|
||||
const i = str.lastIndexOf("\n");
|
||||
column = ~i ? str.length - i : column + str.length;
|
||||
}
|
||||
/**
|
||||
* Mark position and patch `node.position`.
|
||||
*/ function position() {
|
||||
const start = {
|
||||
line: lineno,
|
||||
column: column
|
||||
};
|
||||
return function(node) {
|
||||
node.position = new (0, $0865a9fb4cc365fe$export$2e2bcd8739ae039)(start, {
|
||||
line: lineno,
|
||||
column: column
|
||||
}, options?.source || "");
|
||||
whitespace();
|
||||
return node;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Error `msg`.
|
||||
*/ const errorsList = [];
|
||||
function error(msg) {
|
||||
const err = new (0, $009ddb00d3ec72b8$export$2e2bcd8739ae039)(options?.source || "", msg, lineno, column, css);
|
||||
if (options?.silent) errorsList.push(err);
|
||||
else throw err;
|
||||
}
|
||||
/**
|
||||
* Parse stylesheet.
|
||||
*/ function stylesheet() {
|
||||
const rulesList = rules();
|
||||
const result = {
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).stylesheet,
|
||||
stylesheet: {
|
||||
source: options?.source,
|
||||
rules: rulesList,
|
||||
parsingErrors: errorsList
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Opening brace.
|
||||
*/ function open() {
|
||||
return match(/^{\s*/);
|
||||
}
|
||||
/**
|
||||
* Closing brace.
|
||||
*/ function close() {
|
||||
return match(/^}/);
|
||||
}
|
||||
/**
|
||||
* Parse ruleset.
|
||||
*/ function rules() {
|
||||
let node;
|
||||
const rules = [];
|
||||
whitespace();
|
||||
comments(rules);
|
||||
while(css.length && css.charAt(0) !== "}" && (node = atrule() || rule()))if (node) {
|
||||
rules.push(node);
|
||||
comments(rules);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
/**
|
||||
* Match `re` and return captures.
|
||||
*/ function match(re) {
|
||||
const m = re.exec(css);
|
||||
if (!m) return;
|
||||
const str = m[0];
|
||||
updatePosition(str);
|
||||
css = css.slice(str.length);
|
||||
return m;
|
||||
}
|
||||
/**
|
||||
* Parse whitespace.
|
||||
*/ function whitespace() {
|
||||
match(/^\s*/);
|
||||
}
|
||||
/**
|
||||
* Parse comments;
|
||||
*/ function comments(rules) {
|
||||
let c;
|
||||
rules = rules || [];
|
||||
while(c = comment())if (c) rules.push(c);
|
||||
return rules;
|
||||
}
|
||||
/**
|
||||
* Parse comment.
|
||||
*/ function comment() {
|
||||
const pos = position();
|
||||
if ("/" !== css.charAt(0) || "*" !== css.charAt(1)) return;
|
||||
const m = match(/^\/\*[^]*?\*\//);
|
||||
if (!m) return error("End of comment missing");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).comment,
|
||||
comment: m[0].slice(2, -2)
|
||||
});
|
||||
}
|
||||
function findClosingParenthese(str, start, depth) {
|
||||
let ptr = start + 1;
|
||||
let found = false;
|
||||
let closeParentheses = str.indexOf(")", ptr);
|
||||
while(!found && closeParentheses !== -1){
|
||||
const nextParentheses = str.indexOf("(", ptr);
|
||||
if (nextParentheses !== -1 && nextParentheses < closeParentheses) {
|
||||
const nextSearch = findClosingParenthese(str, nextParentheses + 1, depth + 1);
|
||||
ptr = nextSearch + 1;
|
||||
closeParentheses = str.indexOf(")", ptr);
|
||||
} else found = true;
|
||||
}
|
||||
if (found && closeParentheses !== -1) return closeParentheses;
|
||||
else return -1;
|
||||
}
|
||||
/**
|
||||
* Parse selector.
|
||||
*/ function selector() {
|
||||
const m = match(/^([^{]+)/);
|
||||
if (!m) return;
|
||||
// remove comment in selector;
|
||||
let res = $d708735ed1303b43$var$trim(m[0]).replace($d708735ed1303b43$var$commentre, "");
|
||||
// Optimisation: If there is no ',' no need to split or post-process (this is less costly)
|
||||
if (res.indexOf(",") === -1) return [
|
||||
res
|
||||
];
|
||||
// Replace all the , in the parentheses by \u200C
|
||||
let ptr = 0;
|
||||
let startParentheses = res.indexOf("(", ptr);
|
||||
while(startParentheses !== -1){
|
||||
const closeParentheses = findClosingParenthese(res, startParentheses, 0);
|
||||
if (closeParentheses === -1) break;
|
||||
ptr = closeParentheses + 1;
|
||||
res = res.substring(0, startParentheses) + res.substring(startParentheses, closeParentheses).replace(/,/g, "\u200C") + res.substring(closeParentheses);
|
||||
startParentheses = res.indexOf("(", ptr);
|
||||
}
|
||||
// Replace all the , in ' and " by \u200C
|
||||
res = res/**
|
||||
* replace ',' by \u200C for data selector (div[data-lang="fr,de,us"])
|
||||
*
|
||||
* Examples:
|
||||
* div[data-lang="fr,\"de,us"]
|
||||
* div[data-lang='fr,\'de,us']
|
||||
*
|
||||
* Regex logic:
|
||||
* ("|')(?:\\\1|.)*?\1 => Handle the " and '
|
||||
*
|
||||
* Optimization 1:
|
||||
* No greedy capture (see docs about the difference between .* and .*?)
|
||||
*
|
||||
* Optimization 2:
|
||||
* ("|')(?:\\\1|.)*?\1 this use reference to capture group, it work faster.
|
||||
*/ .replace(/("|')(?:\\\1|.)*?\1/g, (m)=>m.replace(/,/g, "\u200C"));
|
||||
// Split all the left , and replace all the \u200C by ,
|
||||
return res// Split the selector by ','
|
||||
.split(",")// Replace back \u200C by ','
|
||||
.map((s)=>{
|
||||
return $d708735ed1303b43$var$trim(s.replace(/\u200C/g, ","));
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse declaration.
|
||||
*/ function declaration() {
|
||||
const pos = position();
|
||||
// prop
|
||||
const propMatch = match(/^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
|
||||
if (!propMatch) return;
|
||||
const propValue = $d708735ed1303b43$var$trim(propMatch[0]);
|
||||
// :
|
||||
if (!match(/^:\s*/)) return error("property missing ':'");
|
||||
// val
|
||||
const val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/);
|
||||
const ret = pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).declaration,
|
||||
property: propValue.replace($d708735ed1303b43$var$commentre, ""),
|
||||
value: val ? $d708735ed1303b43$var$trim(val[0]).replace($d708735ed1303b43$var$commentre, "") : ""
|
||||
});
|
||||
// ;
|
||||
match(/^[;\s]*/);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Parse declarations.
|
||||
*/ function declarations() {
|
||||
const decls = [];
|
||||
if (!open()) return error("missing '{'");
|
||||
comments(decls);
|
||||
// declarations
|
||||
let decl;
|
||||
while(decl = declaration())if (decl) {
|
||||
decls.push(decl);
|
||||
comments(decls);
|
||||
}
|
||||
if (!close()) return error("missing '}'");
|
||||
return decls;
|
||||
}
|
||||
/**
|
||||
* Parse keyframe.
|
||||
*/ function keyframe() {
|
||||
let m;
|
||||
const vals = [];
|
||||
const pos = position();
|
||||
while(m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)){
|
||||
vals.push(m[1]);
|
||||
match(/^,\s*/);
|
||||
}
|
||||
if (!vals.length) return;
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).keyframe,
|
||||
values: vals,
|
||||
declarations: declarations() || []
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse keyframes.
|
||||
*/ function atkeyframes() {
|
||||
const pos = position();
|
||||
const m1 = match(/^@([-\w]+)?keyframes\s*/);
|
||||
if (!m1) return;
|
||||
const vendor = m1[1];
|
||||
// identifier
|
||||
const m2 = match(/^([-\w]+)\s*/);
|
||||
if (!m2) return error("@keyframes missing name");
|
||||
const name = m2[1];
|
||||
if (!open()) return error("@keyframes missing '{'");
|
||||
let frame;
|
||||
let frames = comments();
|
||||
while(frame = keyframe()){
|
||||
frames.push(frame);
|
||||
frames = frames.concat(comments());
|
||||
}
|
||||
if (!close()) return error("@keyframes missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).keyframes,
|
||||
name: name,
|
||||
vendor: vendor,
|
||||
keyframes: frames
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse supports.
|
||||
*/ function atsupports() {
|
||||
const pos = position();
|
||||
const m = match(/^@supports *([^{]+)/);
|
||||
if (!m) return;
|
||||
const supports = $d708735ed1303b43$var$trim(m[1]);
|
||||
if (!open()) return error("@supports missing '{'");
|
||||
const style = comments().concat(rules());
|
||||
if (!close()) return error("@supports missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).supports,
|
||||
supports: supports,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse host.
|
||||
*/ function athost() {
|
||||
const pos = position();
|
||||
const m = match(/^@host\s*/);
|
||||
if (!m) return;
|
||||
if (!open()) return error("@host missing '{'");
|
||||
const style = comments().concat(rules());
|
||||
if (!close()) return error("@host missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).host,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse container.
|
||||
*/ function atcontainer() {
|
||||
const pos = position();
|
||||
const m = match(/^@container *([^{]+)/);
|
||||
if (!m) return;
|
||||
const container = $d708735ed1303b43$var$trim(m[1]);
|
||||
if (!open()) return error("@container missing '{'");
|
||||
const style = comments().concat(rules());
|
||||
if (!close()) return error("@container missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).container,
|
||||
container: container,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse container.
|
||||
*/ function atlayer() {
|
||||
const pos = position();
|
||||
const m = match(/^@layer *([^{;@]+)/);
|
||||
if (!m) return;
|
||||
const layer = $d708735ed1303b43$var$trim(m[1]);
|
||||
if (!open()) {
|
||||
match(/^[;\s]*/);
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).layer,
|
||||
layer: layer
|
||||
});
|
||||
}
|
||||
const style = comments().concat(rules());
|
||||
if (!close()) return error("@layer missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).layer,
|
||||
layer: layer,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse media.
|
||||
*/ function atmedia() {
|
||||
const pos = position();
|
||||
const m = match(/^@media *([^{]+)/);
|
||||
if (!m) return;
|
||||
const media = $d708735ed1303b43$var$trim(m[1]);
|
||||
if (!open()) return error("@media missing '{'");
|
||||
const style = comments().concat(rules());
|
||||
if (!close()) return error("@media missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).media,
|
||||
media: media,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse custom-media.
|
||||
*/ function atcustommedia() {
|
||||
const pos = position();
|
||||
const m = match(/^@custom-media\s+(--\S+)\s*([^{;\s][^{;]*);/);
|
||||
if (!m) return;
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).customMedia,
|
||||
name: $d708735ed1303b43$var$trim(m[1]),
|
||||
media: $d708735ed1303b43$var$trim(m[2])
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse paged media.
|
||||
*/ function atpage() {
|
||||
const pos = position();
|
||||
const m = match(/^@page */);
|
||||
if (!m) return;
|
||||
const sel = selector() || [];
|
||||
if (!open()) return error("@page missing '{'");
|
||||
let decls = comments();
|
||||
// declarations
|
||||
let decl;
|
||||
while(decl = declaration()){
|
||||
decls.push(decl);
|
||||
decls = decls.concat(comments());
|
||||
}
|
||||
if (!close()) return error("@page missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).page,
|
||||
selectors: sel,
|
||||
declarations: decls
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse document.
|
||||
*/ function atdocument() {
|
||||
const pos = position();
|
||||
const m = match(/^@([-\w]+)?document *([^{]+)/);
|
||||
if (!m) return;
|
||||
const vendor = $d708735ed1303b43$var$trim(m[1]);
|
||||
const doc = $d708735ed1303b43$var$trim(m[2]);
|
||||
if (!open()) return error("@document missing '{'");
|
||||
const style = comments().concat(rules());
|
||||
if (!close()) return error("@document missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).document,
|
||||
document: doc,
|
||||
vendor: vendor,
|
||||
rules: style
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse font-face.
|
||||
*/ function atfontface() {
|
||||
const pos = position();
|
||||
const m = match(/^@font-face\s*/);
|
||||
if (!m) return;
|
||||
if (!open()) return error("@font-face missing '{'");
|
||||
let decls = comments();
|
||||
// declarations
|
||||
let decl;
|
||||
while(decl = declaration()){
|
||||
decls.push(decl);
|
||||
decls = decls.concat(comments());
|
||||
}
|
||||
if (!close()) return error("@font-face missing '}'");
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).fontFace,
|
||||
declarations: decls
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parse import
|
||||
*/ const atimport = _compileAtrule("import");
|
||||
/**
|
||||
* Parse charset
|
||||
*/ const atcharset = _compileAtrule("charset");
|
||||
/**
|
||||
* Parse namespace
|
||||
*/ const atnamespace = _compileAtrule("namespace");
|
||||
/**
|
||||
* Parse non-block at-rules
|
||||
*/ function _compileAtrule(name) {
|
||||
const re = new RegExp("^@" + name + "\\s*((?::?[^;'\"]|\"(?:\\\\\"|[^\"])*?\"|'(?:\\\\'|[^'])*?')+)(?:;|$)");
|
||||
// ^@import\s*([^;"']|("|')(?:\\\2|.)*?\2)+(;|$)
|
||||
return function() {
|
||||
const pos = position();
|
||||
const m = match(re);
|
||||
if (!m) return;
|
||||
const ret = {
|
||||
type: name
|
||||
};
|
||||
ret[name] = m[1].trim();
|
||||
return pos(ret);
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Parse at rule.
|
||||
*/ function atrule() {
|
||||
if (css[0] !== "@") return;
|
||||
return atkeyframes() || atmedia() || atcustommedia() || atsupports() || atimport() || atcharset() || atnamespace() || atdocument() || atpage() || athost() || atfontface() || atcontainer() || atlayer();
|
||||
}
|
||||
/**
|
||||
* Parse rule.
|
||||
*/ function rule() {
|
||||
const pos = position();
|
||||
const sel = selector();
|
||||
if (!sel) return error("selector missing");
|
||||
comments();
|
||||
return pos({
|
||||
type: (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).rule,
|
||||
selectors: sel,
|
||||
declarations: declarations() || []
|
||||
});
|
||||
}
|
||||
return $d708735ed1303b43$var$addParent(stylesheet());
|
||||
};
|
||||
/**
|
||||
* Trim `str`.
|
||||
*/ function $d708735ed1303b43$var$trim(str) {
|
||||
return str ? str.trim() : "";
|
||||
}
|
||||
/**
|
||||
* Adds non-enumerable parent node reference to each node.
|
||||
*/ function $d708735ed1303b43$var$addParent(obj, parent) {
|
||||
const isNode = obj && typeof obj.type === "string";
|
||||
const childParent = isNode ? obj : parent;
|
||||
for(const k in obj){
|
||||
const value = obj[k];
|
||||
if (Array.isArray(value)) value.forEach((v)=>{
|
||||
$d708735ed1303b43$var$addParent(v, childParent);
|
||||
});
|
||||
else if (value && typeof value === "object") $d708735ed1303b43$var$addParent(value, childParent);
|
||||
}
|
||||
if (isNode) Object.defineProperty(obj, "parent", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
value: parent || null
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
var $d708735ed1303b43$export$2e2bcd8739ae039 = $d708735ed1303b43$export$98e6a39c04603d36;
|
||||
|
||||
|
||||
|
||||
class $de9540138ed1fd01$var$Compiler {
|
||||
constructor(options){
|
||||
this.level = 0;
|
||||
this.indentation = " ";
|
||||
this.compress = false;
|
||||
if (typeof options?.indent === "string") this.indentation = options?.indent;
|
||||
if (options?.compress) this.compress = true;
|
||||
}
|
||||
// We disable no-unused-vars for _position. We keep position for potential reintroduction of source-map
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
emit(str, _position) {
|
||||
return str;
|
||||
}
|
||||
/**
|
||||
* Increase, decrease or return current indentation.
|
||||
*/ indent(level) {
|
||||
this.level = this.level || 1;
|
||||
if (level) {
|
||||
this.level += level;
|
||||
return "";
|
||||
}
|
||||
return Array(this.level).join(this.indentation);
|
||||
}
|
||||
visit(node) {
|
||||
switch(node.type){
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).stylesheet:
|
||||
return this.stylesheet(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).rule:
|
||||
return this.rule(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).declaration:
|
||||
return this.declaration(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).comment:
|
||||
return this.comment(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).container:
|
||||
return this.container(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).charset:
|
||||
return this.charset(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).document:
|
||||
return this.document(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).customMedia:
|
||||
return this.customMedia(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).fontFace:
|
||||
return this.fontFace(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).host:
|
||||
return this.host(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).import:
|
||||
return this.import(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).keyframes:
|
||||
return this.keyframes(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).keyframe:
|
||||
return this.keyframe(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).layer:
|
||||
return this.layer(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).media:
|
||||
return this.media(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).namespace:
|
||||
return this.namespace(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).page:
|
||||
return this.page(node);
|
||||
case (0, $b2e137848b48cf4f$export$9be5dd6e61d5d73a).supports:
|
||||
return this.supports(node);
|
||||
}
|
||||
}
|
||||
mapVisit(nodes, delim) {
|
||||
let buf = "";
|
||||
delim = delim || "";
|
||||
for(let i = 0, length = nodes.length; i < length; i++){
|
||||
buf += this.visit(nodes[i]);
|
||||
if (delim && i < length - 1) buf += this.emit(delim);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
compile(node) {
|
||||
if (this.compress) return node.stylesheet.rules.map(this.visit, this).join("");
|
||||
return this.stylesheet(node);
|
||||
}
|
||||
/**
|
||||
* Visit stylesheet node.
|
||||
*/ stylesheet(node) {
|
||||
return this.mapVisit(node.stylesheet.rules, "\n\n");
|
||||
}
|
||||
/**
|
||||
* Visit comment node.
|
||||
*/ comment(node) {
|
||||
if (this.compress) return this.emit("", node.position);
|
||||
return this.emit(this.indent() + "/*" + node.comment + "*/", node.position);
|
||||
}
|
||||
/**
|
||||
* Visit container node.
|
||||
*/ container(node) {
|
||||
if (this.compress) return this.emit("@container " + node.container, node.position) + this.emit("{") + this.mapVisit(node.rules) + this.emit("}");
|
||||
return this.emit(this.indent() + "@container " + node.container, node.position) + this.emit(" {\n" + this.indent(1)) + this.mapVisit(node.rules, "\n\n") + this.emit("\n" + this.indent(-1) + this.indent() + "}");
|
||||
}
|
||||
/**
|
||||
* Visit container node.
|
||||
*/ layer(node) {
|
||||
if (this.compress) return this.emit("@layer " + node.layer, node.position) + (node.rules ? this.emit("{") + this.mapVisit(node.rules) + this.emit("}") : ";");
|
||||
return this.emit(this.indent() + "@layer " + node.layer, node.position) + (node.rules ? this.emit(" {\n" + this.indent(1)) + this.mapVisit(node.rules, "\n\n") + this.emit("\n" + this.indent(-1) + this.indent() + "}") : ";");
|
||||
}
|
||||
/**
|
||||
* Visit import node.
|
||||
*/ import(node) {
|
||||
return this.emit("@import " + node.import + ";", node.position);
|
||||
}
|
||||
/**
|
||||
* Visit media node.
|
||||
*/ media(node) {
|
||||
if (this.compress) return this.emit("@media " + node.media, node.position) + this.emit("{") + this.mapVisit(node.rules) + this.emit("}");
|
||||
return this.emit(this.indent() + "@media " + node.media, node.position) + this.emit(" {\n" + this.indent(1)) + this.mapVisit(node.rules, "\n\n") + this.emit("\n" + this.indent(-1) + this.indent() + "}");
|
||||
}
|
||||
/**
|
||||
* Visit document node.
|
||||
*/ document(node) {
|
||||
const doc = "@" + (node.vendor || "") + "document " + node.document;
|
||||
if (this.compress) return this.emit(doc, node.position) + this.emit("{") + this.mapVisit(node.rules) + this.emit("}");
|
||||
return this.emit(doc, node.position) + this.emit(" {\n" + this.indent(1)) + this.mapVisit(node.rules, "\n\n") + this.emit(this.indent(-1) + "\n}");
|
||||
}
|
||||
/**
|
||||
* Visit charset node.
|
||||
*/ charset(node) {
|
||||
return this.emit("@charset " + node.charset + ";", node.position);
|
||||
}
|
||||
/**
|
||||
* Visit namespace node.
|
||||
*/ namespace(node) {
|
||||
return this.emit("@namespace " + node.namespace + ";", node.position);
|
||||
}
|
||||
/**
|
||||
* Visit supports node.
|
||||
*/ supports(node) {
|
||||
if (this.compress) return this.emit("@supports " + node.supports, node.position) + this.emit("{") + this.mapVisit(node.rules) + this.emit("}");
|
||||
return this.emit(this.indent() + "@supports " + node.supports, node.position) + this.emit(" {\n" + this.indent(1)) + this.mapVisit(node.rules, "\n\n") + this.emit("\n" + this.indent(-1) + this.indent() + "}");
|
||||
}
|
||||
/**
|
||||
* Visit keyframes node.
|
||||
*/ keyframes(node) {
|
||||
if (this.compress) return this.emit("@" + (node.vendor || "") + "keyframes " + node.name, node.position) + this.emit("{") + this.mapVisit(node.keyframes) + this.emit("}");
|
||||
return this.emit("@" + (node.vendor || "") + "keyframes " + node.name, node.position) + this.emit(" {\n" + this.indent(1)) + this.mapVisit(node.keyframes, "\n") + this.emit(this.indent(-1) + "}");
|
||||
}
|
||||
/**
|
||||
* Visit keyframe node.
|
||||
*/ keyframe(node) {
|
||||
const decls = node.declarations;
|
||||
if (this.compress) return this.emit(node.values.join(","), node.position) + this.emit("{") + this.mapVisit(decls) + this.emit("}");
|
||||
return this.emit(this.indent()) + this.emit(node.values.join(", "), node.position) + this.emit(" {\n" + this.indent(1)) + this.mapVisit(decls, "\n") + this.emit(this.indent(-1) + "\n" + this.indent() + "}\n");
|
||||
}
|
||||
/**
|
||||
* Visit page node.
|
||||
*/ page(node) {
|
||||
if (this.compress) {
|
||||
const sel = node.selectors.length ? node.selectors.join(", ") : "";
|
||||
return this.emit("@page " + sel, node.position) + this.emit("{") + this.mapVisit(node.declarations) + this.emit("}");
|
||||
}
|
||||
const sel = node.selectors.length ? node.selectors.join(", ") + " " : "";
|
||||
return this.emit("@page " + sel, node.position) + this.emit("{\n") + this.emit(this.indent(1)) + this.mapVisit(node.declarations, "\n") + this.emit(this.indent(-1)) + this.emit("\n}");
|
||||
}
|
||||
/**
|
||||
* Visit font-face node.
|
||||
*/ fontFace(node) {
|
||||
if (this.compress) return this.emit("@font-face", node.position) + this.emit("{") + this.mapVisit(node.declarations) + this.emit("}");
|
||||
return this.emit("@font-face ", node.position) + this.emit("{\n") + this.emit(this.indent(1)) + this.mapVisit(node.declarations, "\n") + this.emit(this.indent(-1)) + this.emit("\n}");
|
||||
}
|
||||
/**
|
||||
* Visit host node.
|
||||
*/ host(node) {
|
||||
if (this.compress) return this.emit("@host", node.position) + this.emit("{") + this.mapVisit(node.rules) + this.emit("}");
|
||||
return this.emit("@host", node.position) + this.emit(" {\n" + this.indent(1)) + this.mapVisit(node.rules, "\n\n") + this.emit(this.indent(-1) + "\n}");
|
||||
}
|
||||
/**
|
||||
* Visit custom-media node.
|
||||
*/ customMedia(node) {
|
||||
return this.emit("@custom-media " + node.name + " " + node.media + ";", node.position);
|
||||
}
|
||||
/**
|
||||
* Visit rule node.
|
||||
*/ rule(node) {
|
||||
const decls = node.declarations;
|
||||
if (!decls.length) return "";
|
||||
if (this.compress) return this.emit(node.selectors.join(","), node.position) + this.emit("{") + this.mapVisit(decls) + this.emit("}");
|
||||
const indent = this.indent();
|
||||
return this.emit(node.selectors.map((s)=>{
|
||||
return indent + s;
|
||||
}).join(",\n"), node.position) + this.emit(" {\n") + this.emit(this.indent(1)) + this.mapVisit(decls, "\n") + this.emit(this.indent(-1)) + this.emit("\n" + this.indent() + "}");
|
||||
}
|
||||
/**
|
||||
* Visit declaration node.
|
||||
*/ declaration(node) {
|
||||
if (this.compress) return this.emit(node.property + ":" + node.value, node.position) + this.emit(";");
|
||||
return this.emit(this.indent()) + this.emit(node.property + ": " + node.value, node.position) + this.emit(";");
|
||||
}
|
||||
}
|
||||
var $de9540138ed1fd01$export$2e2bcd8739ae039 = $de9540138ed1fd01$var$Compiler;
|
||||
|
||||
|
||||
var $fdf773ab87e20450$export$2e2bcd8739ae039 = (node, options)=>{
|
||||
const compiler = new (0, $de9540138ed1fd01$export$2e2bcd8739ae039)(options || {});
|
||||
return compiler.compile(node);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const $149c1bd638913645$export$98e6a39c04603d36 = (0, $d708735ed1303b43$export$2e2bcd8739ae039);
|
||||
const $149c1bd638913645$export$fac44ee5b035f737 = (0, $fdf773ab87e20450$export$2e2bcd8739ae039);
|
||||
var $149c1bd638913645$export$2e2bcd8739ae039 = {
|
||||
parse: $149c1bd638913645$export$98e6a39c04603d36,
|
||||
stringify: $149c1bd638913645$export$fac44ee5b035f737
|
||||
};
|
||||
|
||||
|
||||
export {$149c1bd638913645$export$98e6a39c04603d36 as parse, $149c1bd638913645$export$fac44ee5b035f737 as stringify, $149c1bd638913645$export$2e2bcd8739ae039 as default, $b2e137848b48cf4f$export$9be5dd6e61d5d73a as CssTypes};
|
||||
//# sourceMappingURL=index.mjs.map
|
@ -69,6 +69,27 @@ EventEmitter.prototype.emit = async function (event) {
|
||||
}
|
||||
};
|
||||
|
||||
EventEmitter.prototype.emitAndWait = function (event) {
|
||||
console.debug('Event emitted: ' + event);
|
||||
|
||||
var i, listeners, length, args = [].slice.call(arguments, 1);
|
||||
|
||||
if (typeof this.events[event] === 'object') {
|
||||
listeners = this.events[event].slice();
|
||||
length = listeners.length;
|
||||
|
||||
for (i = 0; i < length; i++) {
|
||||
try {
|
||||
listeners[i].apply(this, args);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
console.trace('Error in event listener');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
EventEmitter.prototype.once = function (event, listener) {
|
||||
this.on(event, function g () {
|
||||
this.removeListener(event, g);
|
||||
|
1766
public/script.js
1766
public/script.js
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,6 @@ import {
|
||||
online_status,
|
||||
main_api,
|
||||
api_server,
|
||||
api_server_textgenerationwebui,
|
||||
is_send_press,
|
||||
max_context,
|
||||
saveSettingsDebounced,
|
||||
@ -18,6 +17,7 @@ import {
|
||||
eventSource,
|
||||
menu_type,
|
||||
substituteParams,
|
||||
callPopup,
|
||||
} from '../script.js';
|
||||
|
||||
import {
|
||||
@ -34,8 +34,9 @@ import {
|
||||
import { debounce, delay, getStringHash, isValidUrl } from './utils.js';
|
||||
import { chat_completion_sources, oai_settings } from './openai.js';
|
||||
import { getTokenCount } from './tokenizers.js';
|
||||
import { textgen_types, textgenerationwebui_settings as textgen_settings } from './textgen-settings.js';
|
||||
import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
|
||||
|
||||
import Bowser from '../lib/bowser.min.js';
|
||||
|
||||
var RPanelPin = document.getElementById('rm_button_panel_pin');
|
||||
var LPanelPin = document.getElementById('lm_button_panel_pin');
|
||||
@ -98,43 +99,22 @@ export function humanizeGenTime(total_gen_time) {
|
||||
return time_spent;
|
||||
}
|
||||
|
||||
let parsedUA = null;
|
||||
try {
|
||||
parsedUA = Bowser.parse(navigator.userAgent);
|
||||
} catch {
|
||||
// In case the user agent is an empty string or Bowser can't parse it for some other reason
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the device is a mobile device.
|
||||
* @returns {boolean} - True if the device is a mobile device, false otherwise.
|
||||
*/
|
||||
export function isMobile() {
|
||||
const mobileTypes = ['smartphone', 'tablet', 'phablet', 'feature phone', 'portable media player'];
|
||||
const deviceInfo = getDeviceInfo();
|
||||
const mobileTypes = ['mobile', 'tablet'];
|
||||
|
||||
return mobileTypes.includes(deviceInfo?.device?.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads device info from the server. Caches the result in sessionStorage.
|
||||
* @returns {object} - The device info object.
|
||||
*/
|
||||
export function getDeviceInfo() {
|
||||
let deviceInfo = null;
|
||||
|
||||
if (sessionStorage.getItem('deviceInfo')) {
|
||||
deviceInfo = JSON.parse(sessionStorage.getItem('deviceInfo'));
|
||||
} else {
|
||||
$.ajax({
|
||||
url: '/deviceinfo',
|
||||
dataType: 'json',
|
||||
async: false,
|
||||
cache: true,
|
||||
success: function (result) {
|
||||
sessionStorage.setItem('deviceInfo', JSON.stringify(result));
|
||||
deviceInfo = result;
|
||||
},
|
||||
error: function () {
|
||||
console.log('Couldn\'t load device info. Defaulting to desktop');
|
||||
deviceInfo = { device: { type: 'desktop' } };
|
||||
},
|
||||
});
|
||||
}
|
||||
return deviceInfo;
|
||||
return mobileTypes.includes(parsedUA?.platform?.type);
|
||||
}
|
||||
|
||||
function shouldSendOnEnter() {
|
||||
@ -401,10 +381,12 @@ function RA_autoconnect(PrevApi) {
|
||||
}
|
||||
break;
|
||||
case 'textgenerationwebui':
|
||||
if (textgen_settings.type === textgen_types.MANCER && secret_state[SECRET_KEYS.MANCER]) {
|
||||
if ((textgen_settings.type === textgen_types.MANCER && secret_state[SECRET_KEYS.MANCER]) ||
|
||||
(textgen_settings.type === textgen_types.TOGETHERAI && secret_state[SECRET_KEYS.TOGETHERAI])
|
||||
) {
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
}
|
||||
else if (api_server_textgenerationwebui && isValidUrl(api_server_textgenerationwebui)) {
|
||||
else if (isValidUrl(getTextGenServer())) {
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
}
|
||||
break;
|
||||
@ -415,7 +397,9 @@ function RA_autoconnect(PrevApi) {
|
||||
|| (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI)
|
||||
|| (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)
|
||||
|| (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
|
||||
|| (secret_state[SECRET_KEYS.PALM] && oai_settings.chat_completion_source == chat_completion_sources.PALM)
|
||||
|| (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)
|
||||
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
|
||||
) {
|
||||
$('#api_button_openai').trigger('click');
|
||||
}
|
||||
@ -431,8 +415,7 @@ function RA_autoconnect(PrevApi) {
|
||||
}
|
||||
|
||||
function OpenNavPanels() {
|
||||
const deviceInfo = getDeviceInfo();
|
||||
if (deviceInfo && deviceInfo.device.type === 'desktop') {
|
||||
if (!isMobile()) {
|
||||
//auto-open R nav if locked and previously open
|
||||
if (LoadLocalBool('NavLockOn') == true && LoadLocalBool('NavOpened') == true) {
|
||||
//console.log("RA -- clicking right nav to open");
|
||||
@ -508,7 +491,7 @@ export function dragElement(elmnt) {
|
||||
|| Number((String(target.height).replace('px', ''))) < 50
|
||||
|| Number((String(target.width).replace('px', ''))) < 50
|
||||
|| power_user.movingUI === false
|
||||
|| isMobile() === true
|
||||
|| isMobile()
|
||||
) {
|
||||
console.debug('aborting mutator');
|
||||
return;
|
||||
@ -716,7 +699,7 @@ export function dragElement(elmnt) {
|
||||
}
|
||||
|
||||
export async function initMovingUI() {
|
||||
if (isMobile() === false && power_user.movingUI === true) {
|
||||
if (!isMobile() && power_user.movingUI === true) {
|
||||
console.debug('START MOVING UI');
|
||||
dragElement($('#sheld'));
|
||||
dragElement($('#left-nav-panel'));
|
||||
@ -902,7 +885,7 @@ export function initRossMods() {
|
||||
const chatBlock = $('#chat');
|
||||
const originalScrollBottom = chatBlock[0].scrollHeight - (chatBlock.scrollTop() + chatBlock.outerHeight());
|
||||
this.style.height = window.getComputedStyle(this).getPropertyValue('min-height');
|
||||
this.style.height = this.scrollHeight + 0.1 + 'px';
|
||||
this.style.height = this.scrollHeight + 0.3 + 'px';
|
||||
|
||||
if (!isFirefox) {
|
||||
const newScrollTop = Math.round(chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom));
|
||||
@ -1015,9 +998,31 @@ export function initRossMods() {
|
||||
console.debug('Accepting edits with Ctrl+Enter');
|
||||
editMesDone.trigger('click');
|
||||
} else if (is_send_press == false) {
|
||||
console.debug('Regenerating with Ctrl+Enter');
|
||||
$('#option_regenerate').click();
|
||||
$('#options').hide();
|
||||
const skipConfirmKey = 'RegenerateWithCtrlEnter';
|
||||
const skipConfirm = LoadLocalBool(skipConfirmKey);
|
||||
function doRegenerate() {
|
||||
console.debug('Regenerating with Ctrl+Enter');
|
||||
$('#option_regenerate').trigger('click');
|
||||
$('#options').hide();
|
||||
}
|
||||
if (skipConfirm) {
|
||||
doRegenerate();
|
||||
} else {
|
||||
const popupText = `
|
||||
<div class="marginBot10">Are you sure you want to regenerate the latest message?</div>
|
||||
<label class="checkbox_label justifyCenter" for="regenerateWithCtrlEnter">
|
||||
<input type="checkbox" id="regenerateWithCtrlEnter">
|
||||
Don't ask again
|
||||
</label>`;
|
||||
callPopup(popupText, 'confirm').then(result =>{
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const regenerateWithCtrlEnter = $('#regenerateWithCtrlEnter').prop('checked');
|
||||
SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);
|
||||
doRegenerate();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.debug('Ctrl+Enter ignored');
|
||||
}
|
||||
@ -1073,11 +1078,12 @@ export function initRossMods() {
|
||||
}
|
||||
|
||||
if (event.key == 'ArrowUp') { //edits last message if chatbar is empty and focused
|
||||
//console.log('got uparrow input');
|
||||
console.log('got uparrow input');
|
||||
if (
|
||||
$('#send_textarea').val() === '' &&
|
||||
chatbarInFocus === true &&
|
||||
$('.swipe_right:last').css('display') === 'flex' &&
|
||||
//$('.swipe_right:last').css('display') === 'flex' &&
|
||||
$('.last_mes .mes_buttons').is(':visible') &&
|
||||
$('#character_popup').css('display') === 'none' &&
|
||||
$('#shadow_select_chat_popup').css('display') === 'none'
|
||||
) {
|
||||
@ -1123,12 +1129,14 @@ export function initRossMods() {
|
||||
.not('#left-nav-panel')
|
||||
.not('#right-nav-panel')
|
||||
.not('#floatingPrompt')
|
||||
.not('#cfgConfig')
|
||||
.is(':visible')) {
|
||||
let visibleDrawerContent = $('.drawer-content:visible')
|
||||
.not('#WorldInfo')
|
||||
.not('#left-nav-panel')
|
||||
.not('#right-nav-panel')
|
||||
.not('#floatingPrompt');
|
||||
.not('#floatingPrompt')
|
||||
.not('#cfgConfig');
|
||||
$(visibleDrawerContent).parent().find('.drawer-icon').trigger('click');
|
||||
return;
|
||||
}
|
||||
@ -1143,6 +1151,11 @@ export function initRossMods() {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#cfgConfig').is(':visible')) {
|
||||
$('#CFGClose').trigger('click');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#left-nav-panel').is(':visible') &&
|
||||
$(LPanelPin).prop('checked') === false) {
|
||||
$('#leftNavDrawerIcon').trigger('click');
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
animation_duration,
|
||||
chat_metadata,
|
||||
eventSource,
|
||||
event_types,
|
||||
@ -312,7 +313,7 @@ export function setFloatingPrompt() {
|
||||
}
|
||||
}
|
||||
}
|
||||
context.setExtensionPrompt(MODULE_NAME, prompt, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
|
||||
context.setExtensionPrompt(MODULE_NAME, prompt, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan);
|
||||
$('#extension_floating_counter').text(shouldAddPrompt ? '0' : messagesTillInsertion);
|
||||
}
|
||||
|
||||
@ -325,7 +326,7 @@ function onANMenuItemClick() {
|
||||
$('#floatingPrompt').css('opacity', 0.0);
|
||||
$('#floatingPrompt').transition({
|
||||
opacity: 1.0,
|
||||
duration: 250,
|
||||
duration: animation_duration,
|
||||
}, async function () {
|
||||
await delay(50);
|
||||
$('#floatingPrompt').removeClass('resizing');
|
||||
@ -343,7 +344,7 @@ function onANMenuItemClick() {
|
||||
$('#floatingPrompt').addClass('resizing');
|
||||
$('#floatingPrompt').transition({
|
||||
opacity: 0.0,
|
||||
duration: 250,
|
||||
duration: animation_duration,
|
||||
},
|
||||
async function () {
|
||||
await delay(50);
|
||||
@ -351,12 +352,12 @@ function onANMenuItemClick() {
|
||||
});
|
||||
setTimeout(function () {
|
||||
$('#floatingPrompt').hide();
|
||||
}, 250);
|
||||
}, animation_duration);
|
||||
|
||||
}
|
||||
//duplicate options menu close handler from script.js
|
||||
//because this listener takes priority
|
||||
$('#options').stop().fadeOut(250);
|
||||
$('#options').stop().fadeOut(animation_duration);
|
||||
} else {
|
||||
toastr.warning('Select a character before trying to use Author\'s Note', '', { timeOut: 2000 });
|
||||
}
|
||||
@ -415,10 +416,10 @@ export function initAuthorsNote() {
|
||||
$('#ANClose').on('click', function () {
|
||||
$('#floatingPrompt').transition({
|
||||
opacity: 0,
|
||||
duration: 200,
|
||||
duration: animation_duration,
|
||||
easing: 'ease-in-out',
|
||||
});
|
||||
setTimeout(function () { $('#floatingPrompt').hide(); }, 200);
|
||||
setTimeout(function () { $('#floatingPrompt').hide(); }, animation_duration);
|
||||
});
|
||||
$('#option_toggle_AN').on('click', onANMenuItemClick);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl } from '../script.js';
|
||||
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
|
||||
import { saveMetadataDebounced } from './extensions.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { stringFormat } from './utils.js';
|
||||
@ -6,6 +6,19 @@ import { stringFormat } from './utils.js';
|
||||
const BG_METADATA_KEY = 'custom_background';
|
||||
const LIST_METADATA_KEY = 'chat_backgrounds';
|
||||
|
||||
export let background_settings = {
|
||||
name: '__transparent.png',
|
||||
url: generateUrlParameter('__transparent.png', false),
|
||||
};
|
||||
|
||||
export function loadBackgroundSettings(settings) {
|
||||
let backgroundSettings = settings.background;
|
||||
if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) {
|
||||
backgroundSettings = background_settings;
|
||||
}
|
||||
setBackground(backgroundSettings.name, backgroundSettings.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background for the current chat and adds it to the list of custom backgrounds.
|
||||
* @param {{url: string, path:string}} backgroundInfo
|
||||
@ -141,9 +154,8 @@ function onSelectBackgroundClick() {
|
||||
saveBackgroundMetadata(relativeBgImage);
|
||||
setCustomBackground();
|
||||
highlightLockedBackground();
|
||||
} else {
|
||||
highlightLockedBackground();
|
||||
}
|
||||
highlightLockedBackground();
|
||||
|
||||
const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage;
|
||||
|
||||
@ -157,8 +169,7 @@ function onSelectBackgroundClick() {
|
||||
|
||||
// Fetching to browser memory to reduce flicker
|
||||
fetch(backgroundUrl).then(() => {
|
||||
$('#bg1').css('background-image', relativeBgImage);
|
||||
setBackground(bgFile);
|
||||
setBackground(bgFile, relativeBgImage);
|
||||
}).catch(() => {
|
||||
console.log('Background could not be set: ' + backgroundUrl);
|
||||
});
|
||||
@ -333,7 +344,7 @@ export async function getBackgrounds() {
|
||||
'': '',
|
||||
}),
|
||||
});
|
||||
if (response.ok === true) {
|
||||
if (response.ok) {
|
||||
const getData = await response.json();
|
||||
//background = getData;
|
||||
//console.log(getData.length);
|
||||
@ -346,7 +357,7 @@ export async function getBackgrounds() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of the background
|
||||
* Gets the CSS URL of the background
|
||||
* @param {Element} block
|
||||
* @returns {string} URL of the background
|
||||
*/
|
||||
@ -354,6 +365,10 @@ function getUrlParameter(block) {
|
||||
return $(block).closest('.bg_example').data('url');
|
||||
}
|
||||
|
||||
function generateUrlParameter(bg, isCustom) {
|
||||
return isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a background template
|
||||
* @param {string} bg Path to background
|
||||
@ -363,7 +378,7 @@ function getUrlParameter(block) {
|
||||
function getBackgroundFromTemplate(bg, isCustom) {
|
||||
const template = $('#background_template .bg_example').clone();
|
||||
const thumbPath = isCustom ? bg : getThumbnailUrl('bg', bg);
|
||||
const url = isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;
|
||||
const url = generateUrlParameter(bg, isCustom);
|
||||
const title = isCustom ? bg.split('/').pop() : bg;
|
||||
const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
|
||||
template.attr('title', title);
|
||||
@ -375,26 +390,11 @@ function getBackgroundFromTemplate(bg, isCustom) {
|
||||
return template;
|
||||
}
|
||||
|
||||
async function setBackground(bg) {
|
||||
jQuery.ajax({
|
||||
type: 'POST', //
|
||||
url: '/api/backgrounds/set', //
|
||||
data: JSON.stringify({
|
||||
bg: bg,
|
||||
}),
|
||||
beforeSend: function () {
|
||||
|
||||
},
|
||||
cache: false,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
//processData: false,
|
||||
success: function (html) { },
|
||||
error: function (jqXHR, exception) {
|
||||
console.log(exception);
|
||||
console.log(jqXHR);
|
||||
},
|
||||
});
|
||||
async function setBackground(bg, url) {
|
||||
$('#bg1').css('background-image', url);
|
||||
background_settings.name = bg;
|
||||
background_settings.url = url;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function delBackground(bg) {
|
||||
@ -435,8 +435,7 @@ function uploadBackground(formData) {
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: async function (bg) {
|
||||
setBackground(bg);
|
||||
$('#bg1').css('background-image', `url("${getBackgroundPath(bg)}"`);
|
||||
setBackground(bg, generateUrlParameter(bg, false));
|
||||
await getBackgrounds();
|
||||
highlightNewBackground(bg);
|
||||
},
|
||||
|
@ -237,6 +237,12 @@ async function convertSoloToGroupChat() {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await callPopup('Are you sure you want to convert this chat to a group chat?', 'confirm');
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const character = characters[this_chid];
|
||||
|
||||
// Populate group required fields
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
eventSource,
|
||||
event_types,
|
||||
saveSettingsDebounced,
|
||||
animation_duration,
|
||||
} from '../script.js';
|
||||
import { extension_settings, saveMetadataDebounced } from './extensions.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
@ -120,7 +121,7 @@ function onCfgMenuItemClick() {
|
||||
$('#cfgConfig').css('opacity', 0.0);
|
||||
$('#cfgConfig').transition({
|
||||
opacity: 1.0,
|
||||
duration: 250,
|
||||
duration: animation_duration,
|
||||
}, async function () {
|
||||
await delay(50);
|
||||
$('#cfgConfig').removeClass('resizing');
|
||||
@ -138,7 +139,7 @@ function onCfgMenuItemClick() {
|
||||
$('#cfgConfig').addClass('resizing');
|
||||
$('#cfgConfig').transition({
|
||||
opacity: 0.0,
|
||||
duration: 250,
|
||||
duration: animation_duration,
|
||||
},
|
||||
async function () {
|
||||
await delay(50);
|
||||
@ -146,12 +147,12 @@ function onCfgMenuItemClick() {
|
||||
});
|
||||
setTimeout(function () {
|
||||
$('#cfgConfig').hide();
|
||||
}, 250);
|
||||
}, animation_duration);
|
||||
|
||||
}
|
||||
//duplicate options menu close handler from script.js
|
||||
//because this listener takes priority
|
||||
$('#options').stop().fadeOut(250);
|
||||
$('#options').stop().fadeOut(animation_duration);
|
||||
} else {
|
||||
toastr.warning('Select a character before trying to configure CFG', '', { timeOut: 2000 });
|
||||
}
|
||||
@ -281,10 +282,10 @@ export function initCfg() {
|
||||
$('#CFGClose').on('click', function () {
|
||||
$('#cfgConfig').transition({
|
||||
opacity: 0,
|
||||
duration: 200,
|
||||
duration: animation_duration,
|
||||
easing: 'ease-in-out',
|
||||
});
|
||||
setTimeout(function () { $('#cfgConfig').hide(); }, 200);
|
||||
setTimeout(function () { $('#cfgConfig').hide(); }, animation_duration);
|
||||
});
|
||||
|
||||
$('#chat_cfg_guidance_scale').on('input', function() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
// Move chat functions here from script.js (eventually)
|
||||
|
||||
import css from '../lib/css-parser.mjs';
|
||||
import {
|
||||
addCopyToCodeBlocks,
|
||||
appendMediaToMessage,
|
||||
@ -341,6 +342,80 @@ function embedMessageFile(messageId, messageBlock) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends file content to the message text.
|
||||
* @param {object} message Message object
|
||||
* @param {string} messageText Message text
|
||||
* @returns {Promise<string>} Message text with file content appended.
|
||||
*/
|
||||
export async function appendFileContent(message, messageText) {
|
||||
if (message.extra?.file) {
|
||||
const fileText = message.extra.file.text || (await getFileAttachment(message.extra.file.url));
|
||||
|
||||
if (fileText) {
|
||||
const fileWrapped = `\`\`\`\n${fileText}\n\`\`\`\n\n`;
|
||||
message.extra.fileLength = fileWrapped.length;
|
||||
messageText = fileWrapped + messageText;
|
||||
}
|
||||
}
|
||||
return messageText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces style tags in the message text with custom tags with encoded content.
|
||||
* @param {string} text
|
||||
* @returns {string} Encoded message text
|
||||
* @copyright https://github.com/kwaroran/risuAI
|
||||
*/
|
||||
export function encodeStyleTags(text) {
|
||||
const styleRegex = /<style>(.+?)<\/style>/gms;
|
||||
return text.replaceAll(styleRegex, (_, match) => {
|
||||
return `<custom-style>${escape(match)}</custom-style>`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes custom style tags in the message text to prevent DOM pollution.
|
||||
* @param {string} text Message text
|
||||
* @returns {string} Sanitized message text
|
||||
* @copyright https://github.com/kwaroran/risuAI
|
||||
*/
|
||||
export function decodeStyleTags(text) {
|
||||
const styleDecodeRegex = /<custom-style>(.+?)<\/custom-style>/gms;
|
||||
|
||||
return text.replaceAll(styleDecodeRegex, (_, style) => {
|
||||
try {
|
||||
const ast = css.parse(unescape(style));
|
||||
const rules = ast?.stylesheet?.rules;
|
||||
if (rules) {
|
||||
for (const rule of rules) {
|
||||
|
||||
if (rule.type === 'rule') {
|
||||
if (rule.selectors) {
|
||||
for (let i = 0; i < rule.selectors.length; i++) {
|
||||
let selector = rule.selectors[i];
|
||||
if (selector) {
|
||||
let selectors = (selector.split(' ') ?? []).map((v) => {
|
||||
if (v.startsWith('.')) {
|
||||
return '.custom-' + v.substring(1);
|
||||
}
|
||||
return v;
|
||||
}).join(' ');
|
||||
|
||||
rule.selectors[i] = '.mes_text ' + selectors;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return `<style>${css.stringify(ast)}</style>`;
|
||||
} catch (error) {
|
||||
return `CSS ERROR: ${error}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
$(document).on('click', '.mes_hide', async function () {
|
||||
const messageBlock = $(this).closest('.mes');
|
||||
@ -380,6 +455,7 @@ jQuery(function () {
|
||||
$(document).on('click', '.editor_maximize', function () {
|
||||
const broId = $(this).attr('data-for');
|
||||
const bro = $(`#${broId}`);
|
||||
const withTab = $(this).attr('data-tab');
|
||||
|
||||
if (!bro.length) {
|
||||
console.error('Could not find editor with id', broId);
|
||||
@ -392,11 +468,41 @@ jQuery(function () {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = String(bro.val());
|
||||
textarea.classList.add('height100p', 'wide100p');
|
||||
textarea.oninput = function () {
|
||||
textarea.addEventListener('input', function () {
|
||||
bro.val(textarea.value).trigger('input');
|
||||
};
|
||||
});
|
||||
wrapper.appendChild(textarea);
|
||||
|
||||
if (withTab) {
|
||||
textarea.addEventListener('keydown', (evt) => {
|
||||
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||
evt.preventDefault();
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
if (end - start > 0 && textarea.value.substring(start, end).includes('\n')) {
|
||||
const lineStart = textarea.value.lastIndexOf('\n', start);
|
||||
const count = textarea.value.substring(lineStart, end).split('\n').length - 1;
|
||||
textarea.value = `${textarea.value.substring(0, lineStart)}${textarea.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${textarea.value.substring(end)}`;
|
||||
textarea.selectionStart = start + 1;
|
||||
textarea.selectionEnd = end + count;
|
||||
} else {
|
||||
textarea.value = `${textarea.value.substring(0, start)}\t${textarea.value.substring(end)}`;
|
||||
textarea.selectionStart = start + 1;
|
||||
textarea.selectionEnd = end + 1;
|
||||
}
|
||||
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||
evt.preventDefault();
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const lineStart = textarea.value.lastIndexOf('\n', start);
|
||||
const count = textarea.value.substring(lineStart, end).split('\n\t').length - 1;
|
||||
textarea.value = `${textarea.value.substring(0, lineStart)}${textarea.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${textarea.value.substring(end)}`;
|
||||
textarea.selectionStart = start - 1;
|
||||
textarea.selectionEnd = end - count;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callPopup(wrapper, 'text', '', { wide: true, large: true });
|
||||
});
|
||||
|
||||
|
@ -47,8 +47,6 @@ export function saveMetadataDebounced() {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
export const extensionsHandlebars = Handlebars.create();
|
||||
|
||||
/**
|
||||
* Provides an ability for extensions to render HTML templates.
|
||||
* Templates sanitation and localization is forced.
|
||||
@ -61,40 +59,6 @@ export function renderExtensionTemplate(extensionName, templateId, templateData
|
||||
return renderTemplate(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a Handlebars helper for use in extensions.
|
||||
* @param {string} name Handlebars helper name
|
||||
* @param {function} helper Handlebars helper function
|
||||
*/
|
||||
export function registerExtensionHelper(name, helper) {
|
||||
extensionsHandlebars.registerHelper(name, helper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies handlebars extension helpers to a message.
|
||||
* @param {number} messageId Message index in the chat.
|
||||
*/
|
||||
export function processExtensionHelpers(messageId) {
|
||||
const context = getContext();
|
||||
const message = context.chat[messageId];
|
||||
|
||||
if (!message?.mes || typeof message.mes !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't waste time if there are no mustaches
|
||||
if (!substituteParams(message.mes).includes('{{')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const template = extensionsHandlebars.compile(substituteParams(message.mes), { noEscape: true });
|
||||
message.mes = template({});
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Disables parallel updates
|
||||
class ModuleWorkerWrapper {
|
||||
constructor(callback) {
|
||||
@ -146,6 +110,7 @@ const extension_settings = {
|
||||
sd: {
|
||||
prompts: {},
|
||||
character_prompts: {},
|
||||
character_negative_prompts: {},
|
||||
},
|
||||
chromadb: {},
|
||||
translate: {},
|
||||
@ -879,7 +844,7 @@ async function runGenerationInterceptors(chat, contextSize) {
|
||||
exitImmediately = immediately;
|
||||
};
|
||||
|
||||
for (const manifest of Object.values(manifests)) {
|
||||
for (const manifest of Object.values(manifests).sort((a, b) => a.loading_order - b.loading_order)) {
|
||||
const interceptorKey = manifest.generate_interceptor;
|
||||
if (typeof window[interceptorKey] === 'function') {
|
||||
try {
|
||||
|
@ -4,6 +4,7 @@ import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams }
|
||||
import { getMessageTimeStamp } from '../../RossAscends-mods.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'caption';
|
||||
@ -216,7 +217,16 @@ async function captionHorde(base64Img) {
|
||||
* @returns {Promise<{caption: string}>} Generated caption
|
||||
*/
|
||||
async function captionMultimodal(base64Img) {
|
||||
const prompt = extension_settings.caption.prompt || PROMPT_DEFAULT;
|
||||
let prompt = extension_settings.caption.prompt || PROMPT_DEFAULT;
|
||||
|
||||
if (extension_settings.caption.prompt_ask) {
|
||||
const customPrompt = await callPopup('<h3>Enter a comment or question:</h3>', 'input', prompt, { rows: 2 });
|
||||
if (!customPrompt) {
|
||||
throw new Error('User aborted the caption sending.');
|
||||
}
|
||||
prompt = String(customPrompt).trim();
|
||||
}
|
||||
|
||||
const caption = await getMultimodalCaption(base64Img, prompt);
|
||||
return { caption };
|
||||
}
|
||||
@ -271,8 +281,12 @@ jQuery(function () {
|
||||
$(sendButton).on('click', () => {
|
||||
const hasCaptionModule =
|
||||
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && secret_state[SECRET_KEYS.OPENAI]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && secret_state[SECRET_KEYS.MAKERSUITE]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') ||
|
||||
extension_settings.caption.source === 'local' ||
|
||||
extension_settings.caption.source === 'horde';
|
||||
|
||||
@ -299,7 +313,7 @@ jQuery(function () {
|
||||
$('#caption_prompt_block').toggle(isMultimodal);
|
||||
$('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
|
||||
$('#caption_multimodal_model').val(extension_settings.caption.multimodal_model);
|
||||
$('#caption_multimodal_model option').each(function () {
|
||||
$('#caption_multimodal_block [data-type]').each(function () {
|
||||
const type = $(this).data('type');
|
||||
$(this).toggle(type === extension_settings.caption.multimodal_api);
|
||||
});
|
||||
@ -328,7 +342,7 @@ jQuery(function () {
|
||||
<label for="caption_source">Source</label>
|
||||
<select id="caption_source" class="text_pole">
|
||||
<option value="local">Local</option>
|
||||
<option value="multimodal">Multimodal (OpenAI / OpenRouter)</option>
|
||||
<option value="multimodal">Multimodal (OpenAI / llama / Google)</option>
|
||||
<option value="extras">Extras</option>
|
||||
<option value="horde">Horde</option>
|
||||
</select>
|
||||
@ -336,22 +350,43 @@ jQuery(function () {
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_api">API</label>
|
||||
<select id="caption_multimodal_api" class="flex1 text_pole">
|
||||
<option value="llamacpp">llama.cpp</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="google">Google MakerSuite</option>
|
||||
<option value="custom">Custom (OpenAI-compatible)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_model">Model</label>
|
||||
<select id="caption_multimodal_model" class="flex1 text_pole">
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
|
||||
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
|
||||
<option data-type="ollama" value="llava:latest">llava:latest</option>
|
||||
<option data-type="llamacpp" value="llamacpp_current">[Currently loaded]</option>
|
||||
<option data-type="custom" value="custom_current">[Currently selected]</option>
|
||||
</select>
|
||||
</div>
|
||||
<label data-type="openai" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
|
||||
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
|
||||
Allow reverse proxy
|
||||
</label>
|
||||
<div class="flexBasis100p m-b-1">
|
||||
<small><b>Hint:</b> Set your API keys and endpoints in the 'API Connections' tab first.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div id="caption_prompt_block">
|
||||
<label for="caption_prompt">Caption Prompt</label>
|
||||
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="< Use default >">${PROMPT_DEFAULT}</textarea>
|
||||
<label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
|
||||
<input id="caption_prompt_ask" type="checkbox" class="checkbox">
|
||||
Ask every time
|
||||
</label>
|
||||
</div>
|
||||
<label for="caption_template">Message Template <small>(use <code>{{caption}}</code> macro)</small></label>
|
||||
<textarea id="caption_template" class="text_pole" rows="2" placeholder="< Use default >">${TEMPLATE_DEFAULT}</textarea>
|
||||
@ -374,6 +409,8 @@ jQuery(function () {
|
||||
switchMultimodalBlocks();
|
||||
|
||||
$('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));
|
||||
$('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy));
|
||||
$('#caption_prompt_ask').prop('checked', !!(extension_settings.caption.prompt_ask));
|
||||
$('#caption_source').val(extension_settings.caption.source);
|
||||
$('#caption_prompt').val(extension_settings.caption.prompt);
|
||||
$('#caption_template').val(extension_settings.caption.template);
|
||||
@ -391,4 +428,12 @@ jQuery(function () {
|
||||
extension_settings.caption.template = String($('#caption_template').val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#caption_allow_reverse_proxy').on('input', () => {
|
||||
extension_settings.caption.allow_reverse_proxy = $('#caption_allow_reverse_proxy').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#caption_prompt_ask').on('input', () => {
|
||||
extension_settings.caption.prompt_ask = $('#caption_prompt_ask').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
});
|
||||
|
@ -992,8 +992,7 @@ async function getExpressionsList() {
|
||||
}
|
||||
|
||||
const result = await resolveExpressionsList();
|
||||
result.push(...extension_settings.expressions.custom);
|
||||
return result;
|
||||
return [...result, ...extension_settings.expressions.custom];
|
||||
}
|
||||
|
||||
async function setExpression(character, expression, force) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js';
|
||||
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from '../../extensions.js';
|
||||
import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { animation_duration, eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { is_group_generating, selected_group } from '../../group-chats.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { loadMovingUIState } from '../../power-user.js';
|
||||
import { dragElement } from '../../RossAscends-mods.js';
|
||||
import { getTextTokens, tokenizers } from '../../tokenizers.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = '1_memory';
|
||||
@ -42,26 +43,6 @@ const defaultPrompt = '[Pause your roleplay. Summarize the most important facts
|
||||
const defaultTemplate = '[Summary: {{summary}}]';
|
||||
|
||||
const defaultSettings = {
|
||||
minLongMemory: 16,
|
||||
maxLongMemory: 1024,
|
||||
longMemoryLength: 128,
|
||||
shortMemoryLength: 512,
|
||||
minShortMemory: 128,
|
||||
maxShortMemory: 1024,
|
||||
shortMemoryStep: 16,
|
||||
longMemoryStep: 8,
|
||||
repetitionPenaltyStep: 0.05,
|
||||
repetitionPenalty: 1.2,
|
||||
maxRepetitionPenalty: 2.0,
|
||||
minRepetitionPenalty: 1.0,
|
||||
temperature: 1.0,
|
||||
minTemperature: 0.1,
|
||||
maxTemperature: 2.0,
|
||||
temperatureStep: 0.05,
|
||||
lengthPenalty: 1,
|
||||
minLengthPenalty: -4,
|
||||
maxLengthPenalty: 4,
|
||||
lengthPenaltyStep: 0.1,
|
||||
memoryFrozen: false,
|
||||
SkipWIAN: false,
|
||||
source: summary_sources.extras,
|
||||
@ -95,11 +76,6 @@ function loadSettings() {
|
||||
}
|
||||
|
||||
$('#summary_source').val(extension_settings.memory.source).trigger('change');
|
||||
$('#memory_long_length').val(extension_settings.memory.longMemoryLength).trigger('input');
|
||||
$('#memory_short_length').val(extension_settings.memory.shortMemoryLength).trigger('input');
|
||||
$('#memory_repetition_penalty').val(extension_settings.memory.repetitionPenalty).trigger('input');
|
||||
$('#memory_temperature').val(extension_settings.memory.temperature).trigger('input');
|
||||
$('#memory_length_penalty').val(extension_settings.memory.lengthPenalty).trigger('input');
|
||||
$('#memory_frozen').prop('checked', extension_settings.memory.memoryFrozen).trigger('input');
|
||||
$('#memory_skipWIAN').prop('checked', extension_settings.memory.SkipWIAN).trigger('input');
|
||||
$('#memory_prompt').val(extension_settings.memory.prompt).trigger('input');
|
||||
@ -109,61 +85,21 @@ function loadSettings() {
|
||||
$('#memory_depth').val(extension_settings.memory.depth).trigger('input');
|
||||
$(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input');
|
||||
$('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
|
||||
switchSourceControls(extension_settings.memory.source);
|
||||
}
|
||||
|
||||
function onSummarySourceChange(event) {
|
||||
const value = event.target.value;
|
||||
extension_settings.memory.source = value;
|
||||
switchSourceControls(value);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function switchSourceControls(value) {
|
||||
$('#memory_settings [data-source]').each((_, element) => {
|
||||
const source = $(element).data('source');
|
||||
$(element).toggle(source === value);
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryShortInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.shortMemoryLength = Number(value);
|
||||
$('#memory_short_length_tokens').text(value);
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Don't let long buffer be bigger than short
|
||||
if (extension_settings.memory.longMemoryLength > extension_settings.memory.shortMemoryLength) {
|
||||
$('#memory_long_length').val(extension_settings.memory.shortMemoryLength).trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
function onMemoryLongInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.longMemoryLength = Number(value);
|
||||
$('#memory_long_length_tokens').text(value);
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Don't let long buffer be bigger than short
|
||||
if (extension_settings.memory.longMemoryLength > extension_settings.memory.shortMemoryLength) {
|
||||
$('#memory_short_length').val(extension_settings.memory.longMemoryLength).trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
function onMemoryRepetitionPenaltyInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.repetitionPenalty = Number(value);
|
||||
$('#memory_repetition_penalty_value').text(extension_settings.memory.repetitionPenalty.toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryTemperatureInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.temperature = Number(value);
|
||||
$('#memory_temperature_value').text(extension_settings.memory.temperature.toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryLengthPenaltyInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.lengthPenalty = Number(value);
|
||||
$('#memory_length_penalty_value').text(extension_settings.memory.lengthPenalty.toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryFrozenInput() {
|
||||
@ -317,6 +253,11 @@ async function onChatEvent() {
|
||||
}
|
||||
|
||||
async function forceSummarizeChat() {
|
||||
if (extension_settings.memory.source === summary_sources.extras) {
|
||||
toastr.warning('Force summarization is not supported for Extras API');
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
|
||||
const skipWIAN = extension_settings.memory.SkipWIAN;
|
||||
@ -434,33 +375,36 @@ async function summarizeChatExtras(context) {
|
||||
const longMemory = getLatestMemoryFromChat(chat);
|
||||
const reversedChat = chat.slice().reverse();
|
||||
reversedChat.shift();
|
||||
let memoryBuffer = [];
|
||||
const memoryBuffer = [];
|
||||
const CONTEXT_SIZE = 1024 - 64;
|
||||
|
||||
for (let mes of reversedChat) {
|
||||
for (const message of reversedChat) {
|
||||
// we reached the point of latest memory
|
||||
if (longMemory && mes.extra && mes.extra.memory == longMemory) {
|
||||
if (longMemory && message.extra && message.extra.memory == longMemory) {
|
||||
break;
|
||||
}
|
||||
|
||||
// don't care about system
|
||||
if (mes.is_system) {
|
||||
if (message.is_system) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// determine the sender's name
|
||||
const name = mes.is_user ? (context.name1 ?? 'You') : (mes.force_avatar ? mes.name : context.name2);
|
||||
const entry = `${name}:\n${mes['mes']}`;
|
||||
const entry = `${message.name}:\n${message.mes}`;
|
||||
memoryBuffer.push(entry);
|
||||
|
||||
// check if token limit was reached
|
||||
if (context.getTokenCount(getMemoryString()) >= extension_settings.memory.shortMemoryLength) {
|
||||
const tokens = getTextTokens(tokenizers.GPT2, getMemoryString()).length;
|
||||
if (tokens >= CONTEXT_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const resultingString = getMemoryString();
|
||||
const resultingTokens = getTextTokens(tokenizers.GPT2, resultingString).length;
|
||||
|
||||
if (context.getTokenCount(resultingString) < extension_settings.memory.shortMemoryLength) {
|
||||
if (!resultingString || resultingTokens < CONTEXT_SIZE) {
|
||||
console.debug('Not enough context to summarize');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -478,13 +422,7 @@ async function summarizeChatExtras(context) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: resultingString,
|
||||
params: {
|
||||
min_length: extension_settings.memory.longMemoryLength * 0, // testing how it behaves 0 min length
|
||||
max_length: extension_settings.memory.longMemoryLength,
|
||||
repetition_penalty: extension_settings.memory.repetitionPenalty,
|
||||
temperature: extension_settings.memory.temperature,
|
||||
length_penalty: extension_settings.memory.lengthPenalty,
|
||||
},
|
||||
params: {},
|
||||
}),
|
||||
});
|
||||
|
||||
@ -589,14 +527,14 @@ function doPopout(e) {
|
||||
loadSettings();
|
||||
loadMovingUIState();
|
||||
|
||||
$('#summaryExtensionPopout').fadeIn(250);
|
||||
$('#summaryExtensionPopout').fadeIn(animation_duration);
|
||||
dragElement(newElement);
|
||||
|
||||
//setup listener for close button to restore extensions menu
|
||||
$('#summaryExtensionPopoutClose').off('click').on('click', function () {
|
||||
$('#summaryExtensionDrawerContents').removeClass('scrollableInnerFull');
|
||||
const summaryPopoutHTML = $('#summaryExtensionDrawerContents');
|
||||
$('#summaryExtensionPopout').fadeOut(250, () => {
|
||||
$('#summaryExtensionPopout').fadeOut(animation_duration, () => {
|
||||
originalElement.empty();
|
||||
originalElement.html(summaryPopoutHTML);
|
||||
$('#summaryExtensionPopout').remove();
|
||||
@ -605,7 +543,7 @@ function doPopout(e) {
|
||||
});
|
||||
} else {
|
||||
console.debug('saw existing popout, removing');
|
||||
$('#summaryExtensionPopout').fadeOut(250, () => { $('#summaryExtensionPopoutClose').trigger('click'); });
|
||||
$('#summaryExtensionPopout').fadeOut(animation_duration, () => { $('#summaryExtensionPopoutClose').trigger('click'); });
|
||||
}
|
||||
}
|
||||
|
||||
@ -613,11 +551,6 @@ function setupListeners() {
|
||||
//setup shared listeners for popout and regular ext menu
|
||||
$('#memory_restore').off('click').on('click', onMemoryRestoreClick);
|
||||
$('#memory_contents').off('click').on('input', onMemoryContentInput);
|
||||
$('#memory_long_length').off('click').on('input', onMemoryLongInput);
|
||||
$('#memory_short_length').off('click').on('input', onMemoryShortInput);
|
||||
$('#memory_repetition_penalty').off('click').on('input', onMemoryRepetitionPenaltyInput);
|
||||
$('#memory_temperature').off('click').on('input', onMemoryTemperatureInput);
|
||||
$('#memory_length_penalty').off('click').on('input', onMemoryLengthPenaltyInput);
|
||||
$('#memory_frozen').off('click').on('input', onMemoryFrozenInput);
|
||||
$('#memory_skipWIAN').off('click').on('input', onMemorySkipWIANInput);
|
||||
$('#summary_source').off('click').on('change', onSummarySourceChange);
|
||||
@ -659,7 +592,7 @@ jQuery(function () {
|
||||
|
||||
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
|
||||
<div class="memory_contents_controls">
|
||||
<div id="memory_force_summarize" class="menu_button menu_button_icon">
|
||||
<div id="memory_force_summarize" data-source="main" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-database"></i>
|
||||
<span>Summarize now</span>
|
||||
</div>
|
||||
@ -710,18 +643,6 @@ jQuery(function () {
|
||||
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" />
|
||||
<small>If both sliders are non-zero, then both will trigger summary updates a their respective intervals.</small>
|
||||
</div>
|
||||
<div data-source="extras">
|
||||
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
|
||||
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" />
|
||||
<label for="memory_long_length">Summary output length (<span id="memory_long_length_tokens"></span> tokens)</label>
|
||||
<input id="memory_long_length" type="range" value="${defaultSettings.longMemoryLength}" min="${defaultSettings.minLongMemory}" max="${defaultSettings.maxLongMemory}" step="${defaultSettings.longMemoryStep}" />
|
||||
<label for="memory_temperature">Temperature (<span id="memory_temperature_value"></span>)</label>
|
||||
<input id="memory_temperature" type="range" value="${defaultSettings.temperature}" min="${defaultSettings.minTemperature}" max="${defaultSettings.maxTemperature}" step="${defaultSettings.temperatureStep}" />
|
||||
<label for="memory_repetition_penalty">Repetition penalty (<span id="memory_repetition_penalty_value"></span>)</label>
|
||||
<input id="memory_repetition_penalty" type="range" value="${defaultSettings.repetitionPenalty}" min="${defaultSettings.minRepetitionPenalty}" max="${defaultSettings.maxRepetitionPenalty}" step="${defaultSettings.repetitionPenaltyStep}" />
|
||||
<label for="memory_length_penalty">Length preference <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
|
||||
<input id="memory_length_penalty" type="range" value="${defaultSettings.lengthPenalty}" min="${defaultSettings.minLengthPenalty}" max="${defaultSettings.maxLengthPenalty}" step="${defaultSettings.lengthPenaltyStep}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types } from '../../../script.js';
|
||||
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types, animation_duration } from '../../../script.js';
|
||||
import { getContext, extension_settings } from '../../extensions.js';
|
||||
import { getSortableDelay, escapeHtml } from '../../utils.js';
|
||||
import { getSortableDelay, escapeHtml, delay } from '../../utils.js';
|
||||
import { executeSlashCommands, registerSlashCommand } from '../../slash-commands.js';
|
||||
import { ContextMenu } from './src/ContextMenu.js';
|
||||
import { MenuItem } from './src/MenuItem.js';
|
||||
@ -26,7 +26,7 @@ const defaultSettings = {
|
||||
|
||||
//method from worldinfo
|
||||
async function updateQuickReplyPresetList() {
|
||||
const result = await fetch('/getsettings', {
|
||||
const result = await fetch('/api/settings/get', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({}),
|
||||
@ -388,7 +388,7 @@ async function doQuickReplyBarPopout() {
|
||||
});
|
||||
|
||||
loadMovingUIState();
|
||||
$('#quickReplyBarPopout').fadeIn(250);
|
||||
$('#quickReplyBarPopout').fadeIn(animation_duration);
|
||||
dragElement(newElement);
|
||||
|
||||
$('#quickReplyBarPopoutClose').off('click').on('click', function () {
|
||||
@ -396,8 +396,8 @@ async function doQuickReplyBarPopout() {
|
||||
let quickRepliesClone = $('#quickReplies').html();
|
||||
$('#quickReplyBar').append(newQuickRepliesDiv);
|
||||
$('#quickReplies').prepend(quickRepliesClone);
|
||||
$('#quickReplyBar').append(popoutButtonClone).fadeIn(250);
|
||||
$('#quickReplyBarPopout').fadeOut(250, () => { $('#quickReplyBarPopout').remove(); });
|
||||
$('#quickReplyBar').append(popoutButtonClone).fadeIn(animation_duration);
|
||||
$('#quickReplyBarPopout').fadeOut(animation_duration, () => { $('#quickReplyBarPopout').remove(); });
|
||||
$('.quickReplyButton').on('click', function () {
|
||||
let index = $(this).data('index');
|
||||
sendQuickReply(index);
|
||||
@ -639,7 +639,7 @@ function generateQuickReplyElements() {
|
||||
<span class="drag-handle ui-sortable-handle">☰</span>
|
||||
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Button label)">
|
||||
<span class="menu_button menu_button_icon" id="quickReply${i}CtxButton" title="Additional options: context menu, auto-execution">⋮</span>
|
||||
<span class="menu_button menu_button_icon editor_maximize fa-solid fa-maximize" data-for="quickReply${i}Mes" id="quickReply${i}ExpandButton" title="Expand the editor"></span>
|
||||
<span class="menu_button menu_button_icon editor_maximize fa-solid fa-maximize" data-tab="true" data-for="quickReply${i}Mes" id="quickReply${i}ExpandButton" title="Expand the editor"></span>
|
||||
<textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1" rows="2"></textarea>
|
||||
</div>
|
||||
`;
|
||||
@ -717,6 +717,222 @@ function saveQROrder() {
|
||||
});
|
||||
}
|
||||
|
||||
async function qrCreateCallback(args, mes) {
|
||||
const qr = {
|
||||
label: args.label ?? '',
|
||||
mes: (mes ?? '')
|
||||
.replace(/\\\|/g, '|')
|
||||
.replace(/\\\{/g, '{')
|
||||
.replace(/\\\}/g, '}')
|
||||
,
|
||||
title: args.title ?? '',
|
||||
autoExecute_chatLoad: JSON.parse(args.load ?? false),
|
||||
autoExecute_userMessage: JSON.parse(args.user ?? false),
|
||||
autoExecute_botMessage: JSON.parse(args.bot ?? false),
|
||||
autoExecute_appStartup: JSON.parse(args.startup ?? false),
|
||||
hidden: JSON.parse(args.hidden ?? false),
|
||||
};
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
preset.quickReplySlots.push(qr);
|
||||
preset.numberOfSlots++;
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
async function qrUpdateCallback(args, mes) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
|
||||
const oqr = preset.quickReplySlots[idx];
|
||||
const qr = {
|
||||
label: args.newlabel ?? oqr.label ?? '',
|
||||
mes: (mes ?? oqr.mes)
|
||||
.replace('\\|', '|')
|
||||
.replace('\\{', '{')
|
||||
.replace('\\}', '}')
|
||||
,
|
||||
title: args.title ?? oqr.title ?? '',
|
||||
autoExecute_chatLoad: JSON.parse(args.load ?? oqr.autoExecute_chatLoad ?? false),
|
||||
autoExecute_userMessage: JSON.parse(args.user ?? oqr.autoExecute_userMessage ?? false),
|
||||
autoExecute_botMessage: JSON.parse(args.bot ?? oqr.autoExecute_botMessage ?? false),
|
||||
autoExecute_appStartup: JSON.parse(args.startup ?? oqr.autoExecute_appStartup ?? false),
|
||||
hidden: JSON.parse(args.hidden ?? oqr.hidden ?? false),
|
||||
};
|
||||
preset.quickReplySlots[idx] = qr;
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
async function qrDeleteCallback(args, label) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == label);
|
||||
if (idx === -1) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR with label '${label}' not found`);
|
||||
return '';
|
||||
};
|
||||
preset.quickReplySlots.splice(idx, 1);
|
||||
preset.numberOfSlots--;
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
|
||||
async function qrContextAddCallback(args, presetName) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
|
||||
const oqr = preset.quickReplySlots[idx];
|
||||
if (!oqr.contextMenu) {
|
||||
oqr.contextMenu = [];
|
||||
}
|
||||
let item = oqr.contextMenu.find(it => it.preset == presetName);
|
||||
if (item) {
|
||||
item.chain = JSON.parse(args.chain ?? 'null') ?? item.chain ?? false;
|
||||
} else {
|
||||
oqr.contextMenu.push({ preset: presetName, chain: JSON.parse(args.chain ?? 'false') });
|
||||
}
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
async function qrContextDeleteCallback(args, presetName) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
|
||||
const oqr = preset.quickReplySlots[idx];
|
||||
if (!oqr.contextMenu) return;
|
||||
const ctxIdx = oqr.contextMenu.findIndex(it => it.preset == presetName);
|
||||
if (ctxIdx > -1) {
|
||||
oqr.contextMenu.splice(ctxIdx, 1);
|
||||
}
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
async function qrContextClearCallback(args, label) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == label);
|
||||
const oqr = preset.quickReplySlots[idx];
|
||||
oqr.contextMenu = [];
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
|
||||
async function qrPresetAddCallback(args, name) {
|
||||
const quickReplyPreset = {
|
||||
name: name,
|
||||
quickReplyEnabled: JSON.parse(args.enabled ?? null) ?? true,
|
||||
quickActionEnabled: JSON.parse(args.nosend ?? null) ?? false,
|
||||
placeBeforeInputEnabled: JSON.parse(args.before ?? null) ?? false,
|
||||
quickReplySlots: [],
|
||||
numberOfSlots: Number(args.slots ?? '0'),
|
||||
AutoInputInject: JSON.parse(args.inject ?? 'false'),
|
||||
};
|
||||
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(quickReplyPreset),
|
||||
});
|
||||
await updateQuickReplyPresetList();
|
||||
}
|
||||
|
||||
async function qrPresetUpdateCallback(args, name) {
|
||||
const preset = presets.find(it => it.name == name);
|
||||
const quickReplyPreset = {
|
||||
name: preset.name,
|
||||
quickReplyEnabled: JSON.parse(args.enabled ?? null) ?? preset.quickReplyEnabled,
|
||||
quickActionEnabled: JSON.parse(args.nosend ?? null) ?? preset.quickActionEnabled,
|
||||
placeBeforeInputEnabled: JSON.parse(args.before ?? null) ?? preset.placeBeforeInputEnabled,
|
||||
quickReplySlots: preset.quickReplySlots,
|
||||
numberOfSlots: Number(args.slots ?? preset.numberOfSlots),
|
||||
AutoInputInject: JSON.parse(args.inject ?? 'null') ?? preset.AutoInputInject,
|
||||
};
|
||||
Object.assign(preset, quickReplyPreset);
|
||||
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(quickReplyPreset),
|
||||
});
|
||||
}
|
||||
|
||||
let onMessageSentExecuting = false;
|
||||
let onMessageReceivedExecuting = false;
|
||||
let onChatChangedExecuting = false;
|
||||
@ -901,4 +1117,33 @@ jQuery(async () => {
|
||||
jQuery(() => {
|
||||
registerSlashCommand('qr', doQR, [], '<span class="monospace">(number)</span> – activates the specified Quick Reply', true, true);
|
||||
registerSlashCommand('qrset', doQRPresetSwitch, [], '<span class="monospace">(name)</span> – swaps to the specified Quick Reply Preset', true, true);
|
||||
const qrArgs = `
|
||||
label - string - text on the button, e.g., label=MyButton
|
||||
set - string - name of the QR set, e.g., set=PresetName1
|
||||
hidden - bool - whether the button should be hidden, e.g., hidden=true
|
||||
startup - bool - auto execute on app startup, e.g., startup=true
|
||||
user - bool - auto execute on user message, e.g., user=true
|
||||
bot - bool - auto execute on AI message, e.g., bot=true
|
||||
load - bool - auto execute on chat load, e.g., load=true
|
||||
title - bool - title / tooltip to be shown on button, e.g., title="My Fancy Button"
|
||||
`.trim();
|
||||
const qrUpdateArgs = `
|
||||
newlabel - string - new text fort the button, e.g. newlabel=MyRenamedButton
|
||||
${qrArgs}
|
||||
`.trim();
|
||||
registerSlashCommand('qr-create', qrCreateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [message])\n arguments:\n ${qrArgs}</span> – creates a new Quick Reply, example: <tt>/qr-create set=MyPreset label=MyButton /echo 123</tt>`, true, true);
|
||||
registerSlashCommand('qr-update', qrUpdateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [message])\n arguments:\n ${qrUpdateArgs}</span> – updates Quick Reply, example: <tt>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</tt>`, true, true);
|
||||
registerSlashCommand('qr-delete', qrDeleteCallback, [], '<span class="monospace">(set=string [label])</span> – deletes Quick Reply', true, true);
|
||||
registerSlashCommand('qr-contextadd', qrContextAddCallback, [], '<span class="monospace">(set=string label=string chain=bool [preset name])</span> – add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, true);
|
||||
registerSlashCommand('qr-contextdel', qrContextDeleteCallback, [], '<span class="monospace">(set=string label=string [preset name])</span> – remove context menu preset from a QR, example: <tt>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</tt>', true, true);
|
||||
registerSlashCommand('qr-contextclear', qrContextClearCallback, [], '<span class="monospace">(set=string [label])</span> – remove all context menu presets from a QR, example: <tt>/qr-contextclear set=MyPreset MyButton</tt>', true, true);
|
||||
const presetArgs = `
|
||||
enabled - bool - enable or disable the preset
|
||||
nosend - bool - disable send / insert in user input (invalid for slash commands)
|
||||
before - bool - place QR before user input
|
||||
slots - int - number of slots
|
||||
inject - bool - inject user input automatically (if disabled use {{input}})
|
||||
`.trim();
|
||||
registerSlashCommand('qr-presetadd', qrPresetAddCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [label])\n arguments:\n ${presetArgs}</span> – create a new preset (overrides existing ones), example: <tt>/qr-presetadd slots=3 MyNewPreset</tt>`, true, true);
|
||||
registerSlashCommand('qr-presetupdate', qrPresetUpdateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [label])\n arguments:\n ${presetArgs}</span> – update an existing preset, example: <tt>/qr-presetupdate enabled=false MyPreset</tt>`, true, true);
|
||||
});
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { getRequestHeaders } from '../../script.js';
|
||||
import { extension_settings } from '../extensions.js';
|
||||
import { oai_settings } from '../openai.js';
|
||||
import { SECRET_KEYS, secret_state } from '../secrets.js';
|
||||
import { createThumbnail } from '../utils.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
|
||||
import { createThumbnail, isValidUrl } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generates a caption for an image using a multimodal model.
|
||||
@ -10,6 +12,99 @@ import { createThumbnail } from '../utils.js';
|
||||
* @returns {Promise<string>} Generated caption
|
||||
*/
|
||||
export async function getMultimodalCaption(base64Img, prompt) {
|
||||
throwIfInvalidModel();
|
||||
|
||||
const noPrefix = ['google', 'ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api);
|
||||
|
||||
if (noPrefix && base64Img.startsWith('data:image/')) {
|
||||
base64Img = base64Img.split(',')[1];
|
||||
}
|
||||
|
||||
// OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.
|
||||
const isGoogle = extension_settings.caption.multimodal_api === 'google';
|
||||
const isOllama = extension_settings.caption.multimodal_api === 'ollama';
|
||||
const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp';
|
||||
const isCustom = extension_settings.caption.multimodal_api === 'custom';
|
||||
const base64Bytes = base64Img.length * 0.75;
|
||||
const compressionLimit = 2 * 1024 * 1024;
|
||||
if (['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) {
|
||||
const maxSide = 1024;
|
||||
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
|
||||
|
||||
if (isGoogle) {
|
||||
base64Img = base64Img.split(',')[1];
|
||||
}
|
||||
}
|
||||
|
||||
const useReverseProxy =
|
||||
extension_settings.caption.multimodal_api === 'openai'
|
||||
&& extension_settings.caption.allow_reverse_proxy
|
||||
&& oai_settings.reverse_proxy
|
||||
&& isValidUrl(oai_settings.reverse_proxy);
|
||||
|
||||
const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';
|
||||
const proxyPassword = useReverseProxy ? oai_settings.proxy_password : '';
|
||||
|
||||
const requestBody = {
|
||||
image: base64Img,
|
||||
prompt: prompt,
|
||||
};
|
||||
|
||||
if (!isGoogle) {
|
||||
requestBody.api = extension_settings.caption.multimodal_api || 'openai';
|
||||
requestBody.model = extension_settings.caption.multimodal_model || 'gpt-4-vision-preview';
|
||||
requestBody.reverse_proxy = proxyUrl;
|
||||
requestBody.proxy_password = proxyPassword;
|
||||
}
|
||||
|
||||
if (isOllama) {
|
||||
if (extension_settings.caption.multimodal_model === 'ollama_current') {
|
||||
requestBody.model = textgenerationwebui_settings.ollama_model;
|
||||
}
|
||||
|
||||
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
|
||||
}
|
||||
|
||||
if (isLlamaCpp) {
|
||||
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
requestBody.server_url = oai_settings.custom_url;
|
||||
requestBody.model = oai_settings.custom_model || 'gpt-4-vision-preview';
|
||||
requestBody.custom_include_headers = oai_settings.custom_include_headers;
|
||||
requestBody.custom_include_body = oai_settings.custom_include_body;
|
||||
requestBody.custom_exclude_body = oai_settings.custom_exclude_body;
|
||||
}
|
||||
|
||||
function getEndpointUrl() {
|
||||
switch (extension_settings.caption.multimodal_api) {
|
||||
case 'google':
|
||||
return '/api/google/caption-image';
|
||||
case 'llamacpp':
|
||||
return '/api/backends/text-completions/llamacpp/caption-image';
|
||||
case 'ollama':
|
||||
return '/api/backends/text-completions/ollama/caption-image';
|
||||
default:
|
||||
return '/api/openai/caption-image';
|
||||
}
|
||||
}
|
||||
|
||||
const apiResult = await fetch(getEndpointUrl(), {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
throw new Error('Failed to caption image via Multimodal API.');
|
||||
}
|
||||
|
||||
const { caption } = await apiResult.json();
|
||||
return String(caption).trim();
|
||||
}
|
||||
|
||||
function throwIfInvalidModel() {
|
||||
if (extension_settings.caption.multimodal_api === 'openai' && !secret_state[SECRET_KEYS.OPENAI]) {
|
||||
throw new Error('OpenAI API key is not set.');
|
||||
}
|
||||
@ -18,29 +113,23 @@ export async function getMultimodalCaption(base64Img, prompt) {
|
||||
throw new Error('OpenRouter API key is not set.');
|
||||
}
|
||||
|
||||
// OpenRouter has a payload limit of ~2MB
|
||||
const base64Bytes = base64Img.length * 0.75;
|
||||
const compressionLimit = 2 * 1024 * 1024;
|
||||
if (extension_settings.caption.multimodal_api === 'openrouter' && base64Bytes > compressionLimit) {
|
||||
const maxSide = 1024;
|
||||
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
|
||||
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE]) {
|
||||
throw new Error('MakerSuite API key is not set.');
|
||||
}
|
||||
|
||||
const apiResult = await fetch('/api/openai/caption-image', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
image: base64Img,
|
||||
prompt: prompt,
|
||||
api: extension_settings.caption.multimodal_api || 'openai',
|
||||
model: extension_settings.caption.multimodal_model || 'gpt-4-vision-preview',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
throw new Error('Failed to caption image via OpenAI.');
|
||||
if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
|
||||
throw new Error('Ollama server URL is not set.');
|
||||
}
|
||||
|
||||
const { caption } = await apiResult.json();
|
||||
return caption;
|
||||
if (extension_settings.caption.multimodal_api === 'ollama' && extension_settings.caption.multimodal_model === 'ollama_current' && !textgenerationwebui_settings.ollama_model) {
|
||||
throw new Error('Ollama model is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
|
||||
throw new Error('LlamaCPP server URL is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) {
|
||||
throw new Error('Custom API URL is not set.');
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,12 @@
|
||||
<a href="javascript:;" class="notes-link"><span class="note-link-span" title="Will generate a new random seed in SillyTavern that is then used in the ComfyUI workflow.">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div>Custom</div>
|
||||
<div class="sd_comfy_workflow_editor_placeholder_actions">
|
||||
<span id="sd_comfy_workflow_editor_placeholder_add" title="Add custom placeholder">+</span>
|
||||
</div>
|
||||
<ul class="sd_comfy_workflow_editor_placeholder_list" id="sd_comfy_workflow_editor_placeholder_list_custom">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
user_avatar,
|
||||
getCharacterAvatar,
|
||||
formatCharacterAvatar,
|
||||
substituteParams,
|
||||
} from '../../../script.js';
|
||||
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from '../../extensions.js';
|
||||
import { selected_group } from '../../group-chats.js';
|
||||
@ -24,6 +25,7 @@ import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.j
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
// Wraps a string into monospace font-face span
|
||||
@ -44,6 +46,7 @@ const sources = {
|
||||
vlad: 'vlad',
|
||||
openai: 'openai',
|
||||
comfy: 'comfy',
|
||||
togetherai: 'togetherai',
|
||||
};
|
||||
|
||||
const generationMode = {
|
||||
@ -348,6 +351,10 @@ async function loadSettings() {
|
||||
extension_settings.sd.character_prompts = {};
|
||||
}
|
||||
|
||||
if (extension_settings.sd.character_negative_prompts === undefined) {
|
||||
extension_settings.sd.character_negative_prompts = {};
|
||||
}
|
||||
|
||||
if (!Array.isArray(extension_settings.sd.styles)) {
|
||||
extension_settings.sd.styles = defaultStyles;
|
||||
}
|
||||
@ -572,6 +579,7 @@ function onChatChanged() {
|
||||
$('#sd_character_prompt_block').show();
|
||||
const key = getCharaFilename(this_chid);
|
||||
$('#sd_character_prompt').val(key ? (extension_settings.sd.character_prompts[key] || '') : '');
|
||||
$('#sd_character_negative_prompt').val(key ? (extension_settings.sd.character_negative_prompts[key] || '') : '');
|
||||
}
|
||||
|
||||
function onCharacterPromptInput() {
|
||||
@ -581,6 +589,13 @@ function onCharacterPromptInput() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onCharacterNegativePromptInput() {
|
||||
const key = getCharaFilename(this_chid);
|
||||
extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val();
|
||||
resetScrollHeight($(this));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function getCharacterPrefix() {
|
||||
if (!this_chid || selected_group) {
|
||||
return '';
|
||||
@ -595,6 +610,20 @@ function getCharacterPrefix() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getCharacterNegativePrefix() {
|
||||
if (!this_chid || selected_group) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const key = getCharaFilename(this_chid);
|
||||
|
||||
if (key) {
|
||||
return extension_settings.sd.character_negative_prompts[key] || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines two prompt prefixes into one.
|
||||
* @param {string} str1 Base string
|
||||
@ -830,6 +859,16 @@ function onComfyWorkflowChange() {
|
||||
extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
async function changeComfyWorkflow(_, name) {
|
||||
name = name.replace(/(\.json)?$/i, '.json');
|
||||
if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) {
|
||||
extension_settings.sd.comfy_workflow = name;
|
||||
$('#sd_comfy_workflow').val(extension_settings.sd.comfy_workflow);
|
||||
saveSettingsDebounced();
|
||||
} else {
|
||||
toastr.error(`ComfyUI Workflow "${name}" does not exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateAutoUrl() {
|
||||
try {
|
||||
@ -905,7 +944,7 @@ async function onModelChange() {
|
||||
extension_settings.sd.model = $('#sd_model').find(':selected').val();
|
||||
saveSettingsDebounced();
|
||||
|
||||
const cloudSources = [sources.horde, sources.novel, sources.openai];
|
||||
const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai];
|
||||
|
||||
if (cloudSources.includes(extension_settings.sd.source)) {
|
||||
return;
|
||||
@ -1038,11 +1077,14 @@ async function loadSamplers() {
|
||||
samplers = await loadVladSamplers();
|
||||
break;
|
||||
case sources.openai:
|
||||
samplers = await loadOpenAiSamplers();
|
||||
samplers = ['N/A'];
|
||||
break;
|
||||
case sources.comfy:
|
||||
samplers = await loadComfySamplers();
|
||||
break;
|
||||
case sources.togetherai:
|
||||
samplers = ['N/A'];
|
||||
break;
|
||||
}
|
||||
|
||||
for (const sampler of samplers) {
|
||||
@ -1052,6 +1094,11 @@ async function loadSamplers() {
|
||||
option.selected = sampler === extension_settings.sd.sampler;
|
||||
$('#sd_sampler').append(option);
|
||||
}
|
||||
|
||||
if (!extension_settings.sd.sampler && samplers.length > 0) {
|
||||
extension_settings.sd.sampler = samplers[0];
|
||||
$('#sd_sampler').val(extension_settings.sd.sampler).trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHordeSamplers() {
|
||||
@ -1108,10 +1155,6 @@ async function loadAutoSamplers() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOpenAiSamplers() {
|
||||
return ['N/A'];
|
||||
}
|
||||
|
||||
async function loadVladSamplers() {
|
||||
if (!extension_settings.sd.vlad_url) {
|
||||
return [];
|
||||
@ -1200,6 +1243,9 @@ async function loadModels() {
|
||||
case sources.comfy:
|
||||
models = await loadComfyModels();
|
||||
break;
|
||||
case sources.togetherai:
|
||||
models = await loadTogetherAIModels();
|
||||
break;
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
@ -1209,6 +1255,30 @@ async function loadModels() {
|
||||
option.selected = model.value === extension_settings.sd.model;
|
||||
$('#sd_model').append(option);
|
||||
}
|
||||
|
||||
if (!extension_settings.sd.model && models.length > 0) {
|
||||
extension_settings.sd.model = models[0].value;
|
||||
$('#sd_model').val(extension_settings.sd.model).trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTogetherAIModels() {
|
||||
if (!secret_state[SECRET_KEYS.TOGETHERAI]) {
|
||||
console.debug('TogetherAI API key is not set.');
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await fetch('/api/sd/together/models', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const data = await result.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function loadHordeModels() {
|
||||
@ -1422,6 +1492,9 @@ async function loadSchedulers() {
|
||||
case sources.openai:
|
||||
schedulers = ['N/A'];
|
||||
break;
|
||||
case sources.togetherai:
|
||||
schedulers = ['N/A'];
|
||||
break;
|
||||
case sources.comfy:
|
||||
schedulers = await loadComfySchedulers();
|
||||
break;
|
||||
@ -1481,6 +1554,9 @@ async function loadVaes() {
|
||||
case sources.openai:
|
||||
vaes = ['N/A'];
|
||||
break;
|
||||
case sources.togetherai:
|
||||
vaes = ['N/A'];
|
||||
break;
|
||||
case sources.comfy:
|
||||
vaes = await loadComfyVaes();
|
||||
break;
|
||||
@ -1711,7 +1787,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
|
||||
prompt = message || getRawLastMessage();
|
||||
break;
|
||||
case generationMode.FREE:
|
||||
prompt = trigger.trim();
|
||||
prompt = generateFreeModePrompt(trigger.trim());
|
||||
break;
|
||||
case generationMode.FACE_MULTIMODAL:
|
||||
case generationMode.CHARACTER_MULTIMODAL:
|
||||
@ -1730,6 +1806,36 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a free prompt with a character-specific prompt prefix support.
|
||||
* @param {string} trigger - The prompt to use for the image generation.
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateFreeModePrompt(trigger) {
|
||||
return trigger
|
||||
.replace(/(?:^char(\s|,)|\{\{charPrefix\}\})/gi, (_, suffix) => {
|
||||
const getLastCharacterKey = () => {
|
||||
if (typeof this_chid !== 'undefined') {
|
||||
return getCharaFilename(this_chid);
|
||||
}
|
||||
const context = getContext();
|
||||
for (let i = context.chat.length - 1; i >= 0; i--) {
|
||||
const message = context.chat[i];
|
||||
if (message.is_user || message.is_system) {
|
||||
continue;
|
||||
} else if (typeof message.original_avatar === 'string') {
|
||||
return message.original_avatar.replace(/\.[^/.]+$/, '');
|
||||
}
|
||||
}
|
||||
throw new Error('No usable messages found.');
|
||||
};
|
||||
|
||||
const key = getLastCharacterKey();
|
||||
const value = (extension_settings.sd.character_prompts[key] || '').trim();
|
||||
return value ? value + (suffix || '') : '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a prompt using multimodal captioning.
|
||||
* @param {number} generationType - The type of image generation to perform.
|
||||
@ -1756,22 +1862,28 @@ async function generateMultimodalPrompt(generationType, quietPrompt) {
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(avatarUrl);
|
||||
try {
|
||||
const response = await fetch(avatarUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Could not fetch avatar image.');
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error('Could not fetch avatar image.');
|
||||
}
|
||||
|
||||
const avatarBlob = await response.blob();
|
||||
const avatarBase64 = await getBase64Async(avatarBlob);
|
||||
const avatarBlob = await response.blob();
|
||||
const avatarBase64 = await getBase64Async(avatarBlob);
|
||||
|
||||
const caption = await getMultimodalCaption(avatarBase64, quietPrompt);
|
||||
const caption = await getMultimodalCaption(avatarBase64, quietPrompt);
|
||||
|
||||
if (!caption) {
|
||||
if (!caption) {
|
||||
throw new Error('No caption returned from the API.');
|
||||
}
|
||||
|
||||
return caption;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toastr.error('Multimodal captioning failed. Please try again.', 'Image Generation');
|
||||
throw new Error('Multimodal captioning failed.');
|
||||
}
|
||||
|
||||
return caption;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1781,7 +1893,14 @@ async function generateMultimodalPrompt(generationType, quietPrompt) {
|
||||
*/
|
||||
async function generatePrompt(quietPrompt) {
|
||||
const reply = await generateQuietPrompt(quietPrompt, false, false);
|
||||
return processReply(reply);
|
||||
const processedReply = processReply(reply);
|
||||
|
||||
if (!processedReply) {
|
||||
toastr.error('Prompt generation produced no text. Make sure you\'re using a valid instruct template and try again', 'Image Generation');
|
||||
throw new Error('Prompt generation failed.');
|
||||
}
|
||||
|
||||
return processedReply;
|
||||
}
|
||||
|
||||
async function sendGenerationRequest(generationType, prompt, characterName = null, callback) {
|
||||
@ -1792,31 +1911,38 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
|
||||
|
||||
const prefixedPrompt = combinePrefixes(prefix, prompt, '{prompt}');
|
||||
|
||||
const negativePrompt = noCharPrefix.includes(generationType)
|
||||
? extension_settings.sd.negative_prompt
|
||||
: combinePrefixes(extension_settings.sd.negative_prompt, getCharacterNegativePrefix());
|
||||
|
||||
let result = { format: '', data: '' };
|
||||
const currentChatId = getCurrentChatId();
|
||||
|
||||
try {
|
||||
switch (extension_settings.sd.source) {
|
||||
case sources.extras:
|
||||
result = await generateExtrasImage(prefixedPrompt);
|
||||
result = await generateExtrasImage(prefixedPrompt, negativePrompt);
|
||||
break;
|
||||
case sources.horde:
|
||||
result = await generateHordeImage(prefixedPrompt);
|
||||
result = await generateHordeImage(prefixedPrompt, negativePrompt);
|
||||
break;
|
||||
case sources.vlad:
|
||||
result = await generateAutoImage(prefixedPrompt);
|
||||
result = await generateAutoImage(prefixedPrompt, negativePrompt);
|
||||
break;
|
||||
case sources.auto:
|
||||
result = await generateAutoImage(prefixedPrompt);
|
||||
result = await generateAutoImage(prefixedPrompt, negativePrompt);
|
||||
break;
|
||||
case sources.novel:
|
||||
result = await generateNovelImage(prefixedPrompt);
|
||||
result = await generateNovelImage(prefixedPrompt, negativePrompt);
|
||||
break;
|
||||
case sources.openai:
|
||||
result = await generateOpenAiImage(prefixedPrompt);
|
||||
break;
|
||||
case sources.comfy:
|
||||
result = await generateComfyImage(prefixedPrompt);
|
||||
result = await generateComfyImage(prefixedPrompt, negativePrompt);
|
||||
break;
|
||||
case sources.togetherai:
|
||||
result = await generateTogetherAIImage(prefixedPrompt, negativePrompt);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1840,13 +1966,37 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
|
||||
callback ? callback(prompt, base64Image, generationType) : sendMessage(prompt, base64Image, generationType);
|
||||
}
|
||||
|
||||
async function generateTogetherAIImage(prompt, negativePrompt) {
|
||||
const result = await fetch('/api/sd/together/generate', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
model: extension_settings.sd.model,
|
||||
steps: extension_settings.sd.steps,
|
||||
width: extension_settings.sd.width,
|
||||
height: extension_settings.sd.height,
|
||||
}),
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const data = await result.json();
|
||||
return { format: 'jpg', data: data?.output?.choices?.[0]?.image_base64 };
|
||||
} else {
|
||||
const text = await result.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an "extras" image using a provided prompt and other settings.
|
||||
*
|
||||
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||
* @param {string} negativePrompt - The instruction used to restrict the image generation.
|
||||
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
|
||||
*/
|
||||
async function generateExtrasImage(prompt) {
|
||||
async function generateExtrasImage(prompt, negativePrompt) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/image';
|
||||
const result = await doExtrasFetch(url, {
|
||||
@ -1861,7 +2011,7 @@ async function generateExtrasImage(prompt) {
|
||||
scale: extension_settings.sd.scale,
|
||||
width: extension_settings.sd.width,
|
||||
height: extension_settings.sd.height,
|
||||
negative_prompt: extension_settings.sd.negative_prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
restore_faces: !!extension_settings.sd.restore_faces,
|
||||
enable_hr: !!extension_settings.sd.enable_hr,
|
||||
karras: !!extension_settings.sd.horde_karras,
|
||||
@ -1885,9 +2035,10 @@ async function generateExtrasImage(prompt) {
|
||||
* Generates a "horde" image using the provided prompt and configuration settings.
|
||||
*
|
||||
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||
* @param {string} negativePrompt - The instruction used to restrict the image generation.
|
||||
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
|
||||
*/
|
||||
async function generateHordeImage(prompt) {
|
||||
async function generateHordeImage(prompt, negativePrompt) {
|
||||
const result = await fetch('/api/horde/generate-image', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
@ -1898,7 +2049,7 @@ async function generateHordeImage(prompt) {
|
||||
scale: extension_settings.sd.scale,
|
||||
width: extension_settings.sd.width,
|
||||
height: extension_settings.sd.height,
|
||||
negative_prompt: extension_settings.sd.negative_prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
model: extension_settings.sd.model,
|
||||
nsfw: extension_settings.sd.horde_nsfw,
|
||||
restore_faces: !!extension_settings.sd.restore_faces,
|
||||
@ -1920,16 +2071,17 @@ async function generateHordeImage(prompt) {
|
||||
* Generates an image in SD WebUI API using the provided prompt and configuration settings.
|
||||
*
|
||||
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||
* @param {string} negativePrompt - The instruction used to restrict the image generation.
|
||||
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
|
||||
*/
|
||||
async function generateAutoImage(prompt) {
|
||||
async function generateAutoImage(prompt, negativePrompt) {
|
||||
const result = await fetch('/api/sd/generate', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
...getSdRequestBody(),
|
||||
prompt: prompt,
|
||||
negative_prompt: extension_settings.sd.negative_prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
sampler_name: extension_settings.sd.sampler,
|
||||
steps: extension_settings.sd.steps,
|
||||
cfg_scale: extension_settings.sd.scale,
|
||||
@ -1962,9 +2114,10 @@ async function generateAutoImage(prompt) {
|
||||
* Generates an image in NovelAI API using the provided prompt and configuration settings.
|
||||
*
|
||||
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||
* @param {string} negativePrompt - The instruction used to restrict the image generation.
|
||||
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
|
||||
*/
|
||||
async function generateNovelImage(prompt) {
|
||||
async function generateNovelImage(prompt, negativePrompt) {
|
||||
const { steps, width, height } = getNovelParams();
|
||||
|
||||
const result = await fetch('/api/novelai/generate-image', {
|
||||
@ -1978,7 +2131,7 @@ async function generateNovelImage(prompt) {
|
||||
scale: extension_settings.sd.scale,
|
||||
width: width,
|
||||
height: height,
|
||||
negative_prompt: extension_settings.sd.negative_prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
upscale_ratio: extension_settings.sd.novel_upscale_ratio,
|
||||
}),
|
||||
});
|
||||
@ -2106,11 +2259,11 @@ async function generateOpenAiImage(prompt) {
|
||||
* Generates an image in ComfyUI using the provided prompt and configuration settings.
|
||||
*
|
||||
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||
* @param {string} negativePrompt - The instruction used to restrict the image generation.
|
||||
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
|
||||
*/
|
||||
async function generateComfyImage(prompt) {
|
||||
async function generateComfyImage(prompt, negativePrompt) {
|
||||
const placeholders = [
|
||||
'negative_prompt',
|
||||
'model',
|
||||
'vae',
|
||||
'sampler',
|
||||
@ -2133,10 +2286,14 @@ async function generateComfyImage(prompt) {
|
||||
toastr.error(`Failed to load workflow.\n\n${text}`);
|
||||
}
|
||||
let workflow = (await workflowResponse.json()).replace('"%prompt%"', JSON.stringify(prompt));
|
||||
workflow = (await workflowResponse.json()).replace('"%negative_prompt%"', JSON.stringify(negativePrompt));
|
||||
workflow = workflow.replace('"%seed%"', JSON.stringify(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)));
|
||||
placeholders.forEach(ph => {
|
||||
workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
|
||||
});
|
||||
(extension_settings.sd.comfy_placeholders ?? []).forEach(ph => {
|
||||
workflow = workflow.replace(`"%${ph.find}%"`, JSON.stringify(substituteParams(ph.replace)));
|
||||
});
|
||||
console.log(`{
|
||||
"prompt": ${workflow}
|
||||
}`);
|
||||
@ -2173,6 +2330,50 @@ async function onComfyOpenWorkflowEditorClick() {
|
||||
};
|
||||
$('#sd_comfy_workflow_editor_name').text(extension_settings.sd.comfy_workflow);
|
||||
$('#sd_comfy_workflow_editor_workflow').val(workflow);
|
||||
const addPlaceholderDom = (placeholder) => {
|
||||
const el = $(`
|
||||
<li class="sd_comfy_workflow_editor_not_found" data-placeholder="${placeholder.find}">
|
||||
<span class="sd_comfy_workflow_editor_custom_remove" title="Remove custom placeholder">⊘</span>
|
||||
<span class="sd_comfy_workflow_editor_custom_final">"%${placeholder.find}%"</span><br>
|
||||
<input placeholder="find" title="find" type="text" class="text_pole sd_comfy_workflow_editor_custom_find" value=""><br>
|
||||
<input placeholder="replace" title="replace" type="text" class="text_pole sd_comfy_workflow_editor_custom_replace">
|
||||
</li>
|
||||
`);
|
||||
$('#sd_comfy_workflow_editor_placeholder_list_custom').append(el);
|
||||
el.find('.sd_comfy_workflow_editor_custom_find').val(placeholder.find);
|
||||
el.find('.sd_comfy_workflow_editor_custom_find').on('input', function() {
|
||||
placeholder.find = this.value;
|
||||
el.find('.sd_comfy_workflow_editor_custom_final').text(`"%${this.value}%"`);
|
||||
el.attr('data-placeholder', `${this.value}`);
|
||||
checkPlaceholders();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
el.find('.sd_comfy_workflow_editor_custom_replace').val(placeholder.replace);
|
||||
el.find('.sd_comfy_workflow_editor_custom_replace').on('input', function() {
|
||||
placeholder.replace = this.value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
el.find('.sd_comfy_workflow_editor_custom_remove').on('click', () => {
|
||||
el.remove();
|
||||
extension_settings.sd.comfy_placeholders.splice(extension_settings.sd.comfy_placeholders.indexOf(placeholder));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
};
|
||||
$('#sd_comfy_workflow_editor_placeholder_add').on('click', () => {
|
||||
if (!extension_settings.sd.comfy_placeholders) {
|
||||
extension_settings.sd.comfy_placeholders = [];
|
||||
}
|
||||
const placeholder = {
|
||||
find: '',
|
||||
replace: '',
|
||||
};
|
||||
extension_settings.sd.comfy_placeholders.push(placeholder);
|
||||
addPlaceholderDom(placeholder);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
(extension_settings.sd.comfy_placeholders ?? []).forEach(placeholder=>{
|
||||
addPlaceholderDom(placeholder);
|
||||
});
|
||||
checkPlaceholders();
|
||||
$('#sd_comfy_workflow_editor_workflow').on('input', checkPlaceholders);
|
||||
if (await popupResult) {
|
||||
@ -2333,6 +2534,8 @@ function isValidState() {
|
||||
return secret_state[SECRET_KEYS.OPENAI];
|
||||
case sources.comfy:
|
||||
return true;
|
||||
case sources.togetherai:
|
||||
return secret_state[SECRET_KEYS.TOGETHERAI];
|
||||
}
|
||||
}
|
||||
|
||||
@ -2438,7 +2641,8 @@ $('#sd_dropdown [id]').on('click', function () {
|
||||
});
|
||||
|
||||
jQuery(async () => {
|
||||
getContext().registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
|
||||
registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
|
||||
registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>')
|
||||
|
||||
$('#extensions_settings').append(renderExtensionTemplate('stable-diffusion', 'settings', defaultSettings));
|
||||
$('#sd_source').on('change', onSourceChange);
|
||||
@ -2460,6 +2664,7 @@ jQuery(async () => {
|
||||
$('#sd_enable_hr').on('input', onHighResFixInput);
|
||||
$('#sd_refine_mode').on('input', onRefineModeInput);
|
||||
$('#sd_character_prompt').on('input', onCharacterPromptInput);
|
||||
$('#sd_character_negative_prompt').on('input', onCharacterNegativePromptInput);
|
||||
$('#sd_auto_validate').on('click', validateAutoUrl);
|
||||
$('#sd_auto_url').on('input', onAutoUrlInput);
|
||||
$('#sd_auto_auth').on('input', onAutoAuthInput);
|
||||
@ -2492,6 +2697,7 @@ jQuery(async () => {
|
||||
initScrollHeight($('#sd_prompt_prefix'));
|
||||
initScrollHeight($('#sd_negative_prompt'));
|
||||
initScrollHeight($('#sd_character_prompt'));
|
||||
initScrollHeight($('#sd_character_negative_prompt'));
|
||||
});
|
||||
|
||||
for (const [key, value] of Object.entries(resolutionOptions)) {
|
||||
|
@ -35,6 +35,7 @@
|
||||
<option value="novel">NovelAI Diffusion</option>
|
||||
<option value="openai">OpenAI (DALL-E)</option>
|
||||
<option value="comfy">ComfyUI</option>
|
||||
<option value="togetherai">TogetherAI</option>
|
||||
</select>
|
||||
<div data-sd-source="auto">
|
||||
<label for="sd_auto_url">SD Web UI URL</label>
|
||||
@ -207,6 +208,9 @@
|
||||
<label for="sd_character_prompt">Character-specific prompt prefix</label>
|
||||
<small>Won't be used in groups.</small>
|
||||
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix. Example: female, green eyes, brown hair, pink shirt"></textarea>
|
||||
<label for="sd_character_negative_prompt">Character-specific negative prompt prefix</label>
|
||||
<small>Won't be used in groups.</small>
|
||||
<textarea id="sd_character_negative_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prefix. Example: jewellery, shoes, glasses"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -82,3 +82,17 @@
|
||||
.sd_comfy_workflow_editor_placeholder_list>li>.notes-link {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.sd_comfy_workflow_editor_placeholder_list input {
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
.sd_comfy_workflow_editor_custom_remove, #sd_comfy_workflow_editor_placeholder_add {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
width: 1em;
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from '../../../script.js';
|
||||
import { extension_settings, getContext } from '../../extensions.js';
|
||||
import { secret_state, writeSecret } from '../../secrets.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
|
||||
export const autoModeOptions = {
|
||||
NONE: 'none',
|
||||
@ -315,6 +316,28 @@ async function translateProviderBing(text, lang) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits text into chunks and translates each chunk separately
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @param {(text: string, lang: string) => Promise<string>} translateFn Function to translate a single chunk (must return a Promise)
|
||||
* @param {number} chunkSize Maximum chunk size
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function chunkedTranslate(text, lang, translateFn, chunkSize = 5000) {
|
||||
if (text.length <= chunkSize) {
|
||||
return await translateFn(text, lang);
|
||||
}
|
||||
|
||||
const chunks = splitRecursive(text, chunkSize);
|
||||
|
||||
let result = '';
|
||||
for (const chunk of chunks) {
|
||||
result += await translateFn(chunk, lang);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the selected translation provider
|
||||
* @param {string} text Text to translate
|
||||
@ -331,15 +354,15 @@ async function translate(text, lang) {
|
||||
case 'libre':
|
||||
return await translateProviderLibre(text, lang);
|
||||
case 'google':
|
||||
return await translateProviderGoogle(text, lang);
|
||||
return await chunkedTranslate(text, lang, translateProviderGoogle, 5000);
|
||||
case 'deepl':
|
||||
return await translateProviderDeepl(text, lang);
|
||||
case 'deeplx':
|
||||
return await translateProviderDeepLX(text, lang);
|
||||
return await chunkedTranslate(text, lang, translateProviderDeepLX, 1500);
|
||||
case 'oneringtranslator':
|
||||
return await translateProviderOneRing(text, lang);
|
||||
case 'bing':
|
||||
return await translateProviderBing(text, lang);
|
||||
return await chunkedTranslate(text, lang, translateProviderBing, 1000);
|
||||
default:
|
||||
console.error('Unknown translation provider', extension_settings.translate.provider);
|
||||
return text;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js';
|
||||
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js';
|
||||
import { escapeRegex, getStringHash } from '../../utils.js';
|
||||
import { delay, escapeRegex, getStringHash } from '../../utils.js';
|
||||
import { EdgeTtsProvider } from './edge.js';
|
||||
import { ElevenLabsTtsProvider } from './elevenlabs.js';
|
||||
import { SileroTtsProvider } from './silerotts.js';
|
||||
@ -482,6 +482,12 @@ async function processTtsQueue() {
|
||||
console.debug('New message found, running TTS');
|
||||
currentTtsJob = ttsJobQueue.shift();
|
||||
let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes;
|
||||
|
||||
if (extension_settings.tts.skip_codeblocks) {
|
||||
text = text.replace(/^\s{4}.*$/gm, '').trim();
|
||||
text = text.replace(/```.*?```/gs, '').trim();
|
||||
}
|
||||
|
||||
text = extension_settings.tts.narrate_dialogues_only
|
||||
? text.replace(/\*[^*]*?(\*|$)/g, '').trim() // remove asterisks content
|
||||
: text.replaceAll('*', '').trim(); // remove just the asterisks
|
||||
@ -639,6 +645,11 @@ function onNarrateTranslatedOnlyClick() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onSkipCodeblocksClick() {
|
||||
extension_settings.tts.skip_codeblocks = !!$('#tts_skip_codeblocks').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
//##############//
|
||||
// TTS Provider //
|
||||
//##############//
|
||||
@ -687,7 +698,8 @@ export function saveTtsProviderSettings() {
|
||||
|
||||
async function onChatChanged() {
|
||||
await resetTtsPlayback();
|
||||
await initVoiceMap();
|
||||
const voiceMapInit = initVoiceMap();
|
||||
await Promise.race([voiceMapInit, delay(1000)]);
|
||||
ttsLastMessage = null;
|
||||
}
|
||||
|
||||
@ -952,6 +964,10 @@ $(document).ready(function () {
|
||||
<input type="checkbox" id="tts_narrate_translated_only">
|
||||
<small>Narrate only the translated text</small>
|
||||
</label>
|
||||
<label class="checkbox_label" for="tts_skip_codeblocks">
|
||||
<input type="checkbox" id="tts_skip_codeblocks">
|
||||
<small>Skip codeblocks</small>
|
||||
</label>
|
||||
</div>
|
||||
<div id="tts_voicemap_block">
|
||||
</div>
|
||||
@ -972,6 +988,7 @@ $(document).ready(function () {
|
||||
$('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick);
|
||||
$('#tts_narrate_quoted').on('click', onNarrateQuotedClick);
|
||||
$('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick);
|
||||
$('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick);
|
||||
$('#tts_auto_generation').on('click', onAutoGenerationClick);
|
||||
$('#tts_narrate_user').on('click', onNarrateUserClick);
|
||||
$('#tts_voices').on('click', onTtsVoicesClick);
|
||||
|
@ -44,7 +44,7 @@ class OpenAITtsProvider {
|
||||
</div>
|
||||
<div>
|
||||
<label for="openai-tts-speed">Speed: <span id="openai-tts-speed-output"></span></label>
|
||||
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.25">
|
||||
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.05">
|
||||
</div>`;
|
||||
return html;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export const EXTENSION_PROMPT_TAG = '3_vectors';
|
||||
const settings = {
|
||||
// For both
|
||||
source: 'transformers',
|
||||
include_wi: false,
|
||||
|
||||
// For chats
|
||||
enabled_chats: false,
|
||||
@ -254,7 +255,7 @@ 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);
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, '', extension_prompt_types.IN_PROMPT, 0, settings.include_wi);
|
||||
|
||||
if (settings.enabled_files) {
|
||||
await processFiles(chat);
|
||||
@ -319,7 +320,7 @@ async function rearrangeChat(chat) {
|
||||
|
||||
// Format queried messages into a single string
|
||||
const insertedText = getPromptText(queriedMessages);
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, insertedText, settings.position, settings.depth);
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, insertedText, settings.position, settings.depth, settings.include_wi);
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to rearrange chat', error);
|
||||
}
|
||||
@ -393,7 +394,8 @@ async function getSavedHashes(collectionId) {
|
||||
*/
|
||||
async function insertVectorItems(collectionId, items) {
|
||||
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
|
||||
settings.source === 'palm' && !secret_state[SECRET_KEYS.PALM]) {
|
||||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
|
||||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI]) {
|
||||
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
|
||||
}
|
||||
|
||||
@ -574,6 +576,12 @@ jQuery(async () => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_include_wi').prop('checked', settings.include_wi).on('input', () => {
|
||||
settings.include_wi = !!$('#vectors_include_wi').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
toggleSettings();
|
||||
eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);
|
||||
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);
|
||||
|
@ -13,6 +13,7 @@
|
||||
<option value="transformers">Local (Transformers)</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="palm">Google MakerSuite (PaLM)</option>
|
||||
<option value="mistral">MistralAI</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -23,6 +24,11 @@
|
||||
<input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" />
|
||||
</div>
|
||||
|
||||
<label class="checkbox_label" for="vectors_include_wi" title="Query results can activate World Info entries.">
|
||||
<input id="vectors_include_wi" type="checkbox" class="checkbox">
|
||||
Include in World Info Scanning
|
||||
</label>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
extractAllWords,
|
||||
saveBase64AsFile,
|
||||
PAGINATION_TEMPLATE,
|
||||
waitUntilCondition,
|
||||
getBase64Async,
|
||||
} from './utils.js';
|
||||
import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';
|
||||
@ -46,7 +45,6 @@ import {
|
||||
updateChatMetadata,
|
||||
isStreamingEnabled,
|
||||
getThumbnailUrl,
|
||||
streamingProcessor,
|
||||
getRequestHeaders,
|
||||
setMenuType,
|
||||
menu_type,
|
||||
@ -69,6 +67,7 @@ import {
|
||||
baseChatReplace,
|
||||
depth_prompt_depth_default,
|
||||
loadItemizedPrompts,
|
||||
animation_duration,
|
||||
} from '../script.js';
|
||||
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||
@ -111,10 +110,18 @@ export const group_generation_mode = {
|
||||
APPEND: 1,
|
||||
};
|
||||
|
||||
const DEFAULT_AUTO_MODE_DELAY = 5;
|
||||
|
||||
export const groupCandidatesFilter = new FilterHelper(debounce(printGroupCandidates, 100));
|
||||
setInterval(groupChatAutoModeWorker, 5000);
|
||||
let autoModeWorker = null;
|
||||
const saveGroupDebounced = debounce(async (group, reload) => await _save(group, reload), 500);
|
||||
|
||||
function setAutoModeWorker() {
|
||||
clearInterval(autoModeWorker);
|
||||
const autoModeDelay = groups.find(x => x.id === selected_group)?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY;
|
||||
autoModeWorker = setInterval(groupChatAutoModeWorker, autoModeDelay * 1000);
|
||||
}
|
||||
|
||||
async function _save(group, reload = true) {
|
||||
await fetch('/api/groups/edit', {
|
||||
method: 'POST',
|
||||
@ -611,14 +618,20 @@ function getGroupChatNames(groupId) {
|
||||
}
|
||||
|
||||
async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
function throwIfAborted() {
|
||||
if (params.signal instanceof AbortSignal && params.signal.aborted) {
|
||||
throw new Error('AbortSignal was fired. Group generation stopped');
|
||||
}
|
||||
}
|
||||
|
||||
if (online_status === 'no_connection') {
|
||||
is_group_generating = false;
|
||||
setSendButtonState(false);
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (is_group_generating) {
|
||||
return false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Auto-navigate back to group menu
|
||||
@ -629,13 +642,15 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
|
||||
const group = groups.find((x) => x.id === selected_group);
|
||||
let typingIndicator = $('#chat .typing_indicator');
|
||||
let textResult = '';
|
||||
|
||||
if (!group || !Array.isArray(group.members) || !group.members.length) {
|
||||
sendSystemMessage(system_message_types.EMPTY, '', { isSmallSys: true });
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
throwIfAborted();
|
||||
hideSwipeButtons();
|
||||
is_group_generating = true;
|
||||
setCharacterName('');
|
||||
@ -653,50 +668,18 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
// id of this specific batch for regeneration purposes
|
||||
group_generation_id = Date.now();
|
||||
const lastMessage = chat[chat.length - 1];
|
||||
let messagesBefore = chat.length;
|
||||
let lastMessageText = lastMessage?.mes || '';
|
||||
let activationText = '';
|
||||
let isUserInput = false;
|
||||
let isGenerationDone = false;
|
||||
let isGenerationAborted = false;
|
||||
|
||||
if (userInput?.length && !by_auto_mode) {
|
||||
isUserInput = true;
|
||||
activationText = userInput;
|
||||
messagesBefore++;
|
||||
} else {
|
||||
if (lastMessage && !lastMessage.is_system) {
|
||||
activationText = lastMessage.mes;
|
||||
}
|
||||
}
|
||||
|
||||
const resolveOriginal = params.resolve;
|
||||
const rejectOriginal = params.reject;
|
||||
|
||||
if (params.signal instanceof AbortSignal) {
|
||||
if (params.signal.aborted) {
|
||||
throw new Error('Already aborted signal passed. Group generation stopped');
|
||||
}
|
||||
|
||||
params.signal.onabort = () => {
|
||||
isGenerationAborted = true;
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof params.resolve === 'function') {
|
||||
params.resolve = function () {
|
||||
isGenerationDone = true;
|
||||
resolveOriginal.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof params.reject === 'function') {
|
||||
params.reject = function () {
|
||||
isGenerationDone = true;
|
||||
rejectOriginal.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
const activationStrategy = Number(group.activation_strategy ?? group_activation_strategy.NATURAL);
|
||||
const enabledMembers = group.members.filter(x => !group.disabled_members.includes(x));
|
||||
let activatedMembers = [];
|
||||
@ -741,14 +724,12 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
|
||||
// now the real generation begins: cycle through every activated character
|
||||
for (const chId of activatedMembers) {
|
||||
throwIfAborted();
|
||||
deactivateSendButtons();
|
||||
isGenerationDone = false;
|
||||
const generateType = type == 'swipe' || type == 'impersonate' || type == 'quiet' || type == 'continue' ? type : 'group_chat';
|
||||
setCharacterId(chId);
|
||||
setCharacterName(characters[chId].name);
|
||||
|
||||
await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
|
||||
|
||||
if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
|
||||
// update indicator and scroll down
|
||||
typingIndicator
|
||||
@ -757,75 +738,9 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
typingIndicator.show();
|
||||
}
|
||||
|
||||
// TODO: This is awful. Refactor this
|
||||
while (true) {
|
||||
deactivateSendButtons();
|
||||
if (isGenerationAborted) {
|
||||
throw new Error('Group generation aborted');
|
||||
}
|
||||
|
||||
// if not swipe - check if message generated already
|
||||
if (generateType === 'group_chat' && chat.length == messagesBefore) {
|
||||
await delay(100);
|
||||
}
|
||||
// if swipe - see if message changed
|
||||
else if (type === 'swipe') {
|
||||
if (isStreamingEnabled()) {
|
||||
if (streamingProcessor && !streamingProcessor.isFinished) {
|
||||
await delay(100);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (lastMessageText === chat[chat.length - 1].mes) {
|
||||
await delay(100);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type === 'impersonate') {
|
||||
if (isStreamingEnabled()) {
|
||||
if (streamingProcessor && !streamingProcessor.isFinished) {
|
||||
await delay(100);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!$('#send_textarea').val() || $('#send_textarea').val() == userInput) {
|
||||
await delay(100);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type === 'quiet') {
|
||||
if (isGenerationDone) {
|
||||
break;
|
||||
} else {
|
||||
await delay(100);
|
||||
}
|
||||
}
|
||||
else if (isStreamingEnabled()) {
|
||||
if (streamingProcessor && !streamingProcessor.isFinished) {
|
||||
await delay(100);
|
||||
} else {
|
||||
await waitUntilCondition(() => streamingProcessor == null, 1000, 10);
|
||||
messagesBefore++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
messagesBefore++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wait for generation to finish
|
||||
const generateFinished = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
|
||||
textResult = await generateFinished;
|
||||
}
|
||||
} finally {
|
||||
typingIndicator.hide();
|
||||
@ -838,6 +753,8 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
activateSendButtons();
|
||||
showSwipeButtons();
|
||||
}
|
||||
|
||||
return Promise.resolve(textResult);
|
||||
}
|
||||
|
||||
function getLastMessageGenerationId() {
|
||||
@ -860,12 +777,35 @@ function activateImpersonate(members) {
|
||||
return memberIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates a group member based on the last message.
|
||||
* @param {string[]} members Array of group member avatar ids
|
||||
* @returns {number[]} Array of character ids
|
||||
*/
|
||||
function activateSwipe(members) {
|
||||
let activatedNames = [];
|
||||
const lastMessage = chat[chat.length - 1];
|
||||
|
||||
if (lastMessage.is_user || lastMessage.is_system || lastMessage.extra?.type === system_message_types.NARRATOR) {
|
||||
for (const message of chat.slice().reverse()) {
|
||||
if (message.is_user || message.is_system || message.extra?.type === system_message_types.NARRATOR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.original_avatar) {
|
||||
activatedNames.push(message.original_avatar);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (activatedNames.length === 0) {
|
||||
activatedNames.push(shuffle(members.slice())[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// pre-update group chat swipe
|
||||
if (!chat[chat.length - 1].original_avatar) {
|
||||
const matches = characters.filter(x => x.name == chat[chat.length - 1].name);
|
||||
if (!lastMessage.original_avatar) {
|
||||
const matches = characters.filter(x => x.name == lastMessage.name);
|
||||
|
||||
for (const match of matches) {
|
||||
if (members.includes(match.avatar)) {
|
||||
@ -875,7 +815,7 @@ function activateSwipe(members) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
activatedNames.push(chat[chat.length - 1].original_avatar);
|
||||
activatedNames.push(lastMessage.original_avatar);
|
||||
}
|
||||
|
||||
const memberIds = activatedNames
|
||||
@ -1103,6 +1043,15 @@ async function onGroupGenerationModeInput(e) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onGroupAutoModeDelayInput(e) {
|
||||
if (openGroupId) {
|
||||
let _thisGroup = groups.find((x) => x.id == openGroupId);
|
||||
_thisGroup.auto_mode_delay = Number(e.target.value);
|
||||
await editGroup(openGroupId, false, false);
|
||||
setAutoModeWorker();
|
||||
}
|
||||
}
|
||||
|
||||
async function onGroupNameInput() {
|
||||
if (openGroupId) {
|
||||
let _thisGroup = groups.find((x) => x.id == openGroupId);
|
||||
@ -1159,7 +1108,7 @@ function printGroupCandidates() {
|
||||
showNavigator: true,
|
||||
showSizeChanger: true,
|
||||
pageSize: Number(localStorage.getItem(storageKey)) || 5,
|
||||
sizeChangerOptions: [5, 10, 25, 50, 100, 200],
|
||||
sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
|
||||
afterSizeSelectorChange: function (e) {
|
||||
localStorage.setItem(storageKey, e.target.value);
|
||||
},
|
||||
@ -1186,7 +1135,7 @@ function printGroupMembers() {
|
||||
showNavigator: true,
|
||||
showSizeChanger: true,
|
||||
pageSize: Number(localStorage.getItem(storageKey)) || 5,
|
||||
sizeChangerOptions: [5, 10, 25, 50, 100, 200],
|
||||
sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
|
||||
afterSizeSelectorChange: function (e) {
|
||||
localStorage.setItem(storageKey, e.target.value);
|
||||
},
|
||||
@ -1299,6 +1248,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
$('#rm_group_submit').prop('disabled', !groupHasMembers);
|
||||
$('#rm_group_allow_self_responses').prop('checked', group && group.allow_self_responses);
|
||||
$('#rm_group_hidemutedsprites').prop('checked', group && group.hideMutedSprites);
|
||||
$('#rm_group_automode_delay').val(group?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY);
|
||||
|
||||
// bottom buttons
|
||||
if (openGroupId) {
|
||||
@ -1317,6 +1267,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
}
|
||||
|
||||
updateFavButtonState(group?.fav ?? false);
|
||||
setAutoModeWorker();
|
||||
|
||||
// top bar
|
||||
if (group) {
|
||||
@ -1509,6 +1460,7 @@ async function createGroup() {
|
||||
let allowSelfResponses = !!$('#rm_group_allow_self_responses').prop('checked');
|
||||
let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL;
|
||||
let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP;
|
||||
let autoModeDelay = Number($('#rm_group_automode_delay').val()) ?? DEFAULT_AUTO_MODE_DELAY;
|
||||
const members = newGroupMembers;
|
||||
const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', ');
|
||||
|
||||
@ -1537,6 +1489,7 @@ async function createGroup() {
|
||||
fav: fav_grp_checked,
|
||||
chat_id: chatName,
|
||||
chats: chats,
|
||||
auto_mode_delay: autoModeDelay,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -1768,17 +1721,17 @@ function doCurMemberListPopout() {
|
||||
|
||||
$('body').append(newElement);
|
||||
loadMovingUIState();
|
||||
$('#groupMemberListPopout').fadeIn(250);
|
||||
$('#groupMemberListPopout').fadeIn(animation_duration);
|
||||
dragElement(newElement);
|
||||
$('#groupMemberListPopoutClose').off('click').on('click', function () {
|
||||
$('#groupMemberListPopout').fadeOut(250, () => { $('#groupMemberListPopout').remove(); });
|
||||
$('#groupMemberListPopout').fadeOut(animation_duration, () => { $('#groupMemberListPopout').remove(); });
|
||||
});
|
||||
|
||||
// Re-add pagination not working in popout
|
||||
printGroupMembers();
|
||||
} else {
|
||||
console.debug('saw existing popout, removing');
|
||||
$('#groupMemberListPopout').fadeOut(250, () => { $('#groupMemberListPopout').remove(); });
|
||||
$('#groupMemberListPopout').fadeOut(animation_duration, () => { $('#groupMemberListPopout').remove(); });
|
||||
}
|
||||
}
|
||||
|
||||
@ -1809,6 +1762,7 @@ jQuery(() => {
|
||||
$('#rm_group_allow_self_responses').on('input', onGroupSelfResponsesClick);
|
||||
$('#rm_group_activation_strategy').on('change', onGroupActivationStrategyInput);
|
||||
$('#rm_group_generation_mode').on('change', onGroupGenerationModeInput);
|
||||
$('#rm_group_automode_delay').on('input', onGroupAutoModeDelayInput);
|
||||
$('#group_avatar_button').on('input', uploadGroupAvatar);
|
||||
$('#rm_group_restore_avatar').on('click', restoreGroupAvatar);
|
||||
$(document).on('click', '.group_member .right_menu_button', onGroupActionClick);
|
||||
|
@ -2,14 +2,13 @@ import {
|
||||
saveSettingsDebounced,
|
||||
callPopup,
|
||||
setGenerationProgress,
|
||||
CLIENT_VERSION,
|
||||
getRequestHeaders,
|
||||
max_context,
|
||||
amount_gen,
|
||||
} from '../script.js';
|
||||
import { SECRET_KEYS, writeSecret } from './secrets.js';
|
||||
import { delay } from './utils.js';
|
||||
import { getDeviceInfo } from './RossAscends-mods.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
import { autoSelectInstructPreset } from './instruct-mode.js';
|
||||
|
||||
export {
|
||||
@ -34,19 +33,96 @@ let horde_settings = {
|
||||
const MAX_RETRIES = 480;
|
||||
const CHECK_INTERVAL = 2500;
|
||||
const MIN_LENGTH = 16;
|
||||
const getRequestArgs = () => ({
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Client-Agent': CLIENT_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
async function getWorkers() {
|
||||
const response = await fetch('https://horde.koboldai.net/api/v2/workers?type=text', getRequestArgs());
|
||||
/**
|
||||
* Gets the available workers from Horde.
|
||||
* @param {boolean} force Do a force refresh of the workers
|
||||
* @returns {Promise<Array>} Array of workers
|
||||
*/
|
||||
async function getWorkers(force) {
|
||||
const response = await fetch('/api/horde/text-workers', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ force }),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the available models from Horde.
|
||||
* @param {boolean} force Do a force refresh of the models
|
||||
* @returns {Promise<Array>} Array of models
|
||||
*/
|
||||
async function getModels(force) {
|
||||
const response = await fetch('/api/horde/text-models', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ force }),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of a Horde task.
|
||||
* @param {string} taskId Task ID
|
||||
* @returns {Promise<Object>} Task status
|
||||
*/
|
||||
async function getTaskStatus(taskId) {
|
||||
const response = await fetch('/api/horde/task-status', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ taskId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get task status: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a Horde task.
|
||||
* @param {string} taskId Task ID
|
||||
*/
|
||||
async function cancelTask(taskId) {
|
||||
const response = await fetch('/api/horde/cancel-task', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ taskId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to cancel task: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Horde is online.
|
||||
* @returns {Promise<boolean>} True if Horde is online, false otherwise
|
||||
*/
|
||||
async function checkHordeStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/horde/status', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.ok;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateHordeModel() {
|
||||
let selectedModels = models.filter(m => horde_settings.models.includes(m.name));
|
||||
|
||||
@ -60,7 +136,7 @@ function validateHordeModel() {
|
||||
|
||||
async function adjustHordeGenerationParams(max_context_length, max_length) {
|
||||
console.log(max_context_length, max_length);
|
||||
const workers = await getWorkers();
|
||||
const workers = await getWorkers(false);
|
||||
let maxContextLength = max_context_length;
|
||||
let maxLength = max_length;
|
||||
let availableWorkers = [];
|
||||
@ -126,10 +202,7 @@ async function generateHorde(prompt, params, signal, reportProgress) {
|
||||
|
||||
const response = await fetch('/api/horde/generate-text', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
'Client-Agent': CLIENT_VERSION,
|
||||
},
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
@ -146,24 +219,17 @@ async function generateHorde(prompt, params, signal, reportProgress) {
|
||||
throw new Error(`Horde generation failed: ${reason}`);
|
||||
}
|
||||
|
||||
const task_id = responseJson.id;
|
||||
const taskId = responseJson.id;
|
||||
let queue_position_first = null;
|
||||
console.log(`Horde task id = ${task_id}`);
|
||||
console.log(`Horde task id = ${taskId}`);
|
||||
|
||||
for (let retryNumber = 0; retryNumber < MAX_RETRIES; retryNumber++) {
|
||||
if (signal.aborted) {
|
||||
fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${task_id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Client-Agent': CLIENT_VERSION,
|
||||
},
|
||||
});
|
||||
cancelTask(taskId);
|
||||
throw new Error('Request aborted');
|
||||
}
|
||||
|
||||
const statusCheckResponse = await fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${task_id}`, getRequestArgs());
|
||||
|
||||
const statusCheckJson = await statusCheckResponse.json();
|
||||
const statusCheckJson = await getTaskStatus(taskId);
|
||||
console.log(statusCheckJson);
|
||||
|
||||
if (statusCheckJson.faulted === true) {
|
||||
@ -202,18 +268,13 @@ async function generateHorde(prompt, params, signal, reportProgress) {
|
||||
throw new Error('Horde timeout');
|
||||
}
|
||||
|
||||
async function checkHordeStatus() {
|
||||
const response = await fetch('https://horde.koboldai.net/api/v2/status/heartbeat', getRequestArgs());
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
async function getHordeModels() {
|
||||
/**
|
||||
* Displays the available models in the Horde model selection dropdown.
|
||||
* @param {boolean} force Force refresh of the models
|
||||
*/
|
||||
async function getHordeModels(force) {
|
||||
$('#horde_model').empty();
|
||||
const response = await fetch('https://horde.koboldai.net/api/v2/status/models?type=text', getRequestArgs());
|
||||
models = await response.json();
|
||||
models.sort((a, b) => {
|
||||
return b.performance - a.performance;
|
||||
});
|
||||
models = (await getModels(force)).sort((a, b) => b.performance - a.performance);
|
||||
for (const model of models) {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.name;
|
||||
@ -299,12 +360,11 @@ jQuery(function () {
|
||||
await writeSecret(SECRET_KEYS.HORDE, key);
|
||||
});
|
||||
|
||||
$('#horde_refresh').on('click', getHordeModels);
|
||||
$('#horde_refresh').on('click', () => getHordeModels(true));
|
||||
$('#horde_kudos').on('click', showKudos);
|
||||
|
||||
// Not needed on mobile
|
||||
const deviceInfo = getDeviceInfo();
|
||||
if (deviceInfo && deviceInfo.device.type === 'desktop') {
|
||||
if (!isMobile()) {
|
||||
$('#horde_model').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select Horde models',
|
||||
|
@ -29,6 +29,7 @@ const controls = [
|
||||
{ id: 'instruct_first_output_sequence', property: 'first_output_sequence', isCheckbox: false },
|
||||
{ id: 'instruct_last_output_sequence', property: 'last_output_sequence', isCheckbox: false },
|
||||
{ id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false },
|
||||
{ id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true },
|
||||
];
|
||||
|
||||
/**
|
||||
@ -136,7 +137,7 @@ export function autoSelectInstructPreset(modelId) {
|
||||
let foundMatch = false;
|
||||
for (const instruct_preset of instruct_presets) {
|
||||
// If instruct preset matches the context template
|
||||
if (instruct_preset.name === power_user.context.preset) {
|
||||
if (power_user.instruct.bind_to_context && instruct_preset.name === power_user.context.preset) {
|
||||
foundMatch = true;
|
||||
selectInstructPreset(instruct_preset.name);
|
||||
break;
|
||||
@ -163,7 +164,7 @@ export function autoSelectInstructPreset(modelId) {
|
||||
}
|
||||
}
|
||||
|
||||
if (power_user.default_instruct && power_user.instruct.preset !== power_user.default_instruct) {
|
||||
if (power_user.instruct.bind_to_context && power_user.default_instruct && power_user.instruct.preset !== power_user.default_instruct) {
|
||||
if (instruct_presets.some(p => p.name === power_user.default_instruct)) {
|
||||
console.log(`Instruct mode: default preset "${power_user.default_instruct}" selected`);
|
||||
$('#instruct_presets').val(power_user.default_instruct).trigger('change');
|
||||
@ -409,6 +410,10 @@ jQuery(() => {
|
||||
});
|
||||
|
||||
$('#instruct_enabled').on('change', function () {
|
||||
if (!power_user.instruct.bind_to_context) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When instruct mode gets enabled, select context template matching selected instruct preset
|
||||
if (power_user.instruct.enabled) {
|
||||
selectMatchingContextTemplate(power_user.instruct.preset);
|
||||
@ -440,8 +445,10 @@ jQuery(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Select matching context template
|
||||
selectMatchingContextTemplate(name);
|
||||
if (power_user.instruct.bind_to_context) {
|
||||
// Select matching context template
|
||||
selectMatchingContextTemplate(name);
|
||||
}
|
||||
|
||||
highlightDefaultPreset();
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import {
|
||||
power_user,
|
||||
} from './power-user.js';
|
||||
import EventSourceStream from './sse-stream.js';
|
||||
import { getSortableDelay } from './utils.js';
|
||||
|
||||
export const kai_settings = {
|
||||
@ -128,13 +129,6 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
|
||||
top_p: kai_settings.top_p,
|
||||
min_p: (kai_flags.can_use_min_p || isHorde) ? kai_settings.min_p : undefined,
|
||||
typical: kai_settings.typical,
|
||||
s1: sampler_order[0],
|
||||
s2: sampler_order[1],
|
||||
s3: sampler_order[2],
|
||||
s4: sampler_order[3],
|
||||
s5: sampler_order[4],
|
||||
s6: sampler_order[5],
|
||||
s7: sampler_order[6],
|
||||
use_world_info: false,
|
||||
singleline: false,
|
||||
stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate, isContinue) : undefined,
|
||||
@ -153,44 +147,50 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
|
||||
return generate_data;
|
||||
}
|
||||
|
||||
function tryParseStreamingError(response, decoded) {
|
||||
try {
|
||||
const data = JSON.parse(decoded);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
toastr.error(data.error.message || response.statusText, 'KoboldAI API');
|
||||
throw new Error(data);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// No JSON. Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateKoboldWithStreaming(generate_data, signal) {
|
||||
const response = await fetch('/generate', {
|
||||
const response = await fetch('/api/backends/kobold/generate', {
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(generate_data),
|
||||
method: 'POST',
|
||||
signal: signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
tryParseStreamingError(response, await response.text());
|
||||
throw new Error(`Got response status ${response.status}`);
|
||||
}
|
||||
const eventStream = new EventSourceStream();
|
||||
response.body.pipeThrough(eventStream);
|
||||
const reader = eventStream.readable.getReader();
|
||||
|
||||
return async function* streamData() {
|
||||
const decoder = new TextDecoder();
|
||||
const reader = response.body.getReader();
|
||||
let getMessage = '';
|
||||
let messageBuffer = '';
|
||||
let text = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
let response = decoder.decode(value);
|
||||
let eventList = [];
|
||||
if (done) return;
|
||||
|
||||
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
|
||||
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
|
||||
messageBuffer += response;
|
||||
eventList = messageBuffer.split('\n\n');
|
||||
// Last element will be an empty string or a leftover partial message
|
||||
messageBuffer = eventList.pop();
|
||||
|
||||
for (let event of eventList) {
|
||||
for (let subEvent of event.split('\n')) {
|
||||
if (subEvent.startsWith('data')) {
|
||||
let data = JSON.parse(subEvent.substring(5));
|
||||
getMessage += (data?.token || '');
|
||||
yield { text: getMessage, swipes: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return;
|
||||
const data = JSON.parse(value.data);
|
||||
if (data?.token) {
|
||||
text += data.token;
|
||||
}
|
||||
yield { text, swipes: [] };
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -310,87 +310,24 @@ const sliders = [
|
||||
},
|
||||
];
|
||||
|
||||
export function setKoboldFlags(version, koboldVersion) {
|
||||
kai_flags.can_use_stop_sequence = canUseKoboldStopSequence(version);
|
||||
kai_flags.can_use_streaming = canUseKoboldStreaming(koboldVersion);
|
||||
kai_flags.can_use_tokenization = canUseKoboldTokenization(koboldVersion);
|
||||
kai_flags.can_use_default_badwordsids = canUseDefaultBadwordIds(version);
|
||||
kai_flags.can_use_mirostat = canUseMirostat(koboldVersion);
|
||||
kai_flags.can_use_grammar = canUseGrammar(koboldVersion);
|
||||
kai_flags.can_use_min_p = canUseMinP(koboldVersion);
|
||||
export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) {
|
||||
kai_flags.can_use_stop_sequence = versionCompare(koboldUnitedVersion, MIN_STOP_SEQUENCE_VERSION);
|
||||
kai_flags.can_use_streaming = versionCompare(koboldCppVersion, MIN_STREAMING_KCPPVERSION);
|
||||
kai_flags.can_use_tokenization = versionCompare(koboldCppVersion, MIN_TOKENIZATION_KCPPVERSION);
|
||||
kai_flags.can_use_default_badwordsids = versionCompare(koboldUnitedVersion, MIN_UNBAN_VERSION);
|
||||
kai_flags.can_use_mirostat = versionCompare(koboldCppVersion, MIN_MIROSTAT_KCPPVERSION);
|
||||
kai_flags.can_use_grammar = versionCompare(koboldCppVersion, MIN_GRAMMAR_KCPPVERSION);
|
||||
kai_flags.can_use_min_p = versionCompare(koboldCppVersion, MIN_MIN_P_KCPPVERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Kobold stop sequence can be used with the given version.
|
||||
* @param {string} version KoboldAI version to check.
|
||||
* @returns {boolean} True if the Kobold stop sequence can be used, false otherwise.
|
||||
* Compares two version numbers, returning true if srcVersion >= minVersion
|
||||
* @param {string} srcVersion The current version.
|
||||
* @param {string} minVersion The target version number to test against
|
||||
* @returns {boolean} True if srcVersion >= minVersion, false if not
|
||||
*/
|
||||
function canUseKoboldStopSequence(version) {
|
||||
return (version || '0.0.0').localeCompare(MIN_STOP_SEQUENCE_VERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Kobold default badword ids can be used with the given version.
|
||||
* @param {string} version KoboldAI version to check.
|
||||
* @returns {boolean} True if the Kobold default badword ids can be used, false otherwise.
|
||||
*/
|
||||
function canUseDefaultBadwordIds(version) {
|
||||
return (version || '0.0.0').localeCompare(MIN_UNBAN_VERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Kobold streaming API can be used with the given version.
|
||||
* @param {{ result: string; version: string; }} koboldVersion KoboldAI version object.
|
||||
* @returns {boolean} True if the Kobold streaming API can be used, false otherwise.
|
||||
*/
|
||||
function canUseKoboldStreaming(koboldVersion) {
|
||||
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
|
||||
return (koboldVersion.version || '0.0').localeCompare(MIN_STREAMING_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Kobold tokenization API can be used with the given version.
|
||||
* @param {{ result: string; version: string; }} koboldVersion KoboldAI version object.
|
||||
* @returns {boolean} True if the Kobold tokenization API can be used, false otherwise.
|
||||
*/
|
||||
function canUseKoboldTokenization(koboldVersion) {
|
||||
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
|
||||
return (koboldVersion.version || '0.0').localeCompare(MIN_TOKENIZATION_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Kobold mirostat can be used with the given version.
|
||||
* @param {{result: string; version: string;}} koboldVersion KoboldAI version object.
|
||||
* @returns {boolean} True if the Kobold mirostat API can be used, false otherwise.
|
||||
*/
|
||||
function canUseMirostat(koboldVersion) {
|
||||
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
|
||||
return (koboldVersion.version || '0.0').localeCompare(MIN_MIROSTAT_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Kobold grammar can be used with the given version.
|
||||
* @param {{result: string; version:string;}} koboldVersion KoboldAI version object.
|
||||
* @returns {boolean} True if the Kobold grammar can be used, false otherwise.
|
||||
*/
|
||||
function canUseGrammar(koboldVersion) {
|
||||
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
|
||||
return (koboldVersion.version || '0.0').localeCompare(MIN_GRAMMAR_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Kobold min_p can be used with the given version.
|
||||
* @param {{result:string, version:string;}} koboldVersion KoboldAI version object.
|
||||
* @returns {boolean} True if the Kobold min_p can be used, false otherwise.
|
||||
*/
|
||||
function canUseMinP(koboldVersion) {
|
||||
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
|
||||
return (koboldVersion.version || '0.0').localeCompare(MIN_MIN_P_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
} else return false;
|
||||
function versionCompare(srcVersion, minVersion) {
|
||||
return (srcVersion || '0.0.0').localeCompare(minVersion, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
126
public/scripts/logit-bias.js
Normal file
126
public/scripts/logit-bias.js
Normal file
@ -0,0 +1,126 @@
|
||||
import { saveSettingsDebounced } from '../script.js';
|
||||
import { getTextTokens } from './tokenizers.js';
|
||||
import { uuidv4 } from './utils.js';
|
||||
|
||||
export const BIAS_CACHE = new Map();
|
||||
|
||||
/**
|
||||
* Displays the logit bias list in the specified container.
|
||||
* @param {object} logitBias Logit bias object
|
||||
* @param {string} containerSelector Container element selector
|
||||
* @returns
|
||||
*/
|
||||
export function displayLogitBias(logitBias, containerSelector) {
|
||||
if (!Array.isArray(logitBias)) {
|
||||
console.log('Logit bias set not found');
|
||||
return;
|
||||
}
|
||||
|
||||
$(containerSelector).find('.logit_bias_list').empty();
|
||||
|
||||
for (const entry of logitBias) {
|
||||
if (entry) {
|
||||
createLogitBiasListItem(entry, logitBias, containerSelector);
|
||||
}
|
||||
}
|
||||
|
||||
BIAS_CACHE.delete(containerSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new logit bias entry
|
||||
* @param {object[]} logitBias Array of logit bias objects
|
||||
* @param {string} containerSelector Container element ID
|
||||
*/
|
||||
export function createNewLogitBiasEntry(logitBias, containerSelector) {
|
||||
const entry = { id: uuidv4(), text: '', value: 0 };
|
||||
logitBias.push(entry);
|
||||
BIAS_CACHE.delete(containerSelector);
|
||||
createLogitBiasListItem(entry, logitBias, containerSelector);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a logit bias list item.
|
||||
* @param {object} entry Logit bias entry
|
||||
* @param {object[]} logitBias Array of logit bias objects
|
||||
* @param {string} containerSelector Container element ID
|
||||
*/
|
||||
function createLogitBiasListItem(entry, logitBias, containerSelector) {
|
||||
const id = entry.id;
|
||||
const template = $('#logit_bias_template .logit_bias_form').clone();
|
||||
template.data('id', id);
|
||||
template.find('.logit_bias_text').val(entry.text).on('input', function () {
|
||||
entry.text = $(this).val();
|
||||
BIAS_CACHE.delete(containerSelector);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
template.find('.logit_bias_value').val(entry.value).on('input', function () {
|
||||
entry.value = Number($(this).val());
|
||||
BIAS_CACHE.delete(containerSelector);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
template.find('.logit_bias_remove').on('click', function () {
|
||||
$(this).closest('.logit_bias_form').remove();
|
||||
const index = logitBias.indexOf(entry);
|
||||
if (index > -1) {
|
||||
logitBias.splice(index, 1);
|
||||
}
|
||||
BIAS_CACHE.delete(containerSelector);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$(containerSelector).find('.logit_bias_list').prepend(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate logit bias list from preset.
|
||||
* @param {object[]} biasPreset Bias preset
|
||||
* @param {number} tokenizerType Tokenizer type (see tokenizers.js)
|
||||
* @param {(bias: number, sequence: number[]) => object} getBiasObject Transformer function to create bias object
|
||||
* @returns {object[]} Array of logit bias objects
|
||||
*/
|
||||
export function getLogitBiasListResult(biasPreset, tokenizerType, getBiasObject) {
|
||||
const result = [];
|
||||
|
||||
for (const entry of biasPreset) {
|
||||
if (entry.text?.length > 0) {
|
||||
const text = entry.text.trim();
|
||||
|
||||
// Skip empty lines
|
||||
if (text.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verbatim text
|
||||
if (text.startsWith('{') && text.endsWith('}')) {
|
||||
const tokens = getTextTokens(tokenizerType, text.slice(1, -1));
|
||||
result.push(getBiasObject(entry.value, tokens));
|
||||
}
|
||||
|
||||
|
||||
// Raw token ids, JSON serialized
|
||||
else if (text.startsWith('[') && text.endsWith(']')) {
|
||||
try {
|
||||
const tokens = JSON.parse(text);
|
||||
|
||||
if (Array.isArray(tokens) && tokens.every(t => Number.isInteger(t))) {
|
||||
result.push(getBiasObject(entry.value, tokens));
|
||||
} else {
|
||||
throw new Error('Not an array of integers');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Failed to parse logit bias token list: ${text}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Text with a leading space
|
||||
else {
|
||||
const biasText = ` ${text}`;
|
||||
const tokens = getTextTokens(tokenizerType, biasText);
|
||||
result.push(getBiasObject(entry.value, tokens));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { setGenerationParamsFromPreset } from '../script.js';
|
||||
import { getDeviceInfo } from './RossAscends-mods.js';
|
||||
import { textgenerationwebui_settings as textgen_settings } from './textgen-settings.js';
|
||||
|
||||
let models = [];
|
||||
|
||||
export async function loadMancerModels(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Invalid Mancer models data', data);
|
||||
return;
|
||||
}
|
||||
|
||||
models = data;
|
||||
|
||||
$('#mancer_model').empty();
|
||||
for (const model of data) {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.id;
|
||||
option.text = model.name;
|
||||
option.selected = model.id === textgen_settings.mancer_model;
|
||||
$('#mancer_model').append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function onMancerModelSelect() {
|
||||
const modelId = String($('#mancer_model').val());
|
||||
textgen_settings.mancer_model = modelId;
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
|
||||
const limits = models.find(x => x.id === modelId)?.limits;
|
||||
setGenerationParamsFromPreset({ max_length: limits.context, genamt: limits.completion });
|
||||
}
|
||||
|
||||
function getMancerModelTemplate(option) {
|
||||
const model = models.find(x => x.id === option?.element?.value);
|
||||
|
||||
if (!option.id || !model) {
|
||||
return option.text;
|
||||
}
|
||||
|
||||
const creditsPerPrompt = (model.limits?.context - model.limits?.completion) * model.pricing?.prompt;
|
||||
const creditsPerCompletion = model.limits?.completion * model.pricing?.completion;
|
||||
const creditsTotal = Math.round(creditsPerPrompt + creditsPerCompletion).toFixed(0);
|
||||
|
||||
return $((`
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | <span>${model.limits?.context} ctx</span> / <span>${model.limits?.completion} res</span> | <small>Credits per request (max): ${creditsTotal}</small></div>
|
||||
</div>
|
||||
`));
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
$('#mancer_model').on('change', onMancerModelSelect);
|
||||
|
||||
const deviceInfo = getDeviceInfo();
|
||||
if (deviceInfo && deviceInfo.device.type === 'desktop') {
|
||||
$('#mancer_model').select2({
|
||||
placeholder: 'Select a model',
|
||||
searchInputPlaceholder: 'Search models...',
|
||||
searchInputCssClass: 'text_pole',
|
||||
width: '100%',
|
||||
templateResult: getMancerModelTemplate,
|
||||
});
|
||||
}
|
||||
});
|
@ -8,14 +8,15 @@ import {
|
||||
substituteParams,
|
||||
} from '../script.js';
|
||||
import { getCfgPrompt } from './cfg-scale.js';
|
||||
import { MAX_CONTEXT_DEFAULT, MAX_RESPONSE_DEFAULT } from './power-user.js';
|
||||
import { MAX_CONTEXT_DEFAULT, MAX_RESPONSE_DEFAULT, power_user } from './power-user.js';
|
||||
import { getTextTokens, tokenizers } from './tokenizers.js';
|
||||
import EventSourceStream from './sse-stream.js';
|
||||
import {
|
||||
getSortableDelay,
|
||||
getStringHash,
|
||||
onlyUnique,
|
||||
uuidv4,
|
||||
} from './utils.js';
|
||||
import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
|
||||
|
||||
const default_preamble = '[ Style: chat, complex, sensory, visceral ]';
|
||||
const default_order = [1, 5, 0, 2, 3, 4];
|
||||
@ -58,7 +59,7 @@ const nai_tiers = {
|
||||
|
||||
let novel_data = null;
|
||||
let badWordsCache = {};
|
||||
let biasCache = undefined;
|
||||
const BIAS_KEY = '#novel_api-settings';
|
||||
|
||||
export function setNovelData(data) {
|
||||
novel_data = data;
|
||||
@ -144,7 +145,7 @@ export function loadNovelSettings(settings) {
|
||||
//load the rest of the Novel settings without any checks
|
||||
nai_settings.model_novel = settings.model_novel;
|
||||
$('#model_novel_select').val(nai_settings.model_novel);
|
||||
$(`#model_novel_select option[value=${nai_settings.model_novel}]`).attr('selected', true);
|
||||
$(`#model_novel_select option[value=${nai_settings.model_novel}]`).prop('selected', true);
|
||||
|
||||
if (settings.nai_preamble !== undefined) {
|
||||
nai_settings.preamble = settings.nai_preamble;
|
||||
@ -216,7 +217,7 @@ function loadNovelSettingsUi(ui_settings) {
|
||||
|
||||
$('#streaming_novel').prop('checked', ui_settings.streaming_novel);
|
||||
sortItemsByOrder(ui_settings.order);
|
||||
displayLogitBias(ui_settings.logit_bias);
|
||||
displayLogitBias(ui_settings.logit_bias, BIAS_KEY);
|
||||
}
|
||||
|
||||
const sliders = [
|
||||
@ -432,8 +433,12 @@ export function getNovelGenerationData(finalPrompt, settings, maxLength, isImper
|
||||
|
||||
let logitBias = [];
|
||||
if (tokenizerType !== tokenizers.NONE && Array.isArray(nai_settings.logit_bias) && nai_settings.logit_bias.length) {
|
||||
logitBias = biasCache || calculateLogitBias();
|
||||
biasCache = logitBias;
|
||||
logitBias = BIAS_CACHE.get(BIAS_KEY) || calculateLogitBias();
|
||||
BIAS_CACHE.set(BIAS_KEY, logitBias);
|
||||
}
|
||||
|
||||
if (power_user.console_log_prompts) {
|
||||
console.log(finalPrompt);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -524,65 +529,14 @@ function saveSamplingOrder() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function displayLogitBias(logit_bias) {
|
||||
if (!Array.isArray(logit_bias)) {
|
||||
console.log('Logit bias set not found');
|
||||
return;
|
||||
}
|
||||
|
||||
$('.novelai_logit_bias_list').empty();
|
||||
|
||||
for (const entry of logit_bias) {
|
||||
if (entry) {
|
||||
createLogitBiasListItem(entry);
|
||||
}
|
||||
}
|
||||
|
||||
biasCache = undefined;
|
||||
}
|
||||
|
||||
function createNewLogitBiasEntry() {
|
||||
const entry = { id: uuidv4(), text: '', value: 0 };
|
||||
nai_settings.logit_bias.push(entry);
|
||||
biasCache = undefined;
|
||||
createLogitBiasListItem(entry);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function createLogitBiasListItem(entry) {
|
||||
const id = entry.id;
|
||||
const template = $('#novelai_logit_bias_template .novelai_logit_bias_form').clone();
|
||||
template.data('id', id);
|
||||
template.find('.novelai_logit_bias_text').val(entry.text).on('input', function () {
|
||||
entry.text = $(this).val();
|
||||
biasCache = undefined;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
template.find('.novelai_logit_bias_value').val(entry.value).on('input', function () {
|
||||
entry.value = Number($(this).val());
|
||||
biasCache = undefined;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
template.find('.novelai_logit_bias_remove').on('click', function () {
|
||||
$(this).closest('.novelai_logit_bias_form').remove();
|
||||
const index = nai_settings.logit_bias.indexOf(entry);
|
||||
if (index > -1) {
|
||||
nai_settings.logit_bias.splice(index, 1);
|
||||
}
|
||||
biasCache = undefined;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('.novelai_logit_bias_list').prepend(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates logit bias for Novel AI
|
||||
* @returns {object[]} Array of logit bias objects
|
||||
*/
|
||||
function calculateLogitBias() {
|
||||
const bias_preset = nai_settings.logit_bias;
|
||||
const biasPreset = nai_settings.logit_bias;
|
||||
|
||||
if (!Array.isArray(bias_preset) || bias_preset.length === 0) {
|
||||
if (!Array.isArray(biasPreset) || biasPreset.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -604,47 +558,7 @@ function calculateLogitBias() {
|
||||
};
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for (const entry of bias_preset) {
|
||||
if (entry.text?.length > 0) {
|
||||
const text = entry.text.trim();
|
||||
|
||||
// Skip empty lines
|
||||
if (text.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verbatim text
|
||||
if (text.startsWith('{') && text.endsWith('}')) {
|
||||
const tokens = getTextTokens(tokenizerType, text.slice(1, -1));
|
||||
result.push(getBiasObject(entry.value, tokens));
|
||||
}
|
||||
|
||||
// Raw token ids, JSON serialized
|
||||
else if (text.startsWith('[') && text.endsWith(']')) {
|
||||
try {
|
||||
const tokens = JSON.parse(text);
|
||||
|
||||
if (Array.isArray(tokens) && tokens.every(t => Number.isInteger(t))) {
|
||||
result.push(getBiasObject(entry.value, tokens));
|
||||
} else {
|
||||
throw new Error('Not an array of integers');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Failed to parse logit bias token list: ${text}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Text with a leading space
|
||||
else {
|
||||
const biasText = ` ${text}`;
|
||||
const tokens = getTextTokens(tokenizerType, biasText);
|
||||
result.push(getBiasObject(entry.value, tokens));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = getLogitBiasListResult(biasPreset, tokenizerType, getBiasObject);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -663,7 +577,7 @@ export function adjustNovelInstructionPrompt(prompt) {
|
||||
return stripedPrompt;
|
||||
}
|
||||
|
||||
function tryParseStreamingError(decoded) {
|
||||
function tryParseStreamingError(response, decoded) {
|
||||
try {
|
||||
const data = JSON.parse(decoded);
|
||||
|
||||
@ -671,8 +585,8 @@ function tryParseStreamingError(decoded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.message && data.statusCode >= 400) {
|
||||
toastr.error(data.message, 'Error');
|
||||
if (data.message || data.error) {
|
||||
toastr.error(data.message || data.error?.message || response.statusText, 'NovelAI API');
|
||||
throw new Error(data);
|
||||
}
|
||||
}
|
||||
@ -690,39 +604,27 @@ export async function generateNovelWithStreaming(generate_data, signal) {
|
||||
method: 'POST',
|
||||
signal: signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
tryParseStreamingError(response, await response.text());
|
||||
throw new Error(`Got response status ${response.status}`);
|
||||
}
|
||||
const eventStream = new EventSourceStream();
|
||||
response.body.pipeThrough(eventStream);
|
||||
const reader = eventStream.readable.getReader();
|
||||
|
||||
return async function* streamData() {
|
||||
const decoder = new TextDecoder();
|
||||
const reader = response.body.getReader();
|
||||
let getMessage = '';
|
||||
let messageBuffer = '';
|
||||
let text = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
let decoded = decoder.decode(value);
|
||||
let eventList = [];
|
||||
if (done) return;
|
||||
|
||||
tryParseStreamingError(decoded);
|
||||
const data = JSON.parse(value.data);
|
||||
|
||||
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
|
||||
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
|
||||
messageBuffer += decoded;
|
||||
eventList = messageBuffer.split('\n\n');
|
||||
// Last element will be an empty string or a leftover partial message
|
||||
messageBuffer = eventList.pop();
|
||||
|
||||
for (let event of eventList) {
|
||||
for (let subEvent of event.split('\n')) {
|
||||
if (subEvent.startsWith('data')) {
|
||||
let data = JSON.parse(subEvent.substring(5));
|
||||
getMessage += (data?.token || '');
|
||||
yield { text: getMessage, swipes: [] };
|
||||
}
|
||||
}
|
||||
if (data.token) {
|
||||
text += data.token;
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
yield { text, swipes: [] };
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -789,5 +691,5 @@ jQuery(function () {
|
||||
saveSamplingOrder();
|
||||
});
|
||||
|
||||
$('#novelai_logit_bias_new_entry').on('click', createNewLogitBiasEntry);
|
||||
$('#novelai_logit_bias_new_entry').on('click', () => createNewLogitBiasEntry(nai_settings.logit_bias, BIAS_KEY));
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -193,6 +193,22 @@ export function autoSelectPersona(name) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the name of a persona if it exists.
|
||||
* @param {string} avatarId User avatar id
|
||||
* @param {string} newName New name for the persona
|
||||
*/
|
||||
export async function updatePersonaNameIfExists(avatarId, newName) {
|
||||
if (avatarId in power_user.personas) {
|
||||
power_user.personas[avatarId] = newName;
|
||||
await getUserAvatars();
|
||||
saveSettingsDebounced();
|
||||
console.log(`Updated persona name for ${avatarId} to ${newName}`);
|
||||
} else {
|
||||
console.log(`Persona name ${avatarId} was not updated because it does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
async function bindUserNameToPersona() {
|
||||
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
|
||||
|
||||
|
@ -19,6 +19,8 @@ import {
|
||||
showMoreMessages,
|
||||
saveSettings,
|
||||
saveChatConditional,
|
||||
setAnimationDuration,
|
||||
ANIMATION_DURATION_DEFAULT,
|
||||
} from '../script.js';
|
||||
import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
|
||||
import {
|
||||
@ -34,8 +36,9 @@ import {
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { tags } from './tags.js';
|
||||
import { tokenizers } from './tokenizers.js';
|
||||
import { BIAS_CACHE } from './logit-bias.js';
|
||||
|
||||
import { countOccurrences, debounce, delay, isOdd, resetScrollHeight, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
||||
import { countOccurrences, debounce, delay, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
||||
|
||||
export {
|
||||
loadPowerUserSettings,
|
||||
@ -55,7 +58,7 @@ const MAX_CONTEXT_UNLOCKED = 200 * 1024;
|
||||
const MAX_RESPONSE_UNLOCKED = 16 * 1024;
|
||||
const unlockedMaxContextStep = 512;
|
||||
const maxContextMin = 512;
|
||||
const maxContextStep = 256;
|
||||
const maxContextStep = 64;
|
||||
|
||||
const defaultStoryString = '{{#if system}}{{system}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}\'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}';
|
||||
const defaultExampleSeparator = '***';
|
||||
@ -112,6 +115,7 @@ let power_user = {
|
||||
},
|
||||
markdown_escape_strings: '',
|
||||
chat_truncation: 100,
|
||||
streaming_fps: 30,
|
||||
|
||||
ui_mode: ui_mode.POWER,
|
||||
fast_ui_mode: true,
|
||||
@ -201,6 +205,7 @@ let power_user = {
|
||||
names: false,
|
||||
names_force_groups: true,
|
||||
activation_regex: '',
|
||||
bind_to_context: false,
|
||||
},
|
||||
|
||||
default_context: 'Default',
|
||||
@ -228,6 +233,8 @@ let power_user = {
|
||||
bogus_folders: false,
|
||||
aux_field: 'character_version',
|
||||
restore_user_input: true,
|
||||
reduced_motion: false,
|
||||
compact_input_area: true,
|
||||
};
|
||||
|
||||
let themes = [];
|
||||
@ -268,6 +275,8 @@ const storage_keys = {
|
||||
expand_message_actions: 'ExpandMessageActions',
|
||||
enableZenSliders: 'enableZenSliders',
|
||||
enableLabMode: 'enableLabMode',
|
||||
reduced_motion: 'reduced_motion',
|
||||
compact_input_area: 'compact_input_area',
|
||||
};
|
||||
|
||||
const contextControls = [
|
||||
@ -436,6 +445,22 @@ function switchMessageActions() {
|
||||
$('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
|
||||
}
|
||||
|
||||
function switchReducedMotion() {
|
||||
const value = localStorage.getItem(storage_keys.reduced_motion);
|
||||
power_user.reduced_motion = value === null ? false : value == 'true';
|
||||
jQuery.fx.off = power_user.reduced_motion;
|
||||
const overrideDuration = power_user.reduced_motion ? 0 : ANIMATION_DURATION_DEFAULT;
|
||||
setAnimationDuration(overrideDuration);
|
||||
$('#reduced_motion').prop('checked', power_user.reduced_motion);
|
||||
}
|
||||
|
||||
function switchCompactInputArea() {
|
||||
const value = localStorage.getItem(storage_keys.compact_input_area);
|
||||
power_user.compact_input_area = value === null ? true : value == 'true';
|
||||
$('#send_form').toggleClass('compact', power_user.compact_input_area);
|
||||
$('#compact_input_area').prop('checked', power_user.compact_input_area);
|
||||
}
|
||||
|
||||
var originalSliderValues = [];
|
||||
|
||||
async function switchLabMode() {
|
||||
@ -533,7 +558,7 @@ async function CreateZenSliders(elmnt) {
|
||||
var sliderMax = Number(originalSlider.attr('max'));
|
||||
var sliderValue = originalSlider.val();
|
||||
var sliderRange = sliderMax - sliderMin;
|
||||
var numSteps = 10;
|
||||
var numSteps = 20;
|
||||
var decimals = 2;
|
||||
var offVal, allVal;
|
||||
var stepScale;
|
||||
@ -1227,6 +1252,22 @@ async function applyTheme(name) {
|
||||
await printCharacters(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'reduced_motion',
|
||||
action: async () => {
|
||||
localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
|
||||
$('#reduced_motion').prop('checked', power_user.reduced_motion);
|
||||
switchReducedMotion();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'compact_input_area',
|
||||
action: async () => {
|
||||
localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
|
||||
$('#compact_input_area').prop('checked', power_user.compact_input_area);
|
||||
switchCompactInputArea();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const { key, selector, type, action } of themeProperties) {
|
||||
@ -1440,6 +1481,9 @@ function loadPowerUserSettings(settings, data) {
|
||||
$('#chat_truncation').val(power_user.chat_truncation);
|
||||
$('#chat_truncation_counter').val(power_user.chat_truncation);
|
||||
|
||||
$('#streaming_fps').val(power_user.streaming_fps);
|
||||
$('#streaming_fps_counter').val(power_user.streaming_fps);
|
||||
|
||||
$('#font_scale').val(power_user.font_scale);
|
||||
$('#font_scale_counter').val(power_user.font_scale);
|
||||
|
||||
@ -1459,6 +1503,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
$('#shadow-color-picker').attr('color', power_user.shadow_color);
|
||||
$('#border-color-picker').attr('color', power_user.border_color);
|
||||
$('#ui_mode_select').val(power_user.ui_mode).find(`option[value="${power_user.ui_mode}"]`).attr('selected', true);
|
||||
$('#reduced_motion').prop('checked', power_user.reduced_motion);
|
||||
|
||||
for (const theme of themes) {
|
||||
const option = document.createElement('option');
|
||||
@ -1478,6 +1523,8 @@ function loadPowerUserSettings(settings, data) {
|
||||
|
||||
|
||||
$(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop('selected', true);
|
||||
switchReducedMotion();
|
||||
switchCompactInputArea();
|
||||
reloadMarkdownProcessor(power_user.render_formulas);
|
||||
loadInstructMode(data);
|
||||
loadContextSettings();
|
||||
@ -1504,7 +1551,7 @@ async function loadCharListState() {
|
||||
}
|
||||
|
||||
function loadMovingUIState() {
|
||||
if (isMobile() === false
|
||||
if (!isMobile()
|
||||
&& power_user.movingUIState
|
||||
&& power_user.movingUI === true) {
|
||||
console.debug('loading movingUI state');
|
||||
@ -1672,17 +1719,18 @@ function loadContextSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
// Select matching instruct preset
|
||||
for (const instruct_preset of instruct_presets) {
|
||||
// If instruct preset matches the context template
|
||||
if (instruct_preset.name === name) {
|
||||
selectInstructPreset(instruct_preset.name);
|
||||
break;
|
||||
if (power_user.instruct.bind_to_context) {
|
||||
// Select matching instruct preset
|
||||
for (const instruct_preset of instruct_presets) {
|
||||
// If instruct preset matches the context template
|
||||
if (instruct_preset.name === name) {
|
||||
selectInstructPreset(instruct_preset.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlightDefaultContext();
|
||||
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
@ -1818,10 +1866,6 @@ export function renderStoryString(params) {
|
||||
|
||||
const sortFunc = (a, b) => power_user.sort_order == 'asc' ? compareFunc(a, b) : compareFunc(b, a);
|
||||
const compareFunc = (first, second) => {
|
||||
if (power_user.sort_order == 'random') {
|
||||
return Math.random() > 0.5 ? 1 : -1;
|
||||
}
|
||||
|
||||
const a = first[power_user.sort_field];
|
||||
const b = second[power_user.sort_field];
|
||||
|
||||
@ -1853,6 +1897,11 @@ function sortEntitiesList(entities) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (power_user.sort_order === 'random') {
|
||||
shuffle(entities);
|
||||
return;
|
||||
}
|
||||
|
||||
entities.sort((a, b) => {
|
||||
if (a.type === 'tag' && b.type !== 'tag') {
|
||||
return -1;
|
||||
@ -1866,11 +1915,26 @@ function sortEntitiesList(entities) {
|
||||
});
|
||||
}
|
||||
|
||||
async function saveTheme() {
|
||||
const name = await callPopup('Enter a theme preset name:', 'input');
|
||||
/**
|
||||
* Updates the current UI theme file.
|
||||
*/
|
||||
async function updateTheme() {
|
||||
await saveTheme(power_user.theme);
|
||||
toastr.success('Theme saved.');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
/**
|
||||
* Saves the current theme to the server.
|
||||
* @param {string|undefined} name Theme name. If undefined, a popup will be shown to enter a name.
|
||||
* @returns {Promise<void>} A promise that resolves when the theme is saved.
|
||||
*/
|
||||
async function saveTheme(name = undefined) {
|
||||
if (typeof name !== 'string') {
|
||||
name = await callPopup('Enter a theme preset name:', 'input', power_user.theme);
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@ -1905,6 +1969,8 @@ async function saveTheme() {
|
||||
hotswap_enabled: power_user.hotswap_enabled,
|
||||
custom_css: power_user.custom_css,
|
||||
bogus_folders: power_user.bogus_folders,
|
||||
reduced_motion: power_user.reduced_motion,
|
||||
compact_input_area: power_user.compact_input_area,
|
||||
};
|
||||
|
||||
const response = await fetch('/savetheme', {
|
||||
@ -2678,6 +2744,12 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#streaming_fps').on('input', function () {
|
||||
power_user.streaming_fps = Number($('#streaming_fps').val());
|
||||
$('#streaming_fps_counter').val(power_user.streaming_fps);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('input[name="font_scale"]').on('input', async function (e) {
|
||||
power_user.font_scale = Number(e.target.value);
|
||||
$('#font_scale_counter').val(power_user.font_scale);
|
||||
@ -2771,7 +2843,8 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#ui-preset-save-button').on('click', saveTheme);
|
||||
$('#ui-preset-save-button').on('click', () => saveTheme());
|
||||
$('#ui-preset-update-button').on('click', () => updateTheme());
|
||||
$('#movingui-preset-save-button').on('click', saveMovingUI);
|
||||
|
||||
$('#never_resize_avatars').on('input', function () {
|
||||
@ -2862,6 +2935,7 @@ $(document).ready(() => {
|
||||
$('#tokenizer').on('change', function () {
|
||||
const value = $(this).find(':selected').val();
|
||||
power_user.tokenizer = Number(value);
|
||||
BIAS_CACHE.clear();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Trigger character editor re-tokenize
|
||||
@ -3111,6 +3185,20 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#reduced_motion').on('input', function () {
|
||||
power_user.reduced_motion = !!$(this).prop('checked');
|
||||
localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
|
||||
switchReducedMotion();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#compact_input_area').on('input', function () {
|
||||
power_user.compact_input_area = !!$(this).prop('checked');
|
||||
localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
|
||||
switchCompactInputArea();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('click', '#debug_table [data-debug-function]', function () {
|
||||
const functionId = $(this).data('debug-function');
|
||||
const functionRecord = debug_functions.find(f => f.functionId === functionId);
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
nai_settings,
|
||||
novelai_setting_names,
|
||||
novelai_settings,
|
||||
online_status,
|
||||
saveSettingsDebounced,
|
||||
this_chid,
|
||||
} from '../script.js';
|
||||
@ -19,6 +20,7 @@ import { groups, selected_group } from './group-chats.js';
|
||||
import { instruct_presets } from './instruct-mode.js';
|
||||
import { kai_settings } from './kai-settings.js';
|
||||
import { context_presets, getContextSettings, power_user } from './power-user.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import {
|
||||
textgenerationwebui_preset_names,
|
||||
textgenerationwebui_presets,
|
||||
@ -28,6 +30,9 @@ import { download, parseJsonFile, waitUntilCondition } from './utils.js';
|
||||
|
||||
const presetManagers = {};
|
||||
|
||||
/**
|
||||
* Automatically select a preset for current API based on character or group name.
|
||||
*/
|
||||
function autoSelectPreset() {
|
||||
const presetManager = getPresetManager();
|
||||
|
||||
@ -57,7 +62,12 @@ function autoSelectPreset() {
|
||||
}
|
||||
}
|
||||
|
||||
function getPresetManager(apiId) {
|
||||
/**
|
||||
* Gets a preset manager by API id.
|
||||
* @param {string} apiId API id
|
||||
* @returns {PresetManager} Preset manager
|
||||
*/
|
||||
function getPresetManager(apiId = '') {
|
||||
if (!apiId) {
|
||||
apiId = main_api == 'koboldhorde' ? 'kobold' : main_api;
|
||||
}
|
||||
@ -69,6 +79,9 @@ function getPresetManager(apiId) {
|
||||
return presetManagers[apiId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers preset managers for all select elements with data-preset-manager-for attribute.
|
||||
*/
|
||||
function registerPresetManagers() {
|
||||
$('select[data-preset-manager-for]').each((_, e) => {
|
||||
const forData = $(e).data('preset-manager-for');
|
||||
@ -85,21 +98,46 @@ class PresetManager {
|
||||
this.apiId = apiId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all preset names.
|
||||
* @returns {string[]} List of preset names
|
||||
*/
|
||||
getAllPresets() {
|
||||
return $(this.select).find('option').map((_, el) => el.text).toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a preset by name.
|
||||
* @param {string} name Preset name
|
||||
* @returns {any} Preset value
|
||||
*/
|
||||
findPreset(name) {
|
||||
return $(this.select).find(`option:contains(${name})`).val();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the selected preset value.
|
||||
* @returns {any} Selected preset value
|
||||
*/
|
||||
getSelectedPreset() {
|
||||
return $(this.select).find('option:selected').val();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the selected preset name.
|
||||
* @returns {string} Selected preset name
|
||||
*/
|
||||
getSelectedPresetName() {
|
||||
return $(this.select).find('option:selected').text();
|
||||
}
|
||||
|
||||
selectPreset(preset) {
|
||||
$(this.select).find(`option[value=${preset}]`).prop('selected', true);
|
||||
$(this.select).val(preset).trigger('change');
|
||||
/**
|
||||
* Selects a preset by option value.
|
||||
* @param {string} value Preset option value
|
||||
*/
|
||||
selectPreset(value) {
|
||||
$(this.select).find(`option[value=${value}]`).prop('selected', true);
|
||||
$(this.select).val(value).trigger('change');
|
||||
}
|
||||
|
||||
async updatePreset() {
|
||||
@ -265,8 +303,14 @@ class PresetManager {
|
||||
'model_novel',
|
||||
'streaming_kobold',
|
||||
'enabled',
|
||||
'bind_to_context',
|
||||
'seed',
|
||||
'legacy_api',
|
||||
'mancer_model',
|
||||
'togetherai_model',
|
||||
'ollama_model',
|
||||
'server_urls',
|
||||
'type',
|
||||
];
|
||||
const settings = Object.assign({}, getSettingsByApiId(this.apiId));
|
||||
|
||||
@ -285,7 +329,7 @@ class PresetManager {
|
||||
}
|
||||
|
||||
async deleteCurrentPreset() {
|
||||
const { preset_names } = this.getPresetList();
|
||||
const { preset_names, presets } = this.getPresetList();
|
||||
const value = this.getSelectedPreset();
|
||||
const nameToDelete = this.getSelectedPresetName();
|
||||
|
||||
@ -297,7 +341,9 @@ class PresetManager {
|
||||
$(this.select).find(`option[value="${value}"]`).remove();
|
||||
|
||||
if (this.isKeyedApi()) {
|
||||
preset_names.splice(preset_names.indexOf(value), 1);
|
||||
const index = preset_names.indexOf(nameToDelete);
|
||||
preset_names.splice(index, 1);
|
||||
presets.splice(index, 1);
|
||||
} else {
|
||||
delete preset_names[nameToDelete];
|
||||
}
|
||||
@ -334,11 +380,91 @@ class PresetManager {
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
await waitUntilCondition(() => eventSource !== undefined);
|
||||
/**
|
||||
* Selects a preset by name for current API.
|
||||
* @param {any} _ Named arguments
|
||||
* @param {string} name Unnamed arguments
|
||||
* @returns {Promise<string>} Selected or current preset name
|
||||
*/
|
||||
async function presetCommandCallback(_, name) {
|
||||
const shouldReconnect = online_status !== 'no_connection';
|
||||
const presetManager = getPresetManager();
|
||||
const allPresets = presetManager.getAllPresets();
|
||||
const currentPreset = presetManager.getSelectedPresetName();
|
||||
|
||||
if (!presetManager) {
|
||||
console.debug(`Preset Manager not found for API: ${main_api}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
console.log('No name provided for /preset command, using current preset');
|
||||
return currentPreset;
|
||||
}
|
||||
|
||||
if (!Array.isArray(allPresets) || allPresets.length === 0) {
|
||||
console.log(`No presets found for API: ${main_api}`);
|
||||
return currentPreset;
|
||||
}
|
||||
|
||||
// Find exact match
|
||||
const exactMatch = allPresets.find(p => p.toLowerCase().trim() === name.toLowerCase().trim());
|
||||
|
||||
if (exactMatch) {
|
||||
console.log('Found exact preset match', exactMatch);
|
||||
|
||||
if (currentPreset !== exactMatch) {
|
||||
const presetValue = presetManager.findPreset(exactMatch);
|
||||
|
||||
if (presetValue) {
|
||||
presetManager.selectPreset(presetValue);
|
||||
shouldReconnect && await waitForConnection();
|
||||
}
|
||||
}
|
||||
|
||||
return exactMatch;
|
||||
} else {
|
||||
// Find fuzzy match
|
||||
const fuse = new Fuse(allPresets);
|
||||
const fuzzyMatch = fuse.search(name);
|
||||
|
||||
if (!fuzzyMatch.length) {
|
||||
console.warn(`WARN: Preset found with name ${name}`);
|
||||
return currentPreset;
|
||||
}
|
||||
|
||||
const fuzzyPresetName = fuzzyMatch[0].item;
|
||||
const fuzzyPresetValue = presetManager.findPreset(fuzzyPresetName);
|
||||
|
||||
if (fuzzyPresetValue) {
|
||||
console.log('Found fuzzy preset match', fuzzyPresetName);
|
||||
|
||||
if (currentPreset !== fuzzyPresetName) {
|
||||
presetManager.selectPreset(fuzzyPresetValue);
|
||||
shouldReconnect && await waitForConnection();
|
||||
}
|
||||
}
|
||||
|
||||
return fuzzyPresetName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for API connection to be established.
|
||||
*/
|
||||
async function waitForConnection() {
|
||||
try {
|
||||
await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
|
||||
} catch {
|
||||
console.log('Timeout waiting for API to connect');
|
||||
}
|
||||
}
|
||||
|
||||
export async function initPresetManager() {
|
||||
eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
|
||||
registerPresetManagers();
|
||||
registerSlashCommand('preset', presetCommandCallback, [], '<span class="monospace">(name)</span> – sets a preset by name for the current API', true, true);
|
||||
|
||||
$(document).on('click', '[data-preset-manager-update]', async function () {
|
||||
const apiId = $(this).data('preset-manager-update');
|
||||
const presetManager = getPresetManager(apiId);
|
||||
@ -440,7 +566,7 @@ jQuery(async () => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('click', '[data-preset-manager-restore]', async function() {
|
||||
$(document).on('click', '[data-preset-manager-restore]', async function () {
|
||||
const apiId = $(this).data('preset-manager-restore');
|
||||
const presetManager = getPresetManager(apiId);
|
||||
|
||||
@ -490,4 +616,4 @@ jQuery(async () => {
|
||||
toastr.success('Preset restored');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -12,8 +12,11 @@ export const SECRET_KEYS = {
|
||||
SCALE: 'api_key_scale',
|
||||
AI21: 'api_key_ai21',
|
||||
SCALE_COOKIE: 'scale_cookie',
|
||||
PALM: 'api_key_palm',
|
||||
MAKERSUITE: 'api_key_makersuite',
|
||||
SERPAPI: 'api_key_serpapi',
|
||||
MISTRALAI: 'api_key_mistralai',
|
||||
TOGETHERAI: 'api_key_togetherai',
|
||||
CUSTOM: 'api_key_custom',
|
||||
};
|
||||
|
||||
const INPUT_MAP = {
|
||||
@ -26,9 +29,12 @@ const INPUT_MAP = {
|
||||
[SECRET_KEYS.SCALE]: '#api_key_scale',
|
||||
[SECRET_KEYS.AI21]: '#api_key_ai21',
|
||||
[SECRET_KEYS.SCALE_COOKIE]: '#scale_cookie',
|
||||
[SECRET_KEYS.PALM]: '#api_key_palm',
|
||||
[SECRET_KEYS.MAKERSUITE]: '#api_key_makersuite',
|
||||
[SECRET_KEYS.APHRODITE]: '#api_key_aphrodite',
|
||||
[SECRET_KEYS.TABBY]: '#api_key_tabby',
|
||||
[SECRET_KEYS.MISTRALAI]: '#api_key_mistralai',
|
||||
[SECRET_KEYS.CUSTOM]: '#api_key_custom',
|
||||
[SECRET_KEYS.TOGETHERAI]: '#api_key_togetherai',
|
||||
};
|
||||
|
||||
async function clearSecret() {
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
main_api,
|
||||
name1,
|
||||
reloadCurrentChat,
|
||||
replaceBiasMarkup,
|
||||
removeMacros,
|
||||
saveChatConditional,
|
||||
sendMessageAsUser,
|
||||
sendSystemMessage,
|
||||
@ -186,6 +186,7 @@ parser.addCommand('trimend', trimEndCallback, [], '<span class="monospace">(text
|
||||
parser.addCommand('inject', injectCallback, [], '<span class="monospace">id=injectId (position=before/after/chat depth=number [text])</span> – injects a text into the LLM prompt for the current chat. Requires a unique injection ID. Positions: "before" main prompt, "after" main prompt, in-"chat" (default: after). Depth: injection depth for the prompt (default: 4).', true, true);
|
||||
parser.addCommand('listinjects', listInjectsCallback, [], ' – lists all script injections for the current chat.', true, true);
|
||||
parser.addCommand('flushinjects', flushInjectsCallback, [], ' – removes all script injections for the current chat.', true, true);
|
||||
parser.addCommand('tokens', (_, text) => getTokenCount(text), [], '<span class="monospace">(text)</span> – counts the number of tokens in the text.', true, true);
|
||||
registerVariableCommands();
|
||||
|
||||
const NARRATOR_NAME_KEY = 'narrator_name';
|
||||
@ -841,6 +842,38 @@ async function unhideMessageCallback(_, arg) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Copium for running group actions when the member is offscreen.
|
||||
* @param {number} chid - character ID
|
||||
* @param {string} action - one of 'enable', 'disable', 'up', 'down', 'view', 'remove'
|
||||
* @returns {void}
|
||||
*/
|
||||
function performGroupMemberAction(chid, action) {
|
||||
const memberSelector = `.group_member[chid="${chid}"]`;
|
||||
// Do not optimize. Paginator gets recreated on every action
|
||||
const paginationSelector = '#rm_group_members_pagination';
|
||||
const pageSizeSelector = '#rm_group_members_pagination select';
|
||||
let wasOffscreen = false;
|
||||
let paginationValue = null;
|
||||
let pageValue = null;
|
||||
|
||||
if ($(memberSelector).length === 0) {
|
||||
wasOffscreen = true;
|
||||
paginationValue = Number($(pageSizeSelector).val());
|
||||
pageValue = $(paginationSelector).pagination('getCurrentPageNum');
|
||||
$(pageSizeSelector).val($(pageSizeSelector).find('option').last().val()).trigger('change');
|
||||
}
|
||||
|
||||
$(memberSelector).find(`[data-action="${action}"]`).trigger('click');
|
||||
|
||||
if (wasOffscreen) {
|
||||
$(pageSizeSelector).val(paginationValue).trigger('change');
|
||||
if ($(paginationSelector).length) {
|
||||
$(paginationSelector).pagination('go', pageValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function disableGroupMemberCallback(_, arg) {
|
||||
if (!selected_group) {
|
||||
toastr.warning('Cannot run /disable command outside of a group chat.');
|
||||
@ -854,7 +887,7 @@ async function disableGroupMemberCallback(_, arg) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$(`.group_member[chid="${chid}"] [data-action="disable"]`).trigger('click');
|
||||
performGroupMemberAction(chid, 'disable');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -871,7 +904,7 @@ async function enableGroupMemberCallback(_, arg) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$(`.group_member[chid="${chid}"] [data-action="enable"]`).trigger('click');
|
||||
performGroupMemberAction(chid, 'enable');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -888,7 +921,7 @@ async function moveGroupMemberUpCallback(_, arg) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$(`.group_member[chid="${chid}"] [data-action="up"]`).trigger('click');
|
||||
performGroupMemberAction(chid, 'up');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -905,7 +938,7 @@ async function moveGroupMemberDownCallback(_, arg) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$(`.group_member[chid="${chid}"] [data-action="down"]`).trigger('click');
|
||||
performGroupMemberAction(chid, 'down');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -927,7 +960,7 @@ async function peekCallback(_, arg) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$(`.group_member[chid="${chid}"] [data-action="view"]`).trigger('click');
|
||||
performGroupMemberAction(chid, 'view');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -949,7 +982,7 @@ async function removeGroupMemberCallback(_, arg) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$(`.group_member[chid="${chid}"] [data-action="remove"]`).trigger('click');
|
||||
performGroupMemberAction(chid, 'remove');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -1227,7 +1260,7 @@ export async function sendMessageAs(args, text) {
|
||||
|
||||
// Messages that do nothing but set bias will be hidden from the context
|
||||
const bias = extractMessageBias(mesText);
|
||||
const isSystem = replaceBiasMarkup(mesText).trim().length === 0;
|
||||
const isSystem = bias && !removeMacros(mesText).length;
|
||||
|
||||
const character = characters.find(x => x.name === name);
|
||||
let force_avatar, original_avatar;
|
||||
@ -1280,7 +1313,7 @@ export async function sendNarratorMessage(args, text) {
|
||||
const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
|
||||
// Messages that do nothing but set bias will be hidden from the context
|
||||
const bias = extractMessageBias(text);
|
||||
const isSystem = replaceBiasMarkup(text).trim().length === 0;
|
||||
const isSystem = bias && !removeMacros(text).length;
|
||||
|
||||
const message = {
|
||||
name: name,
|
||||
|
77
public/scripts/sse-stream.js
Normal file
77
public/scripts/sse-stream.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* A stream which handles Server-Sent Events from a binary ReadableStream like you get from the fetch API.
|
||||
*/
|
||||
class EventSourceStream {
|
||||
constructor() {
|
||||
const decoder = new TextDecoderStream('utf-8');
|
||||
|
||||
let streamBuffer = '';
|
||||
let lastEventId = '';
|
||||
|
||||
function processChunk(controller) {
|
||||
// Events are separated by two newlines
|
||||
const events = streamBuffer.split(/\r\n\r\n|\r\r|\n\n/g);
|
||||
if (events.length === 0) return;
|
||||
|
||||
// The leftover text to remain in the buffer is whatever doesn't have two newlines after it. If the buffer ended
|
||||
// with two newlines, this will be an empty string.
|
||||
streamBuffer = events.pop();
|
||||
|
||||
for (const eventChunk of events) {
|
||||
let eventType = '';
|
||||
// Split up by single newlines.
|
||||
const lines = eventChunk.split(/\n|\r|\r\n/g);
|
||||
let eventData = '';
|
||||
for (const line of lines) {
|
||||
const lineMatch = /([^:]+)(?:: ?(.*))?/.exec(line);
|
||||
if (lineMatch) {
|
||||
const field = lineMatch[1];
|
||||
const value = lineMatch[2] || '';
|
||||
|
||||
switch (field) {
|
||||
case 'event':
|
||||
eventType = value;
|
||||
break;
|
||||
case 'data':
|
||||
eventData += value;
|
||||
eventData += '\n';
|
||||
break;
|
||||
case 'id':
|
||||
// The ID field cannot contain null, per the spec
|
||||
if (!value.includes('\0')) lastEventId = value;
|
||||
break;
|
||||
// We do nothing for the `delay` type, and other types are explicitly ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/server-sent-events.html#dispatchMessage
|
||||
// Skip the event if the data buffer is the empty string.
|
||||
if (eventData === '') continue;
|
||||
|
||||
if (eventData[eventData.length - 1] === '\n') {
|
||||
eventData = eventData.slice(0, -1);
|
||||
}
|
||||
|
||||
// Trim the *last* trailing newline only.
|
||||
const event = new MessageEvent(eventType || 'message', { data: eventData, lastEventId });
|
||||
controller.enqueue(event);
|
||||
}
|
||||
}
|
||||
|
||||
const sseStream = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
streamBuffer += chunk;
|
||||
processChunk(controller);
|
||||
},
|
||||
});
|
||||
|
||||
decoder.readable.pipeThrough(sseStream);
|
||||
|
||||
this.readable = sseStream.readable;
|
||||
this.writable = decoder.writable;
|
||||
}
|
||||
}
|
||||
|
||||
export default EventSourceStream;
|
@ -17,6 +17,7 @@
|
||||
<li><tt>{{char}}</tt> – the Character's name</li>
|
||||
<li><tt>{{lastMessage}}</tt> - the text of the latest chat message.</li>
|
||||
<li><tt>{{lastMessageId}}</tt> – index # of the latest chat message. Useful for slash command batching.</li>
|
||||
<li><tt>{{firstIncludedMessageId}}</tt> - the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</li>
|
||||
<li><tt>{{currentSwipeId}}</tt> – the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</li>
|
||||
<li><tt>{{lastSwipeId}}</tt> – the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</li>
|
||||
<li><tt>{{// (note)}}</tt> – you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<h3>Confused or lost?</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="note-link-span">?</span> - click these icons!
|
||||
<span class="note-link-span"><a class="fa-solid fa-circle-question" target="_blank" href="https://docs.sillytavern.app/"></a></span> - click these icons!
|
||||
</li>
|
||||
<li>
|
||||
Enter <code>/?</code> in the chat bar
|
||||
|
202
public/scripts/textgen-models.js
Normal file
202
public/scripts/textgen-models.js
Normal file
@ -0,0 +1,202 @@
|
||||
import { callPopup, getRequestHeaders, setGenerationParamsFromPreset } from '../script.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js';
|
||||
|
||||
let mancerModels = [];
|
||||
let togetherModels = [];
|
||||
|
||||
export async function loadOllamaModels(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Invalid Ollama models data', data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.find(x => x.id === textgen_settings.ollama_model)) {
|
||||
textgen_settings.ollama_model = data[0]?.id || '';
|
||||
}
|
||||
|
||||
$('#ollama_model').empty();
|
||||
for (const model of data) {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.id;
|
||||
option.text = model.name;
|
||||
option.selected = model.id === textgen_settings.ollama_model;
|
||||
$('#ollama_model').append(option);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadTogetherAIModels(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Invalid Together AI models data', data);
|
||||
return;
|
||||
}
|
||||
|
||||
togetherModels = data;
|
||||
|
||||
if (!data.find(x => x.name === textgen_settings.togetherai_model)) {
|
||||
textgen_settings.togetherai_model = data[0]?.name || '';
|
||||
}
|
||||
|
||||
$('#model_togetherai_select').empty();
|
||||
for (const model of data) {
|
||||
// Hey buddy, I think you've got the wrong door.
|
||||
if (model.display_type === 'image') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = model.name;
|
||||
option.text = model.display_name;
|
||||
option.selected = model.name === textgen_settings.togetherai_model;
|
||||
$('#model_togetherai_select').append(option);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadMancerModels(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Invalid Mancer models data', data);
|
||||
return;
|
||||
}
|
||||
|
||||
mancerModels = data;
|
||||
|
||||
if (!data.find(x => x.id === textgen_settings.mancer_model)) {
|
||||
textgen_settings.mancer_model = data[0]?.id || '';
|
||||
}
|
||||
|
||||
$('#mancer_model').empty();
|
||||
for (const model of data) {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.id;
|
||||
option.text = model.name;
|
||||
option.selected = model.id === textgen_settings.mancer_model;
|
||||
$('#mancer_model').append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function onMancerModelSelect() {
|
||||
const modelId = String($('#mancer_model').val());
|
||||
textgen_settings.mancer_model = modelId;
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
|
||||
const limits = mancerModels.find(x => x.id === modelId)?.limits;
|
||||
setGenerationParamsFromPreset({ max_length: limits.context, genamt: limits.completion });
|
||||
}
|
||||
|
||||
function onTogetherModelSelect() {
|
||||
const modelName = String($('#model_togetherai_select').val());
|
||||
textgen_settings.togetherai_model = modelName;
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
const model = togetherModels.find(x => x.name === modelName);
|
||||
setGenerationParamsFromPreset({ max_length: model.context_length });
|
||||
}
|
||||
|
||||
function onOllamaModelSelect() {
|
||||
const modelId = String($('#ollama_model').val());
|
||||
textgen_settings.ollama_model = modelId;
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
}
|
||||
|
||||
function getMancerModelTemplate(option) {
|
||||
const model = mancerModels.find(x => x.id === option?.element?.value);
|
||||
|
||||
if (!option.id || !model) {
|
||||
return option.text;
|
||||
}
|
||||
|
||||
const creditsPerPrompt = (model.limits?.context - model.limits?.completion) * model.pricing?.prompt;
|
||||
const creditsPerCompletion = model.limits?.completion * model.pricing?.completion;
|
||||
const creditsTotal = Math.round(creditsPerPrompt + creditsPerCompletion).toFixed(0);
|
||||
|
||||
return $((`
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | <span>${model.limits?.context} ctx</span> / <span>${model.limits?.completion} res</span> | <small>Credits per request (max): ${creditsTotal}</small></div>
|
||||
</div>
|
||||
`));
|
||||
}
|
||||
|
||||
function getTogetherModelTemplate(option) {
|
||||
const model = togetherModels.find(x => x.name === option?.element?.value);
|
||||
|
||||
if (!option.id || !model) {
|
||||
return option.text;
|
||||
}
|
||||
|
||||
return $((`
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | <span>${model.context_length || '???'} tokens</span></div>
|
||||
<div><small>${DOMPurify.sanitize(model.description)}</small></div>
|
||||
</div>
|
||||
`));
|
||||
}
|
||||
|
||||
async function downloadOllamaModel() {
|
||||
try {
|
||||
const serverUrl = textgen_settings.server_urls[textgen_types.OLLAMA];
|
||||
|
||||
if (!serverUrl) {
|
||||
toastr.info('Please connect to an Ollama server first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const html = `Enter a model tag, for example <code>llama2:latest</code>.<br>
|
||||
See <a target="_blank" href="https://ollama.ai/library">Library</a> for available models.`;
|
||||
const name = await callPopup(html, 'input', '', { okButton: 'Download' });
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
toastr.info('Download may take a while, please wait...', 'Working on it');
|
||||
|
||||
const response = await fetch('/api/backends/text-completions/ollama/download', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
api_server: serverUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// Force refresh the model list
|
||||
toastr.success('Download complete. Please select the model from the dropdown.');
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toastr.error('Failed to download Ollama model. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
$('#mancer_model').on('change', onMancerModelSelect);
|
||||
$('#model_togetherai_select').on('change', onTogetherModelSelect);
|
||||
$('#ollama_model').on('change', onOllamaModelSelect);
|
||||
$('#ollama_download_model').on('click', downloadOllamaModel);
|
||||
|
||||
if (!isMobile()) {
|
||||
$('#mancer_model').select2({
|
||||
placeholder: 'Select a model',
|
||||
searchInputPlaceholder: 'Search models...',
|
||||
searchInputCssClass: 'text_pole',
|
||||
width: '100%',
|
||||
templateResult: getMancerModelTemplate,
|
||||
});
|
||||
$('#model_togetherai_select').select2({
|
||||
placeholder: 'Select a model',
|
||||
searchInputPlaceholder: 'Search models...',
|
||||
searchInputCssClass: 'text_pole',
|
||||
width: '100%',
|
||||
templateResult: getTogetherModelTemplate,
|
||||
});
|
||||
$('#ollama_model').select2({
|
||||
placeholder: 'Select a model',
|
||||
searchInputPlaceholder: 'Search models...',
|
||||
searchInputCssClass: 'text_pole',
|
||||
width: '100%',
|
||||
});
|
||||
}
|
||||
});
|
@ -1,5 +1,4 @@
|
||||
import {
|
||||
api_server_textgenerationwebui,
|
||||
getRequestHeaders,
|
||||
getStoppingStrings,
|
||||
max_context,
|
||||
@ -9,11 +8,13 @@ import {
|
||||
setOnlineStatus,
|
||||
substituteParams,
|
||||
} from '../script.js';
|
||||
import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
|
||||
|
||||
import {
|
||||
power_user,
|
||||
registerDebugFunction,
|
||||
} from './power-user.js';
|
||||
import EventSourceStream from './sse-stream.js';
|
||||
import { SENTENCEPIECE_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
|
||||
import { getSortableDelay, onlyUnique } from './utils.js';
|
||||
|
||||
@ -30,15 +31,29 @@ export const textgen_types = {
|
||||
APHRODITE: 'aphrodite',
|
||||
TABBY: 'tabby',
|
||||
KOBOLDCPP: 'koboldcpp',
|
||||
TOGETHERAI: 'togetherai',
|
||||
LLAMACPP: 'llamacpp',
|
||||
OLLAMA: 'ollama',
|
||||
};
|
||||
|
||||
const { MANCER, APHRODITE } = textgen_types;
|
||||
const { MANCER, APHRODITE, TOGETHERAI, OOBA, OLLAMA, LLAMACPP } = textgen_types;
|
||||
const BIAS_KEY = '#textgenerationwebui_api-settings';
|
||||
|
||||
// Maybe let it be configurable in the future?
|
||||
// (7 days later) The future has come.
|
||||
const MANCER_SERVER_KEY = 'mancer_server';
|
||||
const MANCER_SERVER_DEFAULT = 'https://neuro.mancer.tech';
|
||||
export let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEFAULT;
|
||||
let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEFAULT;
|
||||
let TOGETHERAI_SERVER = 'https://api.together.xyz';
|
||||
|
||||
const SERVER_INPUTS = {
|
||||
[textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
|
||||
[textgen_types.APHRODITE]: '#aphrodite_api_url_text',
|
||||
[textgen_types.TABBY]: '#tabby_api_url_text',
|
||||
[textgen_types.KOBOLDCPP]: '#koboldcpp_api_url_text',
|
||||
[textgen_types.LLAMACPP]: '#llamacpp_api_url_text',
|
||||
[textgen_types.OLLAMA]: '#ollama_api_url_text',
|
||||
};
|
||||
|
||||
const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5];
|
||||
const settings = {
|
||||
@ -88,9 +103,13 @@ const settings = {
|
||||
//prompt_log_probs_aphrodite: 0,
|
||||
type: textgen_types.OOBA,
|
||||
mancer_model: 'mytholite',
|
||||
togetherai_model: 'Gryphe/MythoMax-L2-13b',
|
||||
ollama_model: '',
|
||||
legacy_api: false,
|
||||
sampler_order: KOBOLDCPP_ORDER,
|
||||
logit_bias: [],
|
||||
n: 1,
|
||||
server_urls: {},
|
||||
};
|
||||
|
||||
export let textgenerationwebui_banned_in_macros = [];
|
||||
@ -143,8 +162,40 @@ const setting_names = [
|
||||
//'prompt_log_probs_aphrodite'
|
||||
'sampler_order',
|
||||
'n',
|
||||
'logit_bias',
|
||||
];
|
||||
|
||||
export function validateTextGenUrl() {
|
||||
const selector = SERVER_INPUTS[settings.type];
|
||||
|
||||
if (!selector) {
|
||||
return;
|
||||
}
|
||||
|
||||
const control = $(selector);
|
||||
const url = String(control.val()).trim();
|
||||
const formattedUrl = formatTextGenURL(url);
|
||||
|
||||
if (!formattedUrl) {
|
||||
toastr.error('Enter a valid API URL', 'Text Completion API');
|
||||
return;
|
||||
}
|
||||
|
||||
control.val(formattedUrl);
|
||||
}
|
||||
|
||||
export function getTextGenServer() {
|
||||
if (settings.type === MANCER) {
|
||||
return MANCER_SERVER;
|
||||
}
|
||||
|
||||
if (settings.type === TOGETHERAI) {
|
||||
return TOGETHERAI_SERVER;
|
||||
}
|
||||
|
||||
return settings.server_urls[settings.type] ?? '';
|
||||
}
|
||||
|
||||
async function selectPreset(name) {
|
||||
const preset = textgenerationwebui_presets[textgenerationwebui_preset_names.indexOf(name)];
|
||||
|
||||
@ -158,13 +209,15 @@ async function selectPreset(name) {
|
||||
setSettingByName(name, value, true);
|
||||
}
|
||||
setGenerationParamsFromPreset(preset);
|
||||
BIAS_CACHE.delete(BIAS_KEY);
|
||||
displayLogitBias(preset.logit_bias, BIAS_KEY);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function formatTextGenURL(value) {
|
||||
try {
|
||||
// Mancer doesn't need any formatting (it's hardcoded)
|
||||
if (settings.type === MANCER) {
|
||||
// Mancer/Together doesn't need any formatting (it's hardcoded)
|
||||
if (settings.type === MANCER || settings.type === TOGETHERAI) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -239,11 +292,62 @@ function getCustomTokenBans() {
|
||||
return result.filter(onlyUnique).map(x => String(x)).join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates logit bias object from the logit bias list.
|
||||
* @returns {object} Logit bias object
|
||||
*/
|
||||
function calculateLogitBias() {
|
||||
if (!Array.isArray(settings.logit_bias) || settings.logit_bias.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const tokenizer = SENTENCEPIECE_TOKENIZERS.includes(power_user.tokenizer) ? power_user.tokenizer : tokenizers.LLAMA;
|
||||
const result = {};
|
||||
|
||||
/**
|
||||
* Adds bias to the logit bias object.
|
||||
* @param {number} bias
|
||||
* @param {number[]} sequence
|
||||
* @returns {object} Accumulated logit bias object
|
||||
*/
|
||||
function addBias(bias, sequence) {
|
||||
if (sequence.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const logit of sequence) {
|
||||
const key = String(logit);
|
||||
result[key] = bias;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getLogitBiasListResult(settings.logit_bias, tokenizer, addBias);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadTextGenSettings(data, loadedSettings) {
|
||||
textgenerationwebui_presets = convertPresets(data.textgenerationwebui_presets);
|
||||
textgenerationwebui_preset_names = data.textgenerationwebui_preset_names ?? [];
|
||||
Object.assign(settings, loadedSettings.textgenerationwebui_settings ?? {});
|
||||
|
||||
if (loadedSettings.api_server_textgenerationwebui) {
|
||||
for (const type of Object.keys(SERVER_INPUTS)) {
|
||||
settings.server_urls[type] = loadedSettings.api_server_textgenerationwebui;
|
||||
}
|
||||
delete loadedSettings.api_server_textgenerationwebui;
|
||||
}
|
||||
|
||||
for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
|
||||
const control = $(selector);
|
||||
control.val(settings.server_urls[type] ?? '').on('input', function () {
|
||||
settings.server_urls[type] = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
|
||||
if (loadedSettings.api_use_mancer_webui) {
|
||||
settings.type = MANCER;
|
||||
}
|
||||
@ -266,6 +370,8 @@ function loadTextGenSettings(data, loadedSettings) {
|
||||
|
||||
$('#textgen_type').val(settings.type);
|
||||
showTypeSpecificControls(settings.type);
|
||||
BIAS_CACHE.delete(BIAS_KEY);
|
||||
displayLogitBias(settings.logit_bias, BIAS_KEY);
|
||||
//this is needed because showTypeSpecificControls() does not handle NOT declarations
|
||||
if (settings.type === textgen_types.APHRODITE) {
|
||||
$('[data-forAphro=False]').each(function () {
|
||||
@ -287,19 +393,6 @@ function loadTextGenSettings(data, loadedSettings) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getTextGenUrlSourceId() {
|
||||
switch (settings.type) {
|
||||
case textgen_types.OOBA:
|
||||
return '#textgenerationwebui_api_url_text';
|
||||
case textgen_types.APHRODITE:
|
||||
return '#aphrodite_api_url_text';
|
||||
case textgen_types.TABBY:
|
||||
return '#tabby_api_url_text';
|
||||
case textgen_types.KOBOLDCPP:
|
||||
return '#koboldcpp_api_url_text';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the sampler items by the given order.
|
||||
* @param {any[]} orderArray Sampler order array.
|
||||
@ -369,9 +462,13 @@ jQuery(function () {
|
||||
|
||||
showTypeSpecificControls(type);
|
||||
setOnlineStatus('no_connection');
|
||||
BIAS_CACHE.delete(BIAS_KEY);
|
||||
|
||||
$('#main_api').trigger('change');
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
|
||||
if (!SERVER_INPUTS[type] || settings.server_urls[type]) {
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
@ -411,15 +508,20 @@ jQuery(function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
|
||||
$('#textgen_logit_bias_new_entry').on('click', () => createNewLogitBiasEntry(settings.logit_bias, BIAS_KEY));
|
||||
});
|
||||
|
||||
function showTypeSpecificControls(type) {
|
||||
$('[data-tg-type]').each(function () {
|
||||
const tgType = $(this).attr('data-tg-type');
|
||||
if (tgType == type) {
|
||||
$(this).show();
|
||||
} else {
|
||||
$(this).hide();
|
||||
const tgTypes = $(this).attr('data-tg-type').split(',');
|
||||
for (const tgType of tgTypes) {
|
||||
if (tgType === type || tgType == 'all') {
|
||||
$(this).show();
|
||||
return;
|
||||
} else {
|
||||
$(this).hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -436,6 +538,11 @@ function setSettingByName(setting, value, trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('logit_bias' === setting) {
|
||||
settings.logit_bias = Array.isArray(value) ? value : [];
|
||||
return;
|
||||
}
|
||||
|
||||
const isCheckbox = $(`#${setting}_textgenerationwebui`).attr('type') == 'checkbox';
|
||||
const isText = $(`#${setting}_textgenerationwebui`).attr('type') == 'text' || $(`#${setting}_textgenerationwebui`).is('textarea');
|
||||
if (isCheckbox) {
|
||||
@ -467,7 +574,7 @@ function setSettingByName(setting, value, trigger) {
|
||||
async function generateTextGenWithStreaming(generate_data, signal) {
|
||||
generate_data.stream = true;
|
||||
|
||||
const response = await fetch('/api/textgenerationwebui/generate', {
|
||||
const response = await fetch('/api/backends/text-completions/generate', {
|
||||
headers: {
|
||||
...getRequestHeaders(),
|
||||
},
|
||||
@ -476,68 +583,50 @@ async function generateTextGenWithStreaming(generate_data, signal) {
|
||||
signal: signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
tryParseStreamingError(response, await response.text());
|
||||
throw new Error(`Got response status ${response.status}`);
|
||||
}
|
||||
|
||||
const eventStream = new EventSourceStream();
|
||||
response.body.pipeThrough(eventStream);
|
||||
const reader = eventStream.readable.getReader();
|
||||
|
||||
return async function* streamData() {
|
||||
const decoder = new TextDecoder();
|
||||
const reader = response.body.getReader();
|
||||
let getMessage = '';
|
||||
let messageBuffer = '';
|
||||
let text = '';
|
||||
const swipes = [];
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
// We don't want carriage returns in our messages
|
||||
let response = decoder.decode(value).replace(/\r/g, '');
|
||||
if (done) return;
|
||||
if (value.data === '[DONE]') return;
|
||||
|
||||
tryParseStreamingError(response);
|
||||
tryParseStreamingError(response, value.data);
|
||||
|
||||
let eventList = [];
|
||||
let data = JSON.parse(value.data);
|
||||
|
||||
messageBuffer += response;
|
||||
eventList = messageBuffer.split('\n\n');
|
||||
// Last element will be an empty string or a leftover partial message
|
||||
messageBuffer = eventList.pop();
|
||||
|
||||
for (let event of eventList) {
|
||||
if (event.startsWith('event: completion')) {
|
||||
event = event.split('\n')[1];
|
||||
}
|
||||
|
||||
if (typeof event !== 'string' || !event.length)
|
||||
continue;
|
||||
|
||||
if (!event.startsWith('data'))
|
||||
continue;
|
||||
if (event == 'data: [DONE]') {
|
||||
return;
|
||||
}
|
||||
let data = JSON.parse(event.substring(6));
|
||||
|
||||
if (data?.choices[0]?.index > 0) {
|
||||
const swipeIndex = data.choices[0].index - 1;
|
||||
swipes[swipeIndex] = (swipes[swipeIndex] || '') + data.choices[0].text;
|
||||
} else {
|
||||
getMessage += data?.choices[0]?.text || '';
|
||||
}
|
||||
|
||||
yield { text: getMessage, swipes: swipes };
|
||||
if (data?.choices?.[0]?.index > 0) {
|
||||
const swipeIndex = data.choices[0].index - 1;
|
||||
swipes[swipeIndex] = (swipes[swipeIndex] || '') + data.choices[0].text;
|
||||
} else {
|
||||
text += data?.choices?.[0]?.text || data?.content || '';
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
yield { text, swipes };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses errors in streaming responses and displays them in toastr.
|
||||
* @param {string} response - Response from the server.
|
||||
* @param {Response} response - Response from the server.
|
||||
* @param {string} decoded - Decoded response body.
|
||||
* @returns {void} Nothing.
|
||||
*/
|
||||
function tryParseStreamingError(response) {
|
||||
function tryParseStreamingError(response, decoded) {
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
data = JSON.parse(response);
|
||||
data = JSON.parse(decoded);
|
||||
} catch {
|
||||
// No JSON. Do nothing.
|
||||
}
|
||||
@ -545,11 +634,16 @@ function tryParseStreamingError(response) {
|
||||
const message = data?.error?.message || data?.message;
|
||||
|
||||
if (message) {
|
||||
toastr.error(message, 'API Error');
|
||||
toastr.error(message, 'Text Completion API');
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string of comma-separated integers to an array of integers.
|
||||
* @param {string} string Input string
|
||||
* @returns {number[]} Array of integers
|
||||
*/
|
||||
function toIntArray(string) {
|
||||
if (!string) {
|
||||
return [];
|
||||
@ -563,16 +657,29 @@ function getModel() {
|
||||
return settings.mancer_model;
|
||||
}
|
||||
|
||||
if (settings.type === TOGETHERAI) {
|
||||
return settings.togetherai_model;
|
||||
}
|
||||
|
||||
if (settings.type === APHRODITE) {
|
||||
return online_status;
|
||||
}
|
||||
|
||||
if (settings.type === OLLAMA) {
|
||||
if (!settings.ollama_model) {
|
||||
toastr.error('No Ollama model selected.', 'Text Completion API');
|
||||
throw new Error('No Ollama model selected');
|
||||
}
|
||||
|
||||
return settings.ollama_model;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
|
||||
const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
|
||||
let APIflags = {
|
||||
let params = {
|
||||
'prompt': finalPrompt,
|
||||
'model': getModel(),
|
||||
'max_new_tokens': maxTokens,
|
||||
@ -607,15 +714,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
toIntArray(getCustomTokenBans()) :
|
||||
getCustomTokenBans(),
|
||||
'api_type': settings.type,
|
||||
'api_server': settings.type === MANCER ?
|
||||
MANCER_SERVER :
|
||||
api_server_textgenerationwebui,
|
||||
'legacy_api': settings.legacy_api && settings.type !== MANCER,
|
||||
'sampler_order': settings.type === textgen_types.KOBOLDCPP ?
|
||||
settings.sampler_order :
|
||||
undefined,
|
||||
'api_server': getTextGenServer(),
|
||||
'legacy_api': settings.legacy_api && (settings.type === OOBA || settings.type === APHRODITE),
|
||||
'sampler_order': settings.type === textgen_types.KOBOLDCPP ? settings.sampler_order : undefined,
|
||||
};
|
||||
let aphroditeExclusionFlags = {
|
||||
const nonAphroditeParams = {
|
||||
'repetition_penalty_range': settings.rep_pen_range,
|
||||
'encoder_repetition_penalty': settings.encoder_rep_pen,
|
||||
'no_repeat_ngram_size': settings.no_repeat_ngram_size,
|
||||
@ -626,8 +729,15 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
|
||||
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
|
||||
'grammar_string': settings.grammar_string,
|
||||
// llama.cpp aliases. In case someone wants to use LM Studio as Text Completion API
|
||||
'repeat_penalty': settings.rep_pen,
|
||||
'tfs_z': settings.tfs,
|
||||
'repeat_last_n': settings.rep_pen_range,
|
||||
'n_predict': settings.maxTokens,
|
||||
'mirostat': settings.mirostat_mode,
|
||||
'ignore_eos': settings.ban_eos_token,
|
||||
};
|
||||
let aphroditeFlags = {
|
||||
const aphroditeParams = {
|
||||
'n': canMultiSwipe ? settings.n : 1,
|
||||
'best_of': canMultiSwipe ? settings.n : 1,
|
||||
'ignore_eos': settings.ignore_eos_token_aphrodite,
|
||||
@ -636,12 +746,33 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
//'logprobs': settings.log_probs_aphrodite,
|
||||
//'prompt_logprobs': settings.prompt_log_probs_aphrodite,
|
||||
};
|
||||
if (settings.type === textgen_types.APHRODITE) {
|
||||
APIflags = Object.assign(APIflags, aphroditeFlags);
|
||||
if (settings.type === APHRODITE) {
|
||||
params = Object.assign(params, aphroditeParams);
|
||||
} else {
|
||||
APIflags = Object.assign(APIflags, aphroditeExclusionFlags);
|
||||
params = Object.assign(params, nonAphroditeParams);
|
||||
}
|
||||
|
||||
return APIflags;
|
||||
if (Array.isArray(settings.logit_bias) && settings.logit_bias.length) {
|
||||
const logitBias = BIAS_CACHE.get(BIAS_KEY) || calculateLogitBias();
|
||||
BIAS_CACHE.set(BIAS_KEY, logitBias);
|
||||
params.logit_bias = logitBias;
|
||||
}
|
||||
|
||||
if (settings.type === LLAMACPP || settings.type === OLLAMA) {
|
||||
// Convert bias and token bans to array of arrays
|
||||
const logitBiasArray = (params.logit_bias && typeof params.logit_bias === 'object' && Object.keys(params.logit_bias).length > 0)
|
||||
? Object.entries(params.logit_bias).map(([key, value]) => [Number(key), value])
|
||||
: [];
|
||||
const tokenBans = toIntArray(getCustomTokenBans());
|
||||
logitBiasArray.push(...tokenBans.map(x => [Number(x), false]));
|
||||
const llamaCppParams = {
|
||||
'logit_bias': logitBiasArray,
|
||||
// Conflicts with ooba's grammar_string
|
||||
'grammar': settings.grammar_string,
|
||||
};
|
||||
params = Object.assign(params, llamaCppParams);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { characters, getAPIServerUrl, main_api, nai_settings, online_status, this_chid } from '../script.js';
|
||||
import { characters, main_api, api_server, nai_settings, online_status, this_chid } from '../script.js';
|
||||
import { power_user, registerDebugFunction } from './power-user.js';
|
||||
import { chat_completion_sources, model_list, oai_settings } from './openai.js';
|
||||
import { groups, selected_group } from './group-chats.js';
|
||||
import { getStringHash } from './utils.js';
|
||||
import { kai_flags } from './kai-settings.js';
|
||||
import { textgen_types, textgenerationwebui_settings as textgen_settings } from './textgen-settings.js';
|
||||
import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
|
||||
|
||||
const { OOBA, TABBY, KOBOLDCPP, MANCER } = textgen_types;
|
||||
const { OOBA, TABBY, KOBOLDCPP, APHRODITE, LLAMACPP } = textgen_types;
|
||||
|
||||
export const CHARACTERS_PER_TOKEN_RATIO = 3.35;
|
||||
const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';
|
||||
@ -18,9 +18,11 @@ export const tokenizers = {
|
||||
LLAMA: 3,
|
||||
NERD: 4,
|
||||
NERD2: 5,
|
||||
API: 6,
|
||||
API_CURRENT: 6,
|
||||
MISTRAL: 7,
|
||||
YI: 8,
|
||||
API_TEXTGENERATIONWEBUI: 9,
|
||||
API_KOBOLD: 10,
|
||||
BEST_MATCH: 99,
|
||||
};
|
||||
|
||||
@ -33,6 +35,52 @@ export const SENTENCEPIECE_TOKENIZERS = [
|
||||
//tokenizers.NERD2,
|
||||
];
|
||||
|
||||
const TOKENIZER_URLS = {
|
||||
[tokenizers.GPT2]: {
|
||||
encode: '/api/tokenizers/gpt2/encode',
|
||||
decode: '/api/tokenizers/gpt2/decode',
|
||||
count: '/api/tokenizers/gpt2/encode',
|
||||
},
|
||||
[tokenizers.OPENAI]: {
|
||||
encode: '/api/tokenizers/openai/encode',
|
||||
decode: '/api/tokenizers/openai/decode',
|
||||
count: '/api/tokenizers/openai/encode',
|
||||
},
|
||||
[tokenizers.LLAMA]: {
|
||||
encode: '/api/tokenizers/llama/encode',
|
||||
decode: '/api/tokenizers/llama/decode',
|
||||
count: '/api/tokenizers/llama/encode',
|
||||
},
|
||||
[tokenizers.NERD]: {
|
||||
encode: '/api/tokenizers/nerdstash/encode',
|
||||
decode: '/api/tokenizers/nerdstash/decode',
|
||||
count: '/api/tokenizers/nerdstash/encode',
|
||||
},
|
||||
[tokenizers.NERD2]: {
|
||||
encode: '/api/tokenizers/nerdstash_v2/encode',
|
||||
decode: '/api/tokenizers/nerdstash_v2/decode',
|
||||
count: '/api/tokenizers/nerdstash_v2/encode',
|
||||
},
|
||||
[tokenizers.API_KOBOLD]: {
|
||||
count: '/api/tokenizers/remote/kobold/count',
|
||||
encode: '/api/tokenizers/remote/kobold/count',
|
||||
},
|
||||
[tokenizers.MISTRAL]: {
|
||||
encode: '/api/tokenizers/mistral/encode',
|
||||
decode: '/api/tokenizers/mistral/decode',
|
||||
count: '/api/tokenizers/mistral/encode',
|
||||
},
|
||||
[tokenizers.YI]: {
|
||||
encode: '/api/tokenizers/yi/encode',
|
||||
decode: '/api/tokenizers/yi/decode',
|
||||
count: '/api/tokenizers/yi/encode',
|
||||
},
|
||||
[tokenizers.API_TEXTGENERATIONWEBUI]: {
|
||||
encode: '/api/tokenizers/remote/textgenerationwebui/encode',
|
||||
count: '/api/tokenizers/remote/textgenerationwebui/encode',
|
||||
},
|
||||
};
|
||||
|
||||
const objectStore = new localforage.createInstance({ name: 'SillyTavern_ChatCompletions' });
|
||||
|
||||
let tokenCache = {};
|
||||
@ -92,7 +140,18 @@ export function getFriendlyTokenizerName(forApi) {
|
||||
|
||||
if (forApi !== 'openai' && tokenizerId === tokenizers.BEST_MATCH) {
|
||||
tokenizerId = getTokenizerBestMatch(forApi);
|
||||
tokenizerName = $(`#tokenizer option[value="${tokenizerId}"]`).text();
|
||||
|
||||
switch (tokenizerId) {
|
||||
case tokenizers.API_KOBOLD:
|
||||
tokenizerName = 'API (KoboldAI Classic)';
|
||||
break;
|
||||
case tokenizers.API_TEXTGENERATIONWEBUI:
|
||||
tokenizerName = 'API (Text Completion)';
|
||||
break;
|
||||
default:
|
||||
tokenizerName = $(`#tokenizer option[value="${tokenizerId}"]`).text();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tokenizerName = forApi == 'openai'
|
||||
@ -131,15 +190,15 @@ export function getTokenizerBestMatch(forApi) {
|
||||
// - Tokenizer haven't reported an error previously
|
||||
const hasTokenizerError = sessionStorage.getItem(TOKENIZER_WARNING_KEY);
|
||||
const isConnected = online_status !== 'no_connection';
|
||||
const isTokenizerSupported = textgen_settings.type === OOBA || textgen_settings.type === TABBY || textgen_settings.type === KOBOLDCPP;
|
||||
const isTokenizerSupported = [OOBA, TABBY, KOBOLDCPP, LLAMACPP].includes(textgen_settings.type);
|
||||
|
||||
if (!hasTokenizerError && isConnected) {
|
||||
if (forApi === 'kobold' && kai_flags.can_use_tokenization) {
|
||||
return tokenizers.API;
|
||||
return tokenizers.API_KOBOLD;
|
||||
}
|
||||
|
||||
if (forApi === 'textgenerationwebui' && isTokenizerSupported) {
|
||||
return tokenizers.API;
|
||||
return tokenizers.API_TEXTGENERATIONWEBUI;
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,34 +208,42 @@ export function getTokenizerBestMatch(forApi) {
|
||||
return tokenizers.NONE;
|
||||
}
|
||||
|
||||
// Get the current remote tokenizer API based on the current text generation API.
|
||||
function currentRemoteTokenizerAPI() {
|
||||
switch (main_api) {
|
||||
case 'kobold':
|
||||
return tokenizers.API_KOBOLD;
|
||||
case 'textgenerationwebui':
|
||||
return tokenizers.API_TEXTGENERATIONWEBUI;
|
||||
default:
|
||||
return tokenizers.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the underlying tokenizer model to the token count for a string.
|
||||
* @param {number} type Tokenizer type.
|
||||
* @param {string} str String to tokenize.
|
||||
* @param {number} padding Number of padding tokens.
|
||||
* @returns {number} Token count.
|
||||
*/
|
||||
function callTokenizer(type, str, padding) {
|
||||
function callTokenizer(type, str) {
|
||||
if (type === tokenizers.NONE) return guesstimate(str);
|
||||
|
||||
switch (type) {
|
||||
case tokenizers.NONE:
|
||||
return guesstimate(str) + padding;
|
||||
case tokenizers.GPT2:
|
||||
return countTokensRemote('/api/tokenizers/gpt2/encode', str, padding);
|
||||
case tokenizers.LLAMA:
|
||||
return countTokensRemote('/api/tokenizers/llama/encode', str, padding);
|
||||
case tokenizers.NERD:
|
||||
return countTokensRemote('/api/tokenizers/nerdstash/encode', str, padding);
|
||||
case tokenizers.NERD2:
|
||||
return countTokensRemote('/api/tokenizers/nerdstash_v2/encode', str, padding);
|
||||
case tokenizers.MISTRAL:
|
||||
return countTokensRemote('/api/tokenizers/mistral/encode', str, padding);
|
||||
case tokenizers.YI:
|
||||
return countTokensRemote('/api/tokenizers/yi/encode', str, padding);
|
||||
case tokenizers.API:
|
||||
return countTokensRemote('/tokenize_via_api', str, padding);
|
||||
default:
|
||||
console.warn('Unknown tokenizer type', type);
|
||||
return callTokenizer(tokenizers.NONE, str, padding);
|
||||
case tokenizers.API_CURRENT:
|
||||
return callTokenizer(currentRemoteTokenizerAPI(), str);
|
||||
case tokenizers.API_KOBOLD:
|
||||
return countTokensFromKoboldAPI(str);
|
||||
case tokenizers.API_TEXTGENERATIONWEBUI:
|
||||
return countTokensFromTextgenAPI(str);
|
||||
default: {
|
||||
const endpointUrl = TOKENIZER_URLS[type]?.count;
|
||||
if (!endpointUrl) {
|
||||
console.warn('Unknown tokenizer type', type);
|
||||
return apiFailureTokenCount(str);
|
||||
}
|
||||
return countTokensFromServer(endpointUrl, str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,7 +286,7 @@ export function getTokenCount(str, padding = undefined) {
|
||||
return cacheObject[cacheKey];
|
||||
}
|
||||
|
||||
const result = callTokenizer(tokenizerType, str, padding);
|
||||
const result = callTokenizer(tokenizerType, str) + padding;
|
||||
|
||||
if (isNaN(result)) {
|
||||
console.warn('Token count calculation returned NaN');
|
||||
@ -309,10 +376,22 @@ export function getTokenizerModel() {
|
||||
}
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
|
||||
return oai_settings.google_model;
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
return claudeTokenizer;
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
|
||||
return mistralTokenizer;
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
|
||||
return oai_settings.custom_model;
|
||||
}
|
||||
|
||||
// Default to Turbo 3.5
|
||||
return turboTokenizer;
|
||||
}
|
||||
@ -322,6 +401,15 @@ export function getTokenizerModel() {
|
||||
*/
|
||||
export function countTokensOpenAI(messages, full = false) {
|
||||
const shouldTokenizeAI21 = oai_settings.chat_completion_source === chat_completion_sources.AI21 && oai_settings.use_ai21_tokenizer;
|
||||
const shouldTokenizeGoogle = oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE && oai_settings.use_google_tokenizer;
|
||||
let tokenizerEndpoint = '';
|
||||
if (shouldTokenizeAI21) {
|
||||
tokenizerEndpoint = '/api/tokenizers/ai21/count';
|
||||
} else if (shouldTokenizeGoogle) {
|
||||
tokenizerEndpoint = `/api/tokenizers/google/count?model=${getTokenizerModel()}`;
|
||||
} else {
|
||||
tokenizerEndpoint = `/api/tokenizers/openai/count?model=${getTokenizerModel()}`;
|
||||
}
|
||||
const cacheObject = getTokenCacheObject();
|
||||
|
||||
if (!Array.isArray(messages)) {
|
||||
@ -333,7 +421,7 @@ export function countTokensOpenAI(messages, full = false) {
|
||||
for (const message of messages) {
|
||||
const model = getTokenizerModel();
|
||||
|
||||
if (model === 'claude' || shouldTokenizeAI21) {
|
||||
if (model === 'claude' || shouldTokenizeAI21 || shouldTokenizeGoogle) {
|
||||
full = true;
|
||||
}
|
||||
|
||||
@ -349,7 +437,7 @@ export function countTokensOpenAI(messages, full = false) {
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
type: 'POST', //
|
||||
url: shouldTokenizeAI21 ? '/api/tokenizers/ai21/count' : `/api/tokenizers/openai/count?model=${model}`,
|
||||
url: tokenizerEndpoint,
|
||||
data: JSON.stringify([message]),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
@ -391,76 +479,129 @@ function getTokenCacheObject() {
|
||||
return tokenCache[String(chatId)];
|
||||
}
|
||||
|
||||
function getRemoteTokenizationParams(str) {
|
||||
return {
|
||||
text: str,
|
||||
main_api,
|
||||
api_type: textgen_settings.type,
|
||||
url: getAPIServerUrl(),
|
||||
legacy_api: main_api === 'textgenerationwebui' &&
|
||||
textgen_settings.legacy_api &&
|
||||
textgen_settings.type !== MANCER,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts token using the remote server API.
|
||||
* Count tokens using the server API.
|
||||
* @param {string} endpoint API endpoint.
|
||||
* @param {string} str String to tokenize.
|
||||
* @param {number} padding Number of padding tokens.
|
||||
* @returns {number} Token count with padding.
|
||||
* @returns {number} Token count.
|
||||
*/
|
||||
function countTokensRemote(endpoint, str, padding) {
|
||||
function countTokensFromServer(endpoint, str) {
|
||||
let tokenCount = 0;
|
||||
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
type: 'POST',
|
||||
url: endpoint,
|
||||
data: JSON.stringify(getRemoteTokenizationParams(str)),
|
||||
data: JSON.stringify({ text: str }),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
if (typeof data.count === 'number') {
|
||||
tokenCount = data.count;
|
||||
} else {
|
||||
tokenCount = guesstimate(str);
|
||||
console.error('Error counting tokens');
|
||||
|
||||
if (!sessionStorage.getItem(TOKENIZER_WARNING_KEY)) {
|
||||
toastr.warning(
|
||||
'Your selected API doesn\'t support the tokenization endpoint. Using estimated counts.',
|
||||
'Error counting tokens',
|
||||
{ timeOut: 10000, preventDuplicates: true },
|
||||
);
|
||||
|
||||
sessionStorage.setItem(TOKENIZER_WARNING_KEY, String(true));
|
||||
}
|
||||
tokenCount = apiFailureTokenCount(str);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return tokenCount + padding;
|
||||
return tokenCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count tokens using the AI provider's API.
|
||||
* @param {string} str String to tokenize.
|
||||
* @returns {number} Token count.
|
||||
*/
|
||||
function countTokensFromKoboldAPI(str) {
|
||||
let tokenCount = 0;
|
||||
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
type: 'POST',
|
||||
url: TOKENIZER_URLS[tokenizers.API_KOBOLD].count,
|
||||
data: JSON.stringify({
|
||||
text: str,
|
||||
url: api_server,
|
||||
}),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
if (typeof data.count === 'number') {
|
||||
tokenCount = data.count;
|
||||
} else {
|
||||
tokenCount = apiFailureTokenCount(str);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return tokenCount;
|
||||
}
|
||||
|
||||
function getTextgenAPITokenizationParams(str) {
|
||||
return {
|
||||
text: str,
|
||||
api_type: textgen_settings.type,
|
||||
url: getTextGenServer(),
|
||||
legacy_api: textgen_settings.legacy_api && (textgen_settings.type === OOBA || textgen_settings.type === APHRODITE),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Count tokens using the AI provider's API.
|
||||
* @param {string} str String to tokenize.
|
||||
* @returns {number} Token count.
|
||||
*/
|
||||
function countTokensFromTextgenAPI(str) {
|
||||
let tokenCount = 0;
|
||||
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
type: 'POST',
|
||||
url: TOKENIZER_URLS[tokenizers.API_TEXTGENERATIONWEBUI].count,
|
||||
data: JSON.stringify(getTextgenAPITokenizationParams(str)),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
if (typeof data.count === 'number') {
|
||||
tokenCount = data.count;
|
||||
} else {
|
||||
tokenCount = apiFailureTokenCount(str);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return tokenCount;
|
||||
}
|
||||
|
||||
function apiFailureTokenCount(str) {
|
||||
console.error('Error counting tokens');
|
||||
|
||||
if (!sessionStorage.getItem(TOKENIZER_WARNING_KEY)) {
|
||||
toastr.warning(
|
||||
'Your selected API doesn\'t support the tokenization endpoint. Using estimated counts.',
|
||||
'Error counting tokens',
|
||||
{ timeOut: 10000, preventDuplicates: true },
|
||||
);
|
||||
|
||||
sessionStorage.setItem(TOKENIZER_WARNING_KEY, String(true));
|
||||
}
|
||||
|
||||
return guesstimate(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the underlying tokenizer model to encode a string to tokens.
|
||||
* @param {string} endpoint API endpoint.
|
||||
* @param {string} str String to tokenize.
|
||||
* @param {string} model Tokenizer model.
|
||||
* @returns {number[]} Array of token ids.
|
||||
*/
|
||||
function getTextTokensRemote(endpoint, str, model = '') {
|
||||
if (model) {
|
||||
endpoint += `?model=${model}`;
|
||||
}
|
||||
|
||||
function getTextTokensFromServer(endpoint, str) {
|
||||
let ids = [];
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
type: 'POST',
|
||||
url: endpoint,
|
||||
data: JSON.stringify(getRemoteTokenizationParams(str)),
|
||||
data: JSON.stringify({ text: str }),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
@ -475,16 +616,59 @@ function getTextTokensRemote(endpoint, str, model = '') {
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the AI provider's tokenize API to encode a string to tokens.
|
||||
* @param {string} str String to tokenize.
|
||||
* @returns {number[]} Array of token ids.
|
||||
*/
|
||||
function getTextTokensFromTextgenAPI(str) {
|
||||
let ids = [];
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
type: 'POST',
|
||||
url: TOKENIZER_URLS[tokenizers.API_TEXTGENERATIONWEBUI].encode,
|
||||
data: JSON.stringify(getTextgenAPITokenizationParams(str)),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
ids = data.ids;
|
||||
},
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the AI provider's tokenize API to encode a string to tokens.
|
||||
* @param {string} str String to tokenize.
|
||||
* @returns {number[]} Array of token ids.
|
||||
*/
|
||||
function getTextTokensFromKoboldAPI(str) {
|
||||
let ids = [];
|
||||
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
type: 'POST',
|
||||
url: TOKENIZER_URLS[tokenizers.API_KOBOLD].encode,
|
||||
data: JSON.stringify({
|
||||
text: str,
|
||||
url: api_server,
|
||||
}),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
ids = data.ids;
|
||||
},
|
||||
});
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the underlying tokenizer model to decode token ids to text.
|
||||
* @param {string} endpoint API endpoint.
|
||||
* @param {number[]} ids Array of token ids
|
||||
*/
|
||||
function decodeTextTokensRemote(endpoint, ids, model = '') {
|
||||
if (model) {
|
||||
endpoint += `?model=${model}`;
|
||||
}
|
||||
|
||||
function decodeTextTokensFromServer(endpoint, ids) {
|
||||
let text = '';
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
@ -501,64 +685,64 @@ function decodeTextTokensRemote(endpoint, ids, model = '') {
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string to tokens using the remote server API.
|
||||
* Encodes a string to tokens using the server API.
|
||||
* @param {number} tokenizerType Tokenizer type.
|
||||
* @param {string} str String to tokenize.
|
||||
* @returns {number[]} Array of token ids.
|
||||
*/
|
||||
export function getTextTokens(tokenizerType, str) {
|
||||
switch (tokenizerType) {
|
||||
case tokenizers.GPT2:
|
||||
return getTextTokensRemote('/api/tokenizers/gpt2/encode', str);
|
||||
case tokenizers.LLAMA:
|
||||
return getTextTokensRemote('/api/tokenizers/llama/encode', str);
|
||||
case tokenizers.NERD:
|
||||
return getTextTokensRemote('/api/tokenizers/nerdstash/encode', str);
|
||||
case tokenizers.NERD2:
|
||||
return getTextTokensRemote('/api/tokenizers/nerdstash_v2/encode', str);
|
||||
case tokenizers.MISTRAL:
|
||||
return getTextTokensRemote('/api/tokenizers/mistral/encode', str);
|
||||
case tokenizers.YI:
|
||||
return getTextTokensRemote('/api/tokenizers/yi/encode', str);
|
||||
case tokenizers.OPENAI: {
|
||||
const model = getTokenizerModel();
|
||||
return getTextTokensRemote('/api/tokenizers/openai/encode', str, model);
|
||||
case tokenizers.API_CURRENT:
|
||||
return getTextTokens(currentRemoteTokenizerAPI(), str);
|
||||
case tokenizers.API_TEXTGENERATIONWEBUI:
|
||||
return getTextTokensFromTextgenAPI(str);
|
||||
case tokenizers.API_KOBOLD:
|
||||
return getTextTokensFromKoboldAPI(str);
|
||||
default: {
|
||||
const tokenizerEndpoints = TOKENIZER_URLS[tokenizerType];
|
||||
if (!tokenizerEndpoints) {
|
||||
apiFailureTokenCount(str);
|
||||
console.warn('Unknown tokenizer type', tokenizerType);
|
||||
return [];
|
||||
}
|
||||
let endpointUrl = tokenizerEndpoints.encode;
|
||||
if (!endpointUrl) {
|
||||
apiFailureTokenCount(str);
|
||||
console.warn('This tokenizer type does not support encoding', tokenizerType);
|
||||
return [];
|
||||
}
|
||||
if (tokenizerType === tokenizers.OPENAI) {
|
||||
endpointUrl += `?model=${getTokenizerModel()}`;
|
||||
}
|
||||
return getTextTokensFromServer(endpointUrl, str);
|
||||
}
|
||||
case tokenizers.API:
|
||||
return getTextTokensRemote('/tokenize_via_api', str);
|
||||
default:
|
||||
console.warn('Calling getTextTokens with unsupported tokenizer type', tokenizerType);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes token ids to text using the remote server API.
|
||||
* Decodes token ids to text using the server API.
|
||||
* @param {number} tokenizerType Tokenizer type.
|
||||
* @param {number[]} ids Array of token ids
|
||||
*/
|
||||
export function decodeTextTokens(tokenizerType, ids) {
|
||||
switch (tokenizerType) {
|
||||
case tokenizers.GPT2:
|
||||
return decodeTextTokensRemote('/api/tokenizers/gpt2/decode', ids);
|
||||
case tokenizers.LLAMA:
|
||||
return decodeTextTokensRemote('/api/tokenizers/llama/decode', ids);
|
||||
case tokenizers.NERD:
|
||||
return decodeTextTokensRemote('/api/tokenizers/nerdstash/decode', ids);
|
||||
case tokenizers.NERD2:
|
||||
return decodeTextTokensRemote('/api/tokenizers/nerdstash_v2/decode', ids);
|
||||
case tokenizers.MISTRAL:
|
||||
return decodeTextTokensRemote('/api/tokenizers/mistral/decode', ids);
|
||||
case tokenizers.YI:
|
||||
return decodeTextTokensRemote('/api/tokenizers/yi/decode', ids);
|
||||
case tokenizers.OPENAI: {
|
||||
const model = getTokenizerModel();
|
||||
return decodeTextTokensRemote('/api/tokenizers/openai/decode', ids, model);
|
||||
}
|
||||
default:
|
||||
console.warn('Calling decodeTextTokens with unsupported tokenizer type', tokenizerType);
|
||||
return '';
|
||||
// Currently, neither remote API can decode, but this may change in the future. Put this guard here to be safe
|
||||
if (tokenizerType === tokenizers.API_CURRENT) {
|
||||
return decodeTextTokens(tokenizers.NONE, ids);
|
||||
}
|
||||
const tokenizerEndpoints = TOKENIZER_URLS[tokenizerType];
|
||||
if (!tokenizerEndpoints) {
|
||||
console.warn('Unknown tokenizer type', tokenizerType);
|
||||
return [];
|
||||
}
|
||||
let endpointUrl = tokenizerEndpoints.decode;
|
||||
if (!endpointUrl) {
|
||||
console.warn('This tokenizer type does not support decoding', tokenizerType);
|
||||
return [];
|
||||
}
|
||||
if (tokenizerType === tokenizers.OPENAI) {
|
||||
endpointUrl += `?model=${getTokenizerModel()}`;
|
||||
}
|
||||
return decodeTextTokensFromServer(endpointUrl, ids);
|
||||
}
|
||||
|
||||
export async function initTokenizers() {
|
||||
|
@ -741,6 +741,38 @@ export function escapeRegex(string) {
|
||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
export class Stopwatch {
|
||||
/**
|
||||
* Initializes a Stopwatch class.
|
||||
* @param {number} interval Update interval in milliseconds. Must be a finite number above zero.
|
||||
*/
|
||||
constructor(interval) {
|
||||
if (isNaN(interval) || !isFinite(interval) || interval <= 0) {
|
||||
console.warn('Invalid interval for Stopwatch, setting to 1');
|
||||
interval = 1;
|
||||
}
|
||||
|
||||
this.interval = interval;
|
||||
this.lastAction = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function if the interval passed.
|
||||
* @param {(arg0: any) => any} action Action function
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async tick(action) {
|
||||
const passed = (Date.now() - this.lastAction);
|
||||
|
||||
if (passed < this.interval) {
|
||||
return;
|
||||
}
|
||||
|
||||
await action();
|
||||
this.lastAction = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an interface for rate limiting function calls.
|
||||
*/
|
||||
@ -998,6 +1030,11 @@ export function loadFileToDocument(url, type) {
|
||||
* @returns {Promise<string>} A promise that resolves to the thumbnail data URL.
|
||||
*/
|
||||
export function createThumbnail(dataUrl, maxWidth, maxHeight, type = 'image/jpeg') {
|
||||
// Someone might pass in a base64 encoded string without the data URL prefix
|
||||
if (!dataUrl.includes('data:')) {
|
||||
dataUrl = `data:image/jpeg;base64,${dataUrl}`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = dataUrl;
|
||||
@ -1143,11 +1180,13 @@ export async function extractTextFromPDF(blob) {
|
||||
* @param {Blob} blob HTML content blob
|
||||
* @returns {Promise<string>} A promise that resolves to the parsed text.
|
||||
*/
|
||||
export async function extractTextFromHTML(blob) {
|
||||
export async function extractTextFromHTML(blob, textSelector = 'body') {
|
||||
const html = await blob.text();
|
||||
const domParser = new DOMParser();
|
||||
const document = domParser.parseFromString(DOMPurify.sanitize(html), 'text/html');
|
||||
const text = postProcessText(document.body.textContent);
|
||||
const elements = document.querySelectorAll(textSelector);
|
||||
const rawText = Array.from(elements).map(e => e.textContent).join('\n');
|
||||
const text = postProcessText(rawText);
|
||||
return text;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
|
||||
import { extension_settings, saveMetadataDebounced } from './extensions.js';
|
||||
import { executeSlashCommands, registerSlashCommand } from './slash-commands.js';
|
||||
import { isFalseBoolean } from './utils.js';
|
||||
|
||||
const MAX_LOOPS = 100;
|
||||
|
||||
function getLocalVariable(name, args = {}) {
|
||||
if (!chat_metadata.variables) {
|
||||
@ -301,8 +304,7 @@ function listVariablesCallback() {
|
||||
}
|
||||
|
||||
async function whileCallback(args, command) {
|
||||
const MAX_LOOPS = 100;
|
||||
const isGuardOff = ['off', 'false', '0'].includes(args.guard?.toLowerCase());
|
||||
const isGuardOff = isFalseBoolean(args.guard);
|
||||
const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS;
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
@ -319,6 +321,19 @@ async function whileCallback(args, command) {
|
||||
return '';
|
||||
}
|
||||
|
||||
async function timesCallback(args, value) {
|
||||
const [repeats, ...commandParts] = value.split(' ');
|
||||
const command = commandParts.join(' ');
|
||||
const isGuardOff = isFalseBoolean(args.guard);
|
||||
const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async function ifCallback(args, command) {
|
||||
const { a, b, rule } = parseBooleanOperands(args);
|
||||
const result = evalBoolean(rule, a, b);
|
||||
@ -637,6 +652,21 @@ function lenValuesCallback(value) {
|
||||
return parsedValue.length;
|
||||
}
|
||||
|
||||
function randValuesCallback(from, to, args) {
|
||||
const range = to - from;
|
||||
const value = from + Math.random() * range;
|
||||
if (args.round == 'round') {
|
||||
return Math.round(value);
|
||||
}
|
||||
if (args.round == 'ceil') {
|
||||
return Math.ceil(value);
|
||||
}
|
||||
if (args.round == 'floor') {
|
||||
return Math.floor(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function registerVariableCommands() {
|
||||
registerSlashCommand('listvar', listVariablesCallback, [], ' – list registered chat variables', true, true);
|
||||
registerSlashCommand('setvar', (args, value) => setLocalVariable(args.key || args.name, value, args), [], '<span class="monospace">key=varname index=listIndex (value)</span> – set a local variable value and pass it down the pipe, index is optional, e.g. <tt>/setvar key=color green</tt>', true, true);
|
||||
@ -651,6 +681,7 @@ export function registerVariableCommands() {
|
||||
registerSlashCommand('decglobalvar', (_, value) => decrementGlobalVariable(value), [], '<span class="monospace">(key)</span> – decrement a global variable by 1 and pass the result down the pipe, e.g. <tt>/decglobalvar score</tt>', true, true);
|
||||
registerSlashCommand('if', ifCallback, [], '<span class="monospace">left=varname1 right=varname2 rule=comparison else="(alt.command)" "(command)"</span> – compare the value of the left operand "a" with the value of the right operand "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values and string literals for left and right operands supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, not => !a, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/if left=score right=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
|
||||
registerSlashCommand('while', whileCallback, [], '<span class="monospace">left=varname1 right=varname2 rule=comparison "(command)"</span> – compare the value of the left operand "a" with the value of the right operand "b", and if the condition yields true, then execute any valid slash command enclosed in quotes. Numeric values and string literals for left and right operands supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, not => !a, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/setvar key=i 0 | /while left=i right=10 rule=let "/addvar key=i 1"</tt> adds 1 to the value of "i" until it reaches 10. Loops are limited to 100 iterations by default, pass guard=off to disable.', true, true);
|
||||
registerSlashCommand('times', (args, value) => timesCallback(args, value), [], '<span class="monospace">(repeats) "(command)"</span> – execute any valid slash command enclosed in quotes <tt>repeats</tt> number of times, e.g. <tt>/setvar key=i 1 | /times 5 "/addvar key=i 1"</tt> adds 1 to the value of "i" 5 times. <tt>{{timesIndex}}</tt> is replaced with the iteration number (zero-based), e.g. <tt>/times 4 "/echo {{timesIndex}}"</tt> echos the numbers 0 through 4. Loops are limited to 100 iterations by default, pass guard=off to disable.', true, true);
|
||||
registerSlashCommand('flushvar', (_, value) => deleteLocalVariable(value), [], '<span class="monospace">(key)</span> – delete a local variable, e.g. <tt>/flushvar score</tt>', true, true);
|
||||
registerSlashCommand('flushglobalvar', (_, value) => deleteGlobalVariable(value), [], '<span class="monospace">(key)</span> – delete a global variable, e.g. <tt>/flushglobalvar score</tt>', true, true);
|
||||
registerSlashCommand('add', (_, value) => addValuesCallback(value), [], '<span class="monospace">(a b c d)</span> – performs an addition of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/add 10 i 30 j</tt>', true, true);
|
||||
@ -668,4 +699,5 @@ export function registerVariableCommands() {
|
||||
registerSlashCommand('sqrt', (_, value) => sqrtValuesCallback(value), [], '<span class="monospace">(a)</span> – performs a square root operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/sqrt i</tt>', true, true);
|
||||
registerSlashCommand('round', (_, value) => roundValuesCallback(value), [], '<span class="monospace">(a)</span> – rounds a value and passes the result down the pipe, can use variable names, e.g. <tt>/round i</tt>', true, true);
|
||||
registerSlashCommand('len', (_, value) => lenValuesCallback(value), [], '<span class="monospace">(a)</span> – gets the length of a value and passes the result down the pipe, can use variable names, e.g. <tt>/len i</tt>', true, true);
|
||||
registerSlashCommand('rand', (args, value) => randValuesCallback(Number(args.from ?? 0), Number(args.to ?? (value.length ? value : 1)), args), [], '<span class="monospace">(from=number=0 to=number=1 round=round|ceil|floor)</span> – returns a random number between from and to, e.g. <tt>/rand</tt> or <tt>/rand 10</tt> or <tt>/rand from=5 to=10</tt>', true, true);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile,
|
||||
import { extension_settings, getContext } from './extensions.js';
|
||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { getDeviceInfo } from './RossAscends-mods.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||
import { getTokenCount } from './tokenizers.js';
|
||||
import { power_user } from './power-user.js';
|
||||
@ -441,7 +441,7 @@ async function loadWorldInfoData(name) {
|
||||
}
|
||||
|
||||
async function updateWorldInfoList() {
|
||||
const result = await fetch('/getsettings', {
|
||||
const result = await fetch('/api/settings/get', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({}),
|
||||
@ -896,8 +896,8 @@ function getWorldEntry(name, data, entry) {
|
||||
|
||||
const characterFilter = template.find('select[name="characterFilter"]');
|
||||
characterFilter.data('uid', entry.uid);
|
||||
const deviceInfo = getDeviceInfo();
|
||||
if (deviceInfo && deviceInfo.device.type === 'desktop') {
|
||||
|
||||
if (!isMobile()) {
|
||||
$(characterFilter).select2({
|
||||
width: '100%',
|
||||
placeholder: 'All characters will pull from this entry.',
|
||||
@ -1684,20 +1684,13 @@ async function checkWorldInfo(chat, maxContext) {
|
||||
|
||||
// Add the depth or AN if enabled
|
||||
// Put this code here since otherwise, the chat reference is modified
|
||||
if (extension_settings.note.allowWIScan) {
|
||||
for (const key of Object.keys(context.extensionPrompts)) {
|
||||
if (key.startsWith('DEPTH_PROMPT')) {
|
||||
const depthPrompt = getExtensionPromptByName(key);
|
||||
if (depthPrompt) {
|
||||
textToScan = `${depthPrompt}\n${textToScan}`;
|
||||
}
|
||||
for (const key of Object.keys(context.extensionPrompts)) {
|
||||
if (context.extensionPrompts[key]?.scan) {
|
||||
const prompt = getExtensionPromptByName(key);
|
||||
if (prompt) {
|
||||
textToScan = `${prompt}\n${textToScan}`;
|
||||
}
|
||||
}
|
||||
|
||||
const anPrompt = getExtensionPromptByName(NOTE_MODULE_NAME);
|
||||
if (anPrompt) {
|
||||
textToScan = `${anPrompt}\n${textToScan}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the resulting string
|
||||
@ -1948,7 +1941,7 @@ async function checkWorldInfo(chat, maxContext) {
|
||||
if (shouldWIAddPrompt) {
|
||||
const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value;
|
||||
const ANWithWI = `${ANTopEntries.join('\n')}\n${originalAN}\n${ANBottomEntries.join('\n')}`;
|
||||
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
|
||||
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan);
|
||||
}
|
||||
|
||||
return { worldInfoBefore, worldInfoAfter, WIDepthEntries };
|
||||
@ -2558,8 +2551,7 @@ jQuery(() => {
|
||||
$(document).on('click', '.chat_lorebook_button', assignLorebookToChat);
|
||||
|
||||
// Not needed on mobile
|
||||
const deviceInfo = getDeviceInfo();
|
||||
if (deviceInfo && deviceInfo.device.type === 'desktop') {
|
||||
if (!isMobile()) {
|
||||
$('#world_info').select2({
|
||||
width: '100%',
|
||||
placeholder: 'No Worlds active. Click here to select.',
|
||||
|
@ -316,6 +316,11 @@ table.responsiveTable {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.mes_text img:not(.mes_img) {
|
||||
max-width: 100%;
|
||||
max-height: var(--doc-height);
|
||||
}
|
||||
|
||||
.mes .mes_timer,
|
||||
.mes .mesIDDisplay,
|
||||
.mes .tokenCounterDisplay {
|
||||
@ -414,7 +419,7 @@ hr {
|
||||
}
|
||||
|
||||
#bg1 {
|
||||
background-image: url('backgrounds/tavern day.jpg');
|
||||
background-image: url('backgrounds/__transparent.png');
|
||||
z-index: -3;
|
||||
}
|
||||
|
||||
@ -631,6 +636,10 @@ hr {
|
||||
display: none;
|
||||
order: 2;
|
||||
padding-right: 2px;
|
||||
place-self: center;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#options_button {
|
||||
@ -1444,9 +1453,7 @@ select option:not(:checked) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#api_button:hover,
|
||||
#api_button_novel:hover,
|
||||
#api_button_textgenerationwebui:hover {
|
||||
.menu_button.api_button:hover {
|
||||
background-color: var(--active);
|
||||
}
|
||||
|
||||
@ -2630,7 +2637,7 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
max-height: calc(100vh - 84px);
|
||||
max-height: calc(100svh - 84px);
|
||||
position: absolute;
|
||||
z-index: 3002;
|
||||
z-index: 3000;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
left: 0;
|
||||
@ -3444,30 +3451,30 @@ a {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.novelai_logit_bias_form {
|
||||
.logit_bias_form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.novelai_logit_bias_text,
|
||||
.novelai_logit_bias_value {
|
||||
.logit_bias_text,
|
||||
.logit_bias_value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.novelai_logit_bias_list {
|
||||
.logit_bias_list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.novelai_logit_bias_list:empty {
|
||||
.logit_bias_list:empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.novelai_logit_bias_list:empty::before {
|
||||
.logit_bias_list:empty::before {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -3476,7 +3483,7 @@ a {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.8;
|
||||
min-height: 2.5rem;
|
||||
min-height: 2.5em;
|
||||
}
|
||||
|
||||
.openai_logit_bias_preset_form {
|
||||
@ -3527,11 +3534,11 @@ a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reverse_proxy_warning {
|
||||
.reverse_proxy_warning:not(small) {
|
||||
color: var(--warning);
|
||||
background-color: var(--black70a);
|
||||
text-shadow: none !important;
|
||||
margin-top: 12px !important;
|
||||
margin-top: 5px !important;
|
||||
border-radius: 5px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
@ -3661,12 +3668,13 @@ a {
|
||||
}
|
||||
|
||||
.icon-svg {
|
||||
fill: currentColor;
|
||||
/* Takes on the color of the surrounding text */
|
||||
fill: currentColor;
|
||||
width: auto;
|
||||
height: 14px;
|
||||
vertical-align: middle;
|
||||
aspect-ratio: 1;
|
||||
/* To align with adjacent text */
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
.paginationjs {
|
||||
|
83
src/additional-headers.js
Normal file
83
src/additional-headers.js
Normal file
@ -0,0 +1,83 @@
|
||||
const { TEXTGEN_TYPES } = require('./constants');
|
||||
const { SECRET_KEYS, readSecret } = require('./endpoints/secrets');
|
||||
const { getConfigValue } = require('./util');
|
||||
|
||||
function getMancerHeaders() {
|
||||
const apiKey = readSecret(SECRET_KEYS.MANCER);
|
||||
|
||||
return apiKey ? ({
|
||||
'X-API-KEY': apiKey,
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
}) : {};
|
||||
}
|
||||
|
||||
function getTogetherAIHeaders() {
|
||||
const apiKey = readSecret(SECRET_KEYS.TOGETHERAI);
|
||||
|
||||
return apiKey ? ({
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
}) : {};
|
||||
}
|
||||
|
||||
function getAphroditeHeaders() {
|
||||
const apiKey = readSecret(SECRET_KEYS.APHRODITE);
|
||||
|
||||
return apiKey ? ({
|
||||
'X-API-KEY': apiKey,
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
}) : {};
|
||||
}
|
||||
|
||||
function getTabbyHeaders() {
|
||||
const apiKey = readSecret(SECRET_KEYS.TABBY);
|
||||
|
||||
return apiKey ? ({
|
||||
'x-api-key': apiKey,
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
}) : {};
|
||||
}
|
||||
|
||||
function getOverrideHeaders(urlHost) {
|
||||
const requestOverrides = getConfigValue('requestOverrides', []);
|
||||
const overrideHeaders = requestOverrides?.find((e) => e.hosts?.includes(urlHost))?.headers;
|
||||
if (overrideHeaders && urlHost) {
|
||||
return overrideHeaders;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets additional headers for the request.
|
||||
* @param {object} request Original request body
|
||||
* @param {object} args New request arguments
|
||||
* @param {string|null} server API server for new request
|
||||
*/
|
||||
function setAdditionalHeaders(request, args, server) {
|
||||
let headers;
|
||||
|
||||
switch (request.body.api_type) {
|
||||
case TEXTGEN_TYPES.MANCER:
|
||||
headers = getMancerHeaders();
|
||||
break;
|
||||
case TEXTGEN_TYPES.APHRODITE:
|
||||
headers = getAphroditeHeaders();
|
||||
break;
|
||||
case TEXTGEN_TYPES.TABBY:
|
||||
headers = getTabbyHeaders();
|
||||
break;
|
||||
case TEXTGEN_TYPES.TOGETHERAI:
|
||||
headers = getTogetherAIHeaders();
|
||||
break;
|
||||
default:
|
||||
headers = server ? getOverrideHeaders((new URL(server))?.host) : {};
|
||||
break;
|
||||
}
|
||||
|
||||
Object.assign(args.headers, headers);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOverrideHeaders,
|
||||
setAdditionalHeaders,
|
||||
};
|
@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Convert a prompt from the ChatML objects to the format used by Claude.
|
||||
* @param {object[]} messages Array of messages
|
||||
* @param {boolean} addHumanPrefix Add Human prefix
|
||||
* @param {boolean} addAssistantPostfix Add Assistant postfix
|
||||
* @param {boolean} withSystemPrompt Build system prompt before "\n\nHuman: "
|
||||
* @returns {string} Prompt for Claude
|
||||
* @copyright Prompt Conversion script taken from RisuAI by kwaroran (GPLv3).
|
||||
*/
|
||||
function convertClaudePrompt(messages, addHumanPrefix, addAssistantPostfix, withSystemPrompt) {
|
||||
// Claude doesn't support message names, so we'll just add them to the message content.
|
||||
for (const message of messages) {
|
||||
if (message.name && message.role !== 'system') {
|
||||
message.content = message.name + ': ' + message.content;
|
||||
delete message.name;
|
||||
}
|
||||
}
|
||||
|
||||
let systemPrompt = '';
|
||||
if (withSystemPrompt) {
|
||||
let lastSystemIdx = -1;
|
||||
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
const message = messages[i];
|
||||
if (message.role === 'system' && !message.name) {
|
||||
systemPrompt += message.content + '\n\n';
|
||||
} else {
|
||||
lastSystemIdx = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastSystemIdx >= 0) {
|
||||
messages.splice(0, lastSystemIdx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
let requestPrompt = messages.map((v) => {
|
||||
let prefix = '';
|
||||
switch (v.role) {
|
||||
case 'assistant':
|
||||
prefix = '\n\nAssistant: ';
|
||||
break;
|
||||
case 'user':
|
||||
prefix = '\n\nHuman: ';
|
||||
break;
|
||||
case 'system':
|
||||
// According to the Claude docs, H: and A: should be used for example conversations.
|
||||
if (v.name === 'example_assistant') {
|
||||
prefix = '\n\nA: ';
|
||||
} else if (v.name === 'example_user') {
|
||||
prefix = '\n\nH: ';
|
||||
} else {
|
||||
prefix = '\n\n';
|
||||
}
|
||||
break;
|
||||
}
|
||||
return prefix + v.content;
|
||||
}).join('');
|
||||
|
||||
if (addHumanPrefix) {
|
||||
requestPrompt = '\n\nHuman: ' + requestPrompt;
|
||||
}
|
||||
|
||||
if (addAssistantPostfix) {
|
||||
requestPrompt = requestPrompt + '\n\nAssistant: ';
|
||||
}
|
||||
|
||||
if (withSystemPrompt) {
|
||||
requestPrompt = systemPrompt + requestPrompt;
|
||||
}
|
||||
|
||||
return requestPrompt;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
convertClaudePrompt,
|
||||
};
|
@ -105,7 +105,26 @@ const UNSAFE_EXTENSIONS = [
|
||||
'.ws',
|
||||
];
|
||||
|
||||
const PALM_SAFETY = [
|
||||
const GEMINI_SAFETY = [
|
||||
{
|
||||
category: 'HARM_CATEGORY_HARASSMENT',
|
||||
threshold: 'BLOCK_NONE',
|
||||
},
|
||||
{
|
||||
category: 'HARM_CATEGORY_HATE_SPEECH',
|
||||
threshold: 'BLOCK_NONE',
|
||||
},
|
||||
{
|
||||
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
||||
threshold: 'BLOCK_NONE',
|
||||
},
|
||||
{
|
||||
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
||||
threshold: 'BLOCK_NONE',
|
||||
},
|
||||
];
|
||||
|
||||
const BISON_SAFETY = [
|
||||
{
|
||||
category: 'HARM_CATEGORY_DEROGATORY',
|
||||
threshold: 'BLOCK_NONE',
|
||||
@ -139,7 +158,9 @@ const CHAT_COMPLETION_SOURCES = {
|
||||
SCALE: 'scale',
|
||||
OPENROUTER: 'openrouter',
|
||||
AI21: 'ai21',
|
||||
PALM: 'palm',
|
||||
MAKERSUITE: 'makersuite',
|
||||
MISTRALAI: 'mistralai',
|
||||
CUSTOM: 'custom',
|
||||
};
|
||||
|
||||
const UPLOADS_PATH = './uploads';
|
||||
@ -151,8 +172,42 @@ const TEXTGEN_TYPES = {
|
||||
APHRODITE: 'aphrodite',
|
||||
TABBY: 'tabby',
|
||||
KOBOLDCPP: 'koboldcpp',
|
||||
TOGETHERAI: 'togetherai',
|
||||
LLAMACPP: 'llamacpp',
|
||||
OLLAMA: 'ollama',
|
||||
};
|
||||
|
||||
// https://docs.together.ai/reference/completions
|
||||
const TOGETHERAI_KEYS = [
|
||||
'model',
|
||||
'prompt',
|
||||
'max_tokens',
|
||||
'temperature',
|
||||
'top_p',
|
||||
'top_k',
|
||||
'repetition_penalty',
|
||||
'stream',
|
||||
];
|
||||
|
||||
// https://github.com/jmorganca/ollama/blob/main/docs/api.md#request-with-options
|
||||
const OLLAMA_KEYS = [
|
||||
'num_predict',
|
||||
'stop',
|
||||
'temperature',
|
||||
'repeat_penalty',
|
||||
'presence_penalty',
|
||||
'frequency_penalty',
|
||||
'top_k',
|
||||
'top_p',
|
||||
'tfs_z',
|
||||
'typical_p',
|
||||
'seed',
|
||||
'repeat_last_n',
|
||||
'mirostat',
|
||||
'mirostat_tau',
|
||||
'mirostat_eta',
|
||||
];
|
||||
|
||||
const AVATAR_WIDTH = 400;
|
||||
const AVATAR_HEIGHT = 600;
|
||||
|
||||
@ -160,9 +215,12 @@ module.exports = {
|
||||
DIRECTORIES,
|
||||
UNSAFE_EXTENSIONS,
|
||||
UPLOADS_PATH,
|
||||
PALM_SAFETY,
|
||||
GEMINI_SAFETY,
|
||||
BISON_SAFETY,
|
||||
TEXTGEN_TYPES,
|
||||
CHAT_COMPLETION_SOURCES,
|
||||
AVATAR_WIDTH,
|
||||
AVATAR_HEIGHT,
|
||||
TOGETHERAI_KEYS,
|
||||
OLLAMA_KEYS,
|
||||
};
|
||||
|
870
src/endpoints/backends/chat-completions.js
Normal file
870
src/endpoints/backends/chat-completions.js
Normal file
@ -0,0 +1,870 @@
|
||||
const express = require('express');
|
||||
const fetch = require('node-fetch').default;
|
||||
const { Readable } = require('stream');
|
||||
|
||||
const { jsonParser } = require('../../express-common');
|
||||
const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY } = require('../../constants');
|
||||
const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
|
||||
const { convertClaudePrompt, convertGooglePrompt, convertTextCompletionPrompt } = require('../prompt-converters');
|
||||
|
||||
const { readSecret, SECRET_KEYS } = require('../secrets');
|
||||
const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');
|
||||
|
||||
const API_OPENAI = 'https://api.openai.com/v1';
|
||||
const API_CLAUDE = 'https://api.anthropic.com/v1';
|
||||
|
||||
/**
|
||||
* Sends a request to Claude API.
|
||||
* @param {express.Request} request Express request
|
||||
* @param {express.Response} response Express response
|
||||
*/
|
||||
async function sendClaudeRequest(request, response) {
|
||||
const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();
|
||||
const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.CLAUDE);
|
||||
const divider = '-'.repeat(process.stdout.columns);
|
||||
|
||||
if (!apiKey) {
|
||||
console.log(color.red(`Claude API key is missing.\n${divider}`));
|
||||
return response.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', function () {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
const isSysPromptSupported = request.body.model === 'claude-2' || request.body.model === 'claude-2.1';
|
||||
const requestPrompt = convertClaudePrompt(request.body.messages, !request.body.exclude_assistant, request.body.assistant_prefill, isSysPromptSupported, request.body.claude_use_sysprompt, request.body.human_sysprompt_message);
|
||||
|
||||
// Check Claude messages sequence and prefixes presence.
|
||||
const sequence = requestPrompt.split('\n').filter(x => x.startsWith('Human:') || x.startsWith('Assistant:'));
|
||||
const humanFound = sequence.some(line => line.startsWith('Human:'));
|
||||
const assistantFound = sequence.some(line => line.startsWith('Assistant:'));
|
||||
let humanErrorCount = 0;
|
||||
let assistantErrorCount = 0;
|
||||
|
||||
for (let i = 0; i < sequence.length - 1; i++) {
|
||||
if (sequence[i].startsWith(sequence[i + 1].split(':')[0])) {
|
||||
if (sequence[i].startsWith('Human:')) {
|
||||
humanErrorCount++;
|
||||
} else if (sequence[i].startsWith('Assistant:')) {
|
||||
assistantErrorCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!humanFound) {
|
||||
console.log(color.red(`${divider}\nWarning: No 'Human:' prefix found in the prompt.\n${divider}`));
|
||||
}
|
||||
if (!assistantFound) {
|
||||
console.log(color.red(`${divider}\nWarning: No 'Assistant: ' prefix found in the prompt.\n${divider}`));
|
||||
}
|
||||
if (!sequence[0].startsWith('Human:')) {
|
||||
console.log(color.red(`${divider}\nWarning: The messages sequence should start with 'Human:' prefix.\nMake sure you have 'Human:' prefix at the very beggining of the prompt, or after the system prompt.\n${divider}`));
|
||||
}
|
||||
if (humanErrorCount > 0 || assistantErrorCount > 0) {
|
||||
console.log(color.red(`${divider}\nWarning: Detected incorrect Prefix sequence(s).`));
|
||||
console.log(color.red(`Incorrect "Human:" prefix(es): ${humanErrorCount}.\nIncorrect "Assistant: " prefix(es): ${assistantErrorCount}.`));
|
||||
console.log(color.red('Check the prompt above and fix it in the SillyTavern.'));
|
||||
console.log(color.red('\nThe correct sequence should look like this:\nSystem prompt <-(for the sysprompt format only, else have 2 empty lines above the first human\'s message.)'));
|
||||
console.log(color.red(` <-----(Each message beginning with the "Assistant:/Human:" prefix must have one empty line above.)\nHuman:\n\nAssistant:\n...\n\nHuman:\n\nAssistant:\n${divider}`));
|
||||
}
|
||||
|
||||
// Add custom stop sequences
|
||||
const stopSequences = ['\n\nHuman:', '\n\nSystem:', '\n\nAssistant:'];
|
||||
if (Array.isArray(request.body.stop)) {
|
||||
stopSequences.push(...request.body.stop);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
prompt: requestPrompt,
|
||||
model: request.body.model,
|
||||
max_tokens_to_sample: request.body.max_tokens,
|
||||
stop_sequences: stopSequences,
|
||||
temperature: request.body.temperature,
|
||||
top_p: request.body.top_p,
|
||||
top_k: request.body.top_k,
|
||||
stream: request.body.stream,
|
||||
};
|
||||
|
||||
console.log('Claude request:', requestBody);
|
||||
|
||||
const generateResponse = await fetch(apiUrl + '/complete', {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify(requestBody),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'anthropic-version': '2023-06-01',
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
timeout: 0,
|
||||
});
|
||||
|
||||
if (request.body.stream) {
|
||||
// Pipe remote SSE stream to Express response
|
||||
forwardFetchResponse(generateResponse, response);
|
||||
} else {
|
||||
if (!generateResponse.ok) {
|
||||
console.log(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${await generateResponse.text()}\n${divider}`));
|
||||
return response.status(generateResponse.status).send({ error: true });
|
||||
}
|
||||
|
||||
const generateResponseJson = await generateResponse.json();
|
||||
const responseText = generateResponseJson.completion;
|
||||
console.log('Claude response:', generateResponseJson);
|
||||
|
||||
// Wrap it back to OAI format
|
||||
const reply = { choices: [{ 'message': { 'content': responseText } }] };
|
||||
return response.send(reply);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(color.red(`Error communicating with Claude: ${error}\n${divider}`));
|
||||
if (!response.headersSent) {
|
||||
return response.status(500).send({ error: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to Scale Spellbook API.
|
||||
* @param {import("express").Request} request Express request
|
||||
* @param {import("express").Response} response Express response
|
||||
*/
|
||||
async function sendScaleRequest(request, response) {
|
||||
const apiUrl = new URL(request.body.api_url_scale).toString();
|
||||
const apiKey = readSecret(SECRET_KEYS.SCALE);
|
||||
|
||||
if (!apiKey) {
|
||||
console.log('Scale API key is missing.');
|
||||
return response.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
const requestPrompt = convertTextCompletionPrompt(request.body.messages);
|
||||
console.log('Scale request:', requestPrompt);
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', function () {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
const generateResponse = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ input: { input: requestPrompt } }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Basic ${apiKey}`,
|
||||
},
|
||||
timeout: 0,
|
||||
});
|
||||
|
||||
if (!generateResponse.ok) {
|
||||
console.log(`Scale API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
|
||||
return response.status(generateResponse.status).send({ error: true });
|
||||
}
|
||||
|
||||
const generateResponseJson = await generateResponse.json();
|
||||
console.log('Scale response:', generateResponseJson);
|
||||
|
||||
const reply = { choices: [{ 'message': { 'content': generateResponseJson.output } }] };
|
||||
return response.send(reply);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (!response.headersSent) {
|
||||
return response.status(500).send({ error: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to Google AI API.
|
||||
* @param {express.Request} request Express request
|
||||
* @param {express.Response} response Express response
|
||||
*/
|
||||
async function sendMakerSuiteRequest(request, response) {
|
||||
const apiKey = readSecret(SECRET_KEYS.MAKERSUITE);
|
||||
|
||||
if (!apiKey) {
|
||||
console.log('MakerSuite API key is missing.');
|
||||
return response.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
const model = String(request.body.model);
|
||||
const isGemini = model.includes('gemini');
|
||||
const isText = model.includes('text');
|
||||
const stream = Boolean(request.body.stream) && isGemini;
|
||||
|
||||
const generationConfig = {
|
||||
stopSequences: request.body.stop,
|
||||
candidateCount: 1,
|
||||
maxOutputTokens: request.body.max_tokens,
|
||||
temperature: request.body.temperature,
|
||||
topP: request.body.top_p,
|
||||
topK: request.body.top_k || undefined,
|
||||
};
|
||||
|
||||
function getGeminiBody() {
|
||||
return {
|
||||
contents: convertGooglePrompt(request.body.messages, model),
|
||||
safetySettings: GEMINI_SAFETY,
|
||||
generationConfig: generationConfig,
|
||||
};
|
||||
}
|
||||
|
||||
function getBisonBody() {
|
||||
const prompt = isText
|
||||
? ({ text: convertTextCompletionPrompt(request.body.messages) })
|
||||
: ({ messages: convertGooglePrompt(request.body.messages, model) });
|
||||
|
||||
/** @type {any} Shut the lint up */
|
||||
const bisonBody = {
|
||||
...generationConfig,
|
||||
safetySettings: BISON_SAFETY,
|
||||
candidate_count: 1, // lewgacy spelling
|
||||
prompt: prompt,
|
||||
};
|
||||
|
||||
if (!isText) {
|
||||
delete bisonBody.stopSequences;
|
||||
delete bisonBody.maxOutputTokens;
|
||||
delete bisonBody.safetySettings;
|
||||
|
||||
if (Array.isArray(prompt.messages)) {
|
||||
for (const msg of prompt.messages) {
|
||||
msg.author = msg.role;
|
||||
msg.content = msg.parts[0].text;
|
||||
delete msg.parts;
|
||||
delete msg.role;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete bisonBody.candidateCount;
|
||||
return bisonBody;
|
||||
}
|
||||
|
||||
const body = isGemini ? getGeminiBody() : getBisonBody();
|
||||
console.log('MakerSuite request:', body);
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', function () {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
const apiVersion = isGemini ? 'v1beta' : 'v1beta2';
|
||||
const responseType = isGemini
|
||||
? (stream ? 'streamGenerateContent' : 'generateContent')
|
||||
: (isText ? 'generateText' : 'generateMessage');
|
||||
|
||||
const generateResponse = await fetch(`https://generativelanguage.googleapis.com/${apiVersion}/models/${model}:${responseType}?key=${apiKey}`, {
|
||||
body: JSON.stringify(body),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: controller.signal,
|
||||
timeout: 0,
|
||||
});
|
||||
// have to do this because of their busted ass streaming endpoint
|
||||
if (stream) {
|
||||
try {
|
||||
let partialData = '';
|
||||
generateResponse.body.on('data', (data) => {
|
||||
const chunk = data.toString();
|
||||
if (chunk.startsWith(',') || chunk.endsWith(',') || chunk.startsWith('[') || chunk.endsWith(']')) {
|
||||
partialData = chunk.slice(1);
|
||||
} else {
|
||||
partialData += chunk;
|
||||
}
|
||||
while (true) {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(partialData);
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
response.write(JSON.stringify(json));
|
||||
partialData = '';
|
||||
}
|
||||
});
|
||||
|
||||
request.socket.on('close', function () {
|
||||
if (generateResponse.body instanceof Readable) generateResponse.body.destroy();
|
||||
response.end();
|
||||
});
|
||||
|
||||
generateResponse.body.on('end', () => {
|
||||
console.log('Streaming request finished');
|
||||
response.end();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log('Error forwarding streaming response:', error);
|
||||
if (!response.headersSent) {
|
||||
return response.status(500).send({ error: true });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!generateResponse.ok) {
|
||||
console.log(`MakerSuite API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
|
||||
return response.status(generateResponse.status).send({ error: true });
|
||||
}
|
||||
|
||||
const generateResponseJson = await generateResponse.json();
|
||||
|
||||
const candidates = generateResponseJson?.candidates;
|
||||
if (!candidates || candidates.length === 0) {
|
||||
let message = 'MakerSuite API returned no candidate';
|
||||
console.log(message, generateResponseJson);
|
||||
if (generateResponseJson?.promptFeedback?.blockReason) {
|
||||
message += `\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;
|
||||
}
|
||||
return response.send({ error: { message } });
|
||||
}
|
||||
|
||||
const responseContent = candidates[0].content ?? candidates[0].output;
|
||||
const responseText = typeof responseContent === 'string' ? responseContent : responseContent.parts?.[0]?.text;
|
||||
if (!responseText) {
|
||||
let message = 'MakerSuite Candidate text empty';
|
||||
console.log(message, generateResponseJson);
|
||||
return response.send({ error: { message } });
|
||||
}
|
||||
|
||||
console.log('MakerSuite response:', responseText);
|
||||
|
||||
// Wrap it back to OAI format
|
||||
const reply = { choices: [{ 'message': { 'content': responseText } }] };
|
||||
return response.send(reply);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error communicating with MakerSuite API: ', error);
|
||||
if (!response.headersSent) {
|
||||
return response.status(500).send({ error: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to AI21 API.
|
||||
* @param {express.Request} request Express request
|
||||
* @param {express.Response} response Express response
|
||||
*/
|
||||
async function sendAI21Request(request, response) {
|
||||
if (!request.body) return response.sendStatus(400);
|
||||
const controller = new AbortController();
|
||||
console.log(request.body.messages);
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', function () {
|
||||
controller.abort();
|
||||
});
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
Authorization: `Bearer ${readSecret(SECRET_KEYS.AI21)}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
numResults: 1,
|
||||
maxTokens: request.body.max_tokens,
|
||||
minTokens: 0,
|
||||
temperature: request.body.temperature,
|
||||
topP: request.body.top_p,
|
||||
stopSequences: request.body.stop_tokens,
|
||||
topKReturn: request.body.top_k,
|
||||
frequencyPenalty: {
|
||||
scale: request.body.frequency_penalty * 100,
|
||||
applyToWhitespaces: false,
|
||||
applyToPunctuations: false,
|
||||
applyToNumbers: false,
|
||||
applyToStopwords: false,
|
||||
applyToEmojis: false,
|
||||
},
|
||||
presencePenalty: {
|
||||
scale: request.body.presence_penalty,
|
||||
applyToWhitespaces: false,
|
||||
applyToPunctuations: false,
|
||||
applyToNumbers: false,
|
||||
applyToStopwords: false,
|
||||
applyToEmojis: false,
|
||||
},
|
||||
countPenalty: {
|
||||
scale: request.body.count_pen,
|
||||
applyToWhitespaces: false,
|
||||
applyToPunctuations: false,
|
||||
applyToNumbers: false,
|
||||
applyToStopwords: false,
|
||||
applyToEmojis: false,
|
||||
},
|
||||
prompt: request.body.messages,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
fetch(`https://api.ai21.com/studio/v1/${request.body.model}/complete`, options)
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (r.completions === undefined) {
|
||||
console.log(r);
|
||||
} else {
|
||||
console.log(r.completions[0].data.text);
|
||||
}
|
||||
const reply = { choices: [{ 'message': { 'content': r.completions[0].data.text } }] };
|
||||
return response.send(reply);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
return response.send({ error: true });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to MistralAI API.
|
||||
* @param {express.Request} request Express request
|
||||
* @param {express.Response} response Express response
|
||||
*/
|
||||
async function sendMistralAIRequest(request, response) {
|
||||
const apiKey = readSecret(SECRET_KEYS.MISTRALAI);
|
||||
|
||||
if (!apiKey) {
|
||||
console.log('MistralAI API key is missing.');
|
||||
return response.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
try {
|
||||
//must send a user role as last message
|
||||
const messages = Array.isArray(request.body.messages) ? request.body.messages : [];
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (messages.length > 0 && lastMsg && (lastMsg.role === 'system' || lastMsg.role === 'assistant')) {
|
||||
if (lastMsg.role === 'assistant') {
|
||||
lastMsg.content = lastMsg.name + ': ' + lastMsg.content;
|
||||
} else if (lastMsg.role === 'system') {
|
||||
lastMsg.content = '[INST] ' + lastMsg.content + ' [/INST]';
|
||||
}
|
||||
lastMsg.role = 'user';
|
||||
}
|
||||
|
||||
//system prompts can be stacked at the start, but any futher sys prompts after the first user/assistant message will break the model
|
||||
let encounteredNonSystemMessage = false;
|
||||
messages.forEach(msg => {
|
||||
if ((msg.role === 'user' || msg.role === 'assistant') && !encounteredNonSystemMessage) {
|
||||
encounteredNonSystemMessage = true;
|
||||
}
|
||||
|
||||
if (encounteredNonSystemMessage && msg.role === 'system') {
|
||||
msg.role = 'user';
|
||||
//unsure if the instruct version is what they've deployed on their endpoints and if this will make a difference or not.
|
||||
//it should be better than just sending the message as a user role without context though
|
||||
msg.content = '[INST] ' + msg.content + ' [/INST]';
|
||||
}
|
||||
});
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', function () {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
const requestBody = {
|
||||
'model': request.body.model,
|
||||
'messages': messages,
|
||||
'temperature': request.body.temperature,
|
||||
'top_p': request.body.top_p,
|
||||
'max_tokens': request.body.max_tokens,
|
||||
'stream': request.body.stream,
|
||||
'safe_mode': request.body.safe_mode,
|
||||
'random_seed': request.body.seed === -1 ? undefined : request.body.seed,
|
||||
};
|
||||
|
||||
const config = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + apiKey,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
timeout: 0,
|
||||
};
|
||||
|
||||
console.log('MisralAI request:', requestBody);
|
||||
|
||||
const generateResponse = await fetch('https://api.mistral.ai/v1/chat/completions', config);
|
||||
if (request.body.stream) {
|
||||
forwardFetchResponse(generateResponse, response);
|
||||
} else {
|
||||
if (!generateResponse.ok) {
|
||||
console.log(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
|
||||
// a 401 unauthorized response breaks the frontend auth, so return a 500 instead. prob a better way of dealing with this.
|
||||
// 401s are already handled by the streaming processor and dont pop up an error toast, that should probably be fixed too.
|
||||
return response.status(generateResponse.status === 401 ? 500 : generateResponse.status).send({ error: true });
|
||||
}
|
||||
const generateResponseJson = await generateResponse.json();
|
||||
console.log('MistralAI response:', generateResponseJson);
|
||||
return response.send(generateResponseJson);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error communicating with MistralAI API: ', error);
|
||||
if (!response.headersSent) {
|
||||
response.send({ error: true });
|
||||
} else {
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
|
||||
if (!request.body) return response_getstatus_openai.sendStatus(400);
|
||||
|
||||
let api_url;
|
||||
let api_key_openai;
|
||||
let headers;
|
||||
|
||||
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {
|
||||
api_url = new URL(request.body.reverse_proxy || API_OPENAI).toString();
|
||||
api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.OPENAI);
|
||||
headers = {};
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {
|
||||
api_url = 'https://openrouter.ai/api/v1';
|
||||
api_key_openai = readSecret(SECRET_KEYS.OPENROUTER);
|
||||
// OpenRouter needs to pass the referer: https://openrouter.ai/docs
|
||||
headers = { 'HTTP-Referer': request.headers.referer };
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
|
||||
api_url = 'https://api.mistral.ai/v1';
|
||||
api_key_openai = readSecret(SECRET_KEYS.MISTRALAI);
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
|
||||
api_url = request.body.custom_url;
|
||||
api_key_openai = readSecret(SECRET_KEYS.CUSTOM);
|
||||
headers = {};
|
||||
mergeObjectWithYaml(headers, request.body.custom_include_headers);
|
||||
} else {
|
||||
console.log('This chat completion source is not supported yet.');
|
||||
return response_getstatus_openai.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
if (!api_key_openai && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
|
||||
console.log('OpenAI API key is missing.');
|
||||
return response_getstatus_openai.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(api_url + '/models', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + api_key_openai,
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
response_getstatus_openai.send(data);
|
||||
|
||||
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER && Array.isArray(data?.data)) {
|
||||
let models = [];
|
||||
|
||||
data.data.forEach(model => {
|
||||
const context_length = model.context_length;
|
||||
const tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));
|
||||
const tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
|
||||
models[model.id] = {
|
||||
tokens_per_dollar: tokens_rounded + 'k',
|
||||
context_length: context_length,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Available OpenRouter models:', models);
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
|
||||
const models = data?.data;
|
||||
console.log(models);
|
||||
} else {
|
||||
const models = data?.data;
|
||||
|
||||
if (Array.isArray(models)) {
|
||||
const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();
|
||||
console.log('Available OpenAI models:', modelIds);
|
||||
} else {
|
||||
console.log('OpenAI endpoint did not return a list of models.');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('OpenAI status check failed. Either Access Token is incorrect or API endpoint is down.');
|
||||
response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
if (!response_getstatus_openai.headersSent) {
|
||||
response_getstatus_openai.send({ error: true });
|
||||
} else {
|
||||
response_getstatus_openai.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/bias', jsonParser, async function (request, response) {
|
||||
if (!request.body || !Array.isArray(request.body))
|
||||
return response.sendStatus(400);
|
||||
|
||||
try {
|
||||
const result = {};
|
||||
const model = getTokenizerModel(String(request.query.model || ''));
|
||||
|
||||
// no bias for claude
|
||||
if (model == 'claude') {
|
||||
return response.send(result);
|
||||
}
|
||||
|
||||
let encodeFunction;
|
||||
|
||||
if (sentencepieceTokenizers.includes(model)) {
|
||||
const tokenizer = getSentencepiceTokenizer(model);
|
||||
const instance = await tokenizer?.get();
|
||||
encodeFunction = (text) => new Uint32Array(instance?.encodeIds(text));
|
||||
} else {
|
||||
const tokenizer = getTiktokenTokenizer(model);
|
||||
encodeFunction = (tokenizer.encode.bind(tokenizer));
|
||||
}
|
||||
|
||||
for (const entry of request.body) {
|
||||
if (!entry || !entry.text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = getEntryTokens(entry.text, encodeFunction);
|
||||
|
||||
for (const token of tokens) {
|
||||
result[token] = entry.value;
|
||||
}
|
||||
} catch {
|
||||
console.warn('Tokenizer failed to encode:', entry.text);
|
||||
}
|
||||
}
|
||||
|
||||
// not needed for cached tokenizers
|
||||
//tokenizer.free();
|
||||
return response.send(result);
|
||||
|
||||
/**
|
||||
* Gets tokenids for a given entry
|
||||
* @param {string} text Entry text
|
||||
* @param {(string) => Uint32Array} encode Function to encode text to token ids
|
||||
* @returns {Uint32Array} Array of token ids
|
||||
*/
|
||||
function getEntryTokens(text, encode) {
|
||||
// Get raw token ids from JSON array
|
||||
if (text.trim().startsWith('[') && text.trim().endsWith(']')) {
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (Array.isArray(json) && json.every(x => typeof x === 'number')) {
|
||||
return new Uint32Array(json);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, get token ids from tokenizer
|
||||
return encode(text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return response.send({});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.post('/generate', jsonParser, function (request, response) {
|
||||
if (!request.body) return response.status(400).send({ error: true });
|
||||
|
||||
switch (request.body.chat_completion_source) {
|
||||
case CHAT_COMPLETION_SOURCES.CLAUDE: return sendClaudeRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.SCALE: return sendScaleRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.AI21: return sendAI21Request(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
|
||||
}
|
||||
|
||||
let apiUrl;
|
||||
let apiKey;
|
||||
let headers;
|
||||
let bodyParams;
|
||||
|
||||
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {
|
||||
apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();
|
||||
apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.OPENAI);
|
||||
headers = {};
|
||||
bodyParams = {};
|
||||
|
||||
if (getConfigValue('openai.randomizeUserId', false)) {
|
||||
bodyParams['user'] = uuidv4();
|
||||
}
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {
|
||||
apiUrl = 'https://openrouter.ai/api/v1';
|
||||
apiKey = readSecret(SECRET_KEYS.OPENROUTER);
|
||||
// OpenRouter needs to pass the referer: https://openrouter.ai/docs
|
||||
headers = { 'HTTP-Referer': request.headers.referer };
|
||||
bodyParams = { 'transforms': ['middle-out'] };
|
||||
|
||||
if (request.body.use_fallback) {
|
||||
bodyParams['route'] = 'fallback';
|
||||
}
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
|
||||
apiUrl = request.body.custom_url;
|
||||
apiKey = readSecret(SECRET_KEYS.CUSTOM);
|
||||
headers = {};
|
||||
bodyParams = {};
|
||||
mergeObjectWithYaml(bodyParams, request.body.custom_include_body);
|
||||
mergeObjectWithYaml(headers, request.body.custom_include_headers);
|
||||
} else {
|
||||
console.log('This chat completion source is not supported yet.');
|
||||
return response.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
|
||||
console.log('OpenAI API key is missing.');
|
||||
return response.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
// Add custom stop sequences
|
||||
if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {
|
||||
bodyParams['stop'] = request.body.stop;
|
||||
}
|
||||
|
||||
const isTextCompletion = Boolean(request.body.model && TEXT_COMPLETION_MODELS.includes(request.body.model)) || typeof request.body.messages === 'string';
|
||||
const textPrompt = isTextCompletion ? convertTextCompletionPrompt(request.body.messages) : '';
|
||||
const endpointUrl = isTextCompletion && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.OPENROUTER ?
|
||||
`${apiUrl}/completions` :
|
||||
`${apiUrl}/chat/completions`;
|
||||
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', function () {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
const requestBody = {
|
||||
'messages': isTextCompletion === false ? request.body.messages : undefined,
|
||||
'prompt': isTextCompletion === true ? textPrompt : undefined,
|
||||
'model': request.body.model,
|
||||
'temperature': request.body.temperature,
|
||||
'max_tokens': request.body.max_tokens,
|
||||
'stream': request.body.stream,
|
||||
'presence_penalty': request.body.presence_penalty,
|
||||
'frequency_penalty': request.body.frequency_penalty,
|
||||
'top_p': request.body.top_p,
|
||||
'top_k': request.body.top_k,
|
||||
'stop': isTextCompletion === false ? request.body.stop : undefined,
|
||||
'logit_bias': request.body.logit_bias,
|
||||
'seed': request.body.seed,
|
||||
...bodyParams,
|
||||
};
|
||||
|
||||
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
|
||||
excludeKeysByYaml(requestBody, request.body.custom_exclude_body);
|
||||
}
|
||||
|
||||
/** @type {import('node-fetch').RequestInit} */
|
||||
const config = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + apiKey,
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
timeout: 0,
|
||||
};
|
||||
|
||||
console.log(requestBody);
|
||||
|
||||
makeRequest(config, response, request);
|
||||
|
||||
/**
|
||||
* Makes a fetch request to the OpenAI API endpoint.
|
||||
* @param {import('node-fetch').RequestInit} config Fetch config
|
||||
* @param {express.Response} response Express response
|
||||
* @param {express.Request} request Express request
|
||||
* @param {Number} retries Number of retries left
|
||||
* @param {Number} timeout Request timeout in ms
|
||||
*/
|
||||
async function makeRequest(config, response, request, retries = 5, timeout = 5000) {
|
||||
try {
|
||||
const fetchResponse = await fetch(endpointUrl, config);
|
||||
|
||||
if (request.body.stream) {
|
||||
console.log('Streaming request in progress');
|
||||
forwardFetchResponse(fetchResponse, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fetchResponse.ok) {
|
||||
let json = await fetchResponse.json();
|
||||
response.send(json);
|
||||
console.log(json);
|
||||
console.log(json?.choices[0]?.message);
|
||||
} else if (fetchResponse.status === 429 && retries > 0) {
|
||||
console.log(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
|
||||
setTimeout(() => {
|
||||
timeout *= 2;
|
||||
makeRequest(config, response, request, retries - 1, timeout);
|
||||
}, timeout);
|
||||
} else {
|
||||
await handleErrorResponse(fetchResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Generation failed', error);
|
||||
if (!response.headersSent) {
|
||||
response.send({ error: true });
|
||||
} else {
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("node-fetch").Response} errorResponse
|
||||
*/
|
||||
async function handleErrorResponse(errorResponse) {
|
||||
const responseText = await errorResponse.text();
|
||||
const errorData = tryParse(responseText);
|
||||
|
||||
const statusMessages = {
|
||||
400: 'Bad request',
|
||||
401: 'Unauthorized',
|
||||
402: 'Credit limit reached',
|
||||
403: 'Forbidden',
|
||||
404: 'Not found',
|
||||
429: 'Too many requests',
|
||||
451: 'Unavailable for legal reasons',
|
||||
502: 'Bad gateway',
|
||||
};
|
||||
|
||||
const message = errorData?.error?.message || statusMessages[errorResponse.status] || 'Unknown error occurred';
|
||||
const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';
|
||||
console.log(message);
|
||||
|
||||
if (!response.headersSent) {
|
||||
response.send({ error: { message }, quota_error: quota_error });
|
||||
} else if (!response.writableEnded) {
|
||||
response.write(errorResponse);
|
||||
} else {
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
router,
|
||||
};
|
188
src/endpoints/backends/kobold.js
Normal file
188
src/endpoints/backends/kobold.js
Normal file
@ -0,0 +1,188 @@
|
||||
const express = require('express');
|
||||
const fetch = require('node-fetch').default;
|
||||
|
||||
const { jsonParser } = require('../../express-common');
|
||||
const { forwardFetchResponse, delay } = require('../../util');
|
||||
const { getOverrideHeaders, setAdditionalHeaders } = require('../../additional-headers');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/generate', jsonParser, async function (request, response_generate) {
|
||||
if (!request.body) return response_generate.sendStatus(400);
|
||||
|
||||
if (request.body.api_server.indexOf('localhost') != -1) {
|
||||
request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');
|
||||
}
|
||||
|
||||
const request_prompt = request.body.prompt;
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', async function () {
|
||||
if (request.body.can_abort && !response_generate.writableEnded) {
|
||||
try {
|
||||
console.log('Aborting Kobold generation...');
|
||||
// send abort signal to koboldcpp
|
||||
const abortResponse = await fetch(`${request.body.api_server}/extra/abort`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!abortResponse.ok) {
|
||||
console.log('Error sending abort request to Kobold:', abortResponse.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
let this_settings = {
|
||||
prompt: request_prompt,
|
||||
use_story: false,
|
||||
use_memory: false,
|
||||
use_authors_note: false,
|
||||
use_world_info: false,
|
||||
max_context_length: request.body.max_context_length,
|
||||
max_length: request.body.max_length,
|
||||
};
|
||||
|
||||
if (!request.body.gui_settings) {
|
||||
this_settings = {
|
||||
prompt: request_prompt,
|
||||
use_story: false,
|
||||
use_memory: false,
|
||||
use_authors_note: false,
|
||||
use_world_info: false,
|
||||
max_context_length: request.body.max_context_length,
|
||||
max_length: request.body.max_length,
|
||||
rep_pen: request.body.rep_pen,
|
||||
rep_pen_range: request.body.rep_pen_range,
|
||||
rep_pen_slope: request.body.rep_pen_slope,
|
||||
temperature: request.body.temperature,
|
||||
tfs: request.body.tfs,
|
||||
top_a: request.body.top_a,
|
||||
top_k: request.body.top_k,
|
||||
top_p: request.body.top_p,
|
||||
min_p: request.body.min_p,
|
||||
typical: request.body.typical,
|
||||
sampler_order: request.body.sampler_order,
|
||||
singleline: !!request.body.singleline,
|
||||
use_default_badwordsids: request.body.use_default_badwordsids,
|
||||
mirostat: request.body.mirostat,
|
||||
mirostat_eta: request.body.mirostat_eta,
|
||||
mirostat_tau: request.body.mirostat_tau,
|
||||
grammar: request.body.grammar,
|
||||
sampler_seed: request.body.sampler_seed,
|
||||
};
|
||||
if (request.body.stop_sequence) {
|
||||
this_settings['stop_sequence'] = request.body.stop_sequence;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(this_settings);
|
||||
const args = {
|
||||
body: JSON.stringify(this_settings),
|
||||
headers: Object.assign(
|
||||
{ 'Content-Type': 'application/json' },
|
||||
getOverrideHeaders((new URL(request.body.api_server))?.host),
|
||||
),
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
const MAX_RETRIES = 50;
|
||||
const delayAmount = 2500;
|
||||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
try {
|
||||
const url = request.body.streaming ? `${request.body.api_server}/extra/generate/stream` : `${request.body.api_server}/v1/generate`;
|
||||
const response = await fetch(url, { method: 'POST', timeout: 0, ...args });
|
||||
|
||||
if (request.body.streaming) {
|
||||
// Pipe remote SSE stream to Express response
|
||||
forwardFetchResponse(response, response_generate);
|
||||
return;
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.log(`Kobold returned error: ${response.status} ${response.statusText} ${errorText}`);
|
||||
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText);
|
||||
const message = errorJson?.detail?.msg || errorText;
|
||||
return response_generate.status(400).send({ error: { message } });
|
||||
} catch {
|
||||
return response_generate.status(400).send({ error: { message: errorText } });
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Endpoint response:', data);
|
||||
return response_generate.send(data);
|
||||
}
|
||||
} catch (error) {
|
||||
// response
|
||||
switch (error?.status) {
|
||||
case 403:
|
||||
case 503: // retry in case of temporary service issue, possibly caused by a queue failure?
|
||||
console.debug(`KoboldAI is busy. Retry attempt ${i + 1} of ${MAX_RETRIES}...`);
|
||||
await delay(delayAmount);
|
||||
break;
|
||||
default:
|
||||
if ('status' in error) {
|
||||
console.log('Status Code from Kobold:', error.status);
|
||||
}
|
||||
return response_generate.send({ error: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Max retries exceeded. Giving up.');
|
||||
return response_generate.send({ error: true });
|
||||
});
|
||||
|
||||
router.post('/status', jsonParser, async function (request, response) {
|
||||
if (!request.body) return response.sendStatus(400);
|
||||
let api_server = request.body.api_server;
|
||||
if (api_server.indexOf('localhost') != -1) {
|
||||
api_server = api_server.replace('localhost', '127.0.0.1');
|
||||
}
|
||||
|
||||
const args = {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
setAdditionalHeaders(request, args, api_server);
|
||||
|
||||
const result = {};
|
||||
|
||||
const [koboldUnitedResponse, koboldExtraResponse, koboldModelResponse] = await Promise.all([
|
||||
// We catch errors both from the response not having a successful HTTP status and from JSON parsing failing
|
||||
|
||||
// Kobold United API version
|
||||
fetch(`${api_server}/v1/info/version`).then(response => {
|
||||
if (!response.ok) throw new Error(`Kobold API error: ${response.status, response.statusText}`);
|
||||
return response.json();
|
||||
}).catch(() => ({ result: '0.0.0' })),
|
||||
|
||||
// KoboldCpp version
|
||||
fetch(`${api_server}/extra/version`).then(response => {
|
||||
if (!response.ok) throw new Error(`Kobold API error: ${response.status, response.statusText}`);
|
||||
return response.json();
|
||||
}).catch(() => ({ version: '0.0' })),
|
||||
|
||||
// Current model
|
||||
fetch(`${api_server}/v1/model`).then(response => {
|
||||
if (!response.ok) throw new Error(`Kobold API error: ${response.status, response.statusText}`);
|
||||
return response.json();
|
||||
}).catch(() => null),
|
||||
]);
|
||||
|
||||
result.koboldUnitedVersion = koboldUnitedResponse.result;
|
||||
result.koboldCppVersion = koboldExtraResponse.result;
|
||||
result.model = !koboldModelResponse || koboldModelResponse.result === 'ReadOnly' ?
|
||||
'no_connection' :
|
||||
koboldModelResponse.result;
|
||||
|
||||
response.send(result);
|
||||
});
|
||||
|
||||
module.exports = { router };
|
76
src/endpoints/backends/scale-alt.js
Normal file
76
src/endpoints/backends/scale-alt.js
Normal file
@ -0,0 +1,76 @@
|
||||
const express = require('express');
|
||||
const fetch = require('node-fetch').default;
|
||||
|
||||
const { jsonParser } = require('../../express-common');
|
||||
|
||||
const { readSecret, SECRET_KEYS } = require('../secrets');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/generate', jsonParser, function (request, response) {
|
||||
if (!request.body) return response.sendStatus(400);
|
||||
|
||||
fetch('https://dashboard.scale.com/spellbook/api/trpc/v2.variant.run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'cookie': `_jwt=${readSecret(SECRET_KEYS.SCALE_COOKIE)}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
json: {
|
||||
variant: {
|
||||
name: 'New Variant',
|
||||
appId: '',
|
||||
taxonomy: null,
|
||||
},
|
||||
prompt: {
|
||||
id: '',
|
||||
template: '{{input}}\n',
|
||||
exampleVariables: {},
|
||||
variablesSourceDataId: null,
|
||||
systemMessage: request.body.sysprompt,
|
||||
},
|
||||
modelParameters: {
|
||||
id: '',
|
||||
modelId: 'GPT4',
|
||||
modelType: 'OpenAi',
|
||||
maxTokens: request.body.max_tokens,
|
||||
temperature: request.body.temp,
|
||||
stop: 'user:',
|
||||
suffix: null,
|
||||
topP: request.body.top_p,
|
||||
logprobs: null,
|
||||
logitBias: request.body.logit_bias,
|
||||
},
|
||||
inputs: [
|
||||
{
|
||||
index: '-1',
|
||||
valueByName: {
|
||||
input: request.body.prompt,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
values: {
|
||||
'variant.taxonomy': ['undefined'],
|
||||
'prompt.variablesSourceDataId': ['undefined'],
|
||||
'modelParameters.suffix': ['undefined'],
|
||||
'modelParameters.logprobs': ['undefined'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
console.log(data.result.data.json.outputs[0]);
|
||||
return response.send({ output: data.result.data.json.outputs[0] });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
return response.send({ error: true });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
module.exports = { router };
|
447
src/endpoints/backends/text-completions.js
Normal file
447
src/endpoints/backends/text-completions.js
Normal file
@ -0,0 +1,447 @@
|
||||
const express = require('express');
|
||||
const fetch = require('node-fetch').default;
|
||||
const _ = require('lodash');
|
||||
const Readable = require('stream').Readable;
|
||||
|
||||
const { jsonParser } = require('../../express-common');
|
||||
const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS } = require('../../constants');
|
||||
const { forwardFetchResponse, trimV1 } = require('../../util');
|
||||
const { setAdditionalHeaders } = require('../../additional-headers');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Special boy's steaming routine. Wrap this abomination into proper SSE stream.
|
||||
* @param {import('node-fetch').Response} jsonStream JSON stream
|
||||
* @param {import('express').Request} request Express request
|
||||
* @param {import('express').Response} response Express response
|
||||
* @returns {Promise<any>} Nothing valuable
|
||||
*/
|
||||
async function parseOllamaStream(jsonStream, request, response) {
|
||||
try {
|
||||
let partialData = '';
|
||||
jsonStream.body.on('data', (data) => {
|
||||
const chunk = data.toString();
|
||||
partialData += chunk;
|
||||
while (true) {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(partialData);
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
const text = json.response || '';
|
||||
const chunk = { choices: [{ text }] };
|
||||
response.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
||||
partialData = '';
|
||||
}
|
||||
});
|
||||
|
||||
request.socket.on('close', function () {
|
||||
if (jsonStream.body instanceof Readable) jsonStream.body.destroy();
|
||||
response.end();
|
||||
});
|
||||
|
||||
jsonStream.body.on('end', () => {
|
||||
console.log('Streaming request finished');
|
||||
response.write('data: [DONE]\n\n');
|
||||
response.end();
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error forwarding streaming response:', error);
|
||||
if (!response.headersSent) {
|
||||
return response.status(500).send({ error: true });
|
||||
} else {
|
||||
return response.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort KoboldCpp generation request.
|
||||
* @param {string} url Server base URL
|
||||
* @returns {Promise<void>} Promise resolving when we are done
|
||||
*/
|
||||
async function abortKoboldCppRequest(url) {
|
||||
try {
|
||||
console.log('Aborting Kobold generation...');
|
||||
const abortResponse = await fetch(`${url}/api/extra/abort`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!abortResponse.ok) {
|
||||
console.log('Error sending abort request to Kobold:', abortResponse.status, abortResponse.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
//************** Ooba/OpenAI text completions API
|
||||
router.post('/status', jsonParser, async function (request, response) {
|
||||
if (!request.body) return response.sendStatus(400);
|
||||
|
||||
try {
|
||||
if (request.body.api_server.indexOf('localhost') !== -1) {
|
||||
request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');
|
||||
}
|
||||
|
||||
console.log('Trying to connect to API:', request.body);
|
||||
const baseUrl = trimV1(request.body.api_server);
|
||||
|
||||
const args = {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
setAdditionalHeaders(request, args, baseUrl);
|
||||
|
||||
let url = baseUrl;
|
||||
let result = '';
|
||||
|
||||
if (request.body.legacy_api) {
|
||||
url += '/v1/model';
|
||||
} else {
|
||||
switch (request.body.api_type) {
|
||||
case TEXTGEN_TYPES.OOBA:
|
||||
case TEXTGEN_TYPES.APHRODITE:
|
||||
case TEXTGEN_TYPES.KOBOLDCPP:
|
||||
case TEXTGEN_TYPES.LLAMACPP:
|
||||
url += '/v1/models';
|
||||
break;
|
||||
case TEXTGEN_TYPES.MANCER:
|
||||
url += '/oai/v1/models';
|
||||
break;
|
||||
case TEXTGEN_TYPES.TABBY:
|
||||
url += '/v1/model/list';
|
||||
break;
|
||||
case TEXTGEN_TYPES.TOGETHERAI:
|
||||
url += '/api/models?&info';
|
||||
break;
|
||||
case TEXTGEN_TYPES.OLLAMA:
|
||||
url += '/api/tags';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const modelsReply = await fetch(url, args);
|
||||
|
||||
if (!modelsReply.ok) {
|
||||
console.log('Models endpoint is offline.');
|
||||
return response.status(400);
|
||||
}
|
||||
|
||||
let data = await modelsReply.json();
|
||||
|
||||
if (request.body.legacy_api) {
|
||||
console.log('Legacy API response:', data);
|
||||
return response.send({ result: data?.result });
|
||||
}
|
||||
|
||||
// Rewrap to OAI-like response
|
||||
if (request.body.api_type === TEXTGEN_TYPES.TOGETHERAI && Array.isArray(data)) {
|
||||
data = { data: data.map(x => ({ id: x.name, ...x })) };
|
||||
}
|
||||
|
||||
if (request.body.api_type === TEXTGEN_TYPES.OLLAMA && Array.isArray(data.models)) {
|
||||
data = { data: data.models.map(x => ({ id: x.name, ...x })) };
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.data)) {
|
||||
console.log('Models response is not an array.');
|
||||
return response.status(400);
|
||||
}
|
||||
|
||||
const modelIds = data.data.map(x => x.id);
|
||||
console.log('Models available:', modelIds);
|
||||
|
||||
// Set result to the first model ID
|
||||
result = modelIds[0] || 'Valid';
|
||||
|
||||
if (request.body.api_type === TEXTGEN_TYPES.OOBA) {
|
||||
try {
|
||||
const modelInfoUrl = baseUrl + '/v1/internal/model/info';
|
||||
const modelInfoReply = await fetch(modelInfoUrl, args);
|
||||
|
||||
if (modelInfoReply.ok) {
|
||||
const modelInfo = await modelInfoReply.json();
|
||||
console.log('Ooba model info:', modelInfo);
|
||||
|
||||
const modelName = modelInfo?.model_name;
|
||||
result = modelName || result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get Ooba model info: ${error}`);
|
||||
}
|
||||
} else if (request.body.api_type === TEXTGEN_TYPES.TABBY) {
|
||||
try {
|
||||
const modelInfoUrl = baseUrl + '/v1/model';
|
||||
const modelInfoReply = await fetch(modelInfoUrl, args);
|
||||
|
||||
if (modelInfoReply.ok) {
|
||||
const modelInfo = await modelInfoReply.json();
|
||||
console.log('Tabby model info:', modelInfo);
|
||||
|
||||
const modelName = modelInfo?.id;
|
||||
result = modelName || result;
|
||||
} else {
|
||||
// TabbyAPI returns an error 400 if a model isn't loaded
|
||||
|
||||
result = 'None';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get TabbyAPI model info: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return response.send({ result, data: data.data });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return response.status(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/generate', jsonParser, async function (request, response) {
|
||||
if (!request.body) return response.sendStatus(400);
|
||||
|
||||
try {
|
||||
if (request.body.api_server.indexOf('localhost') !== -1) {
|
||||
request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1');
|
||||
}
|
||||
|
||||
const baseUrl = request.body.api_server;
|
||||
console.log(request.body);
|
||||
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', async function () {
|
||||
if (request.body.api_type === TEXTGEN_TYPES.KOBOLDCPP && !response.writableEnded) {
|
||||
await abortKoboldCppRequest(trimV1(baseUrl));
|
||||
}
|
||||
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
let url = trimV1(baseUrl);
|
||||
|
||||
if (request.body.legacy_api) {
|
||||
url += '/v1/generate';
|
||||
} else {
|
||||
switch (request.body.api_type) {
|
||||
case TEXTGEN_TYPES.APHRODITE:
|
||||
case TEXTGEN_TYPES.OOBA:
|
||||
case TEXTGEN_TYPES.TABBY:
|
||||
case TEXTGEN_TYPES.KOBOLDCPP:
|
||||
case TEXTGEN_TYPES.TOGETHERAI:
|
||||
url += '/v1/completions';
|
||||
break;
|
||||
case TEXTGEN_TYPES.MANCER:
|
||||
url += '/oai/v1/completions';
|
||||
break;
|
||||
case TEXTGEN_TYPES.LLAMACPP:
|
||||
url += '/completion';
|
||||
break;
|
||||
case TEXTGEN_TYPES.OLLAMA:
|
||||
url += '/api/generate';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const args = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request.body),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: controller.signal,
|
||||
timeout: 0,
|
||||
};
|
||||
|
||||
setAdditionalHeaders(request, args, baseUrl);
|
||||
|
||||
if (request.body.api_type === TEXTGEN_TYPES.TOGETHERAI) {
|
||||
const stop = Array.isArray(request.body.stop) ? request.body.stop[0] : '';
|
||||
request.body = _.pickBy(request.body, (_, key) => TOGETHERAI_KEYS.includes(key));
|
||||
if (typeof stop === 'string' && stop.length > 0) {
|
||||
request.body.stop = stop;
|
||||
}
|
||||
args.body = JSON.stringify(request.body);
|
||||
}
|
||||
|
||||
if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
|
||||
args.body = JSON.stringify({
|
||||
model: request.body.model,
|
||||
prompt: request.body.prompt,
|
||||
stream: request.body.stream ?? false,
|
||||
raw: true,
|
||||
options: _.pickBy(request.body, (_, key) => OLLAMA_KEYS.includes(key)),
|
||||
});
|
||||
}
|
||||
|
||||
if (request.body.api_type === TEXTGEN_TYPES.OLLAMA && request.body.stream) {
|
||||
const stream = await fetch(url, args);
|
||||
parseOllamaStream(stream, request, response);
|
||||
} else if (request.body.stream) {
|
||||
const completionsStream = await fetch(url, args);
|
||||
// Pipe remote SSE stream to Express response
|
||||
forwardFetchResponse(completionsStream, response);
|
||||
}
|
||||
else {
|
||||
const completionsReply = await fetch(url, args);
|
||||
|
||||
if (completionsReply.ok) {
|
||||
const data = await completionsReply.json();
|
||||
console.log('Endpoint response:', data);
|
||||
|
||||
// Wrap legacy response to OAI completions format
|
||||
if (request.body.legacy_api) {
|
||||
const text = data?.results[0]?.text;
|
||||
data['choices'] = [{ text }];
|
||||
}
|
||||
|
||||
return response.send(data);
|
||||
} else {
|
||||
const text = await completionsReply.text();
|
||||
const errorBody = { error: true, status: completionsReply.status, response: text };
|
||||
|
||||
if (!response.headersSent) {
|
||||
return response.send(errorBody);
|
||||
}
|
||||
|
||||
return response.end();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
let value = { error: true, status: error?.status, response: error?.statusText };
|
||||
console.log('Endpoint error:', error);
|
||||
|
||||
if (!response.headersSent) {
|
||||
return response.send(value);
|
||||
}
|
||||
|
||||
return response.end();
|
||||
}
|
||||
});
|
||||
|
||||
const ollama = express.Router();
|
||||
|
||||
ollama.post('/download', jsonParser, async function (request, response) {
|
||||
try {
|
||||
if (!request.body.name || !request.body.api_server) return response.sendStatus(400);
|
||||
|
||||
const name = request.body.name;
|
||||
const url = String(request.body.api_server).replace(/\/$/, '');
|
||||
|
||||
const fetchResponse = await fetch(`${url}/api/pull`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
stream: false,
|
||||
}),
|
||||
timeout: 0,
|
||||
});
|
||||
|
||||
if (!fetchResponse.ok) {
|
||||
console.log('Download error:', fetchResponse.status, fetchResponse.statusText);
|
||||
return response.status(fetchResponse.status).send({ error: true });
|
||||
}
|
||||
|
||||
return response.send({ ok: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return response.status(500);
|
||||
}
|
||||
});
|
||||
|
||||
ollama.post('/caption-image', jsonParser, async function (request, response) {
|
||||
try {
|
||||
if (!request.body.server_url || !request.body.model) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
console.log('Ollama caption request:', request.body);
|
||||
const baseUrl = trimV1(request.body.server_url);
|
||||
|
||||
const fetchResponse = await fetch(`${baseUrl}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: request.body.model,
|
||||
prompt: request.body.prompt,
|
||||
images: [request.body.image],
|
||||
stream: false,
|
||||
}),
|
||||
timeout: 0,
|
||||
});
|
||||
|
||||
if (!fetchResponse.ok) {
|
||||
console.log('Ollama caption error:', fetchResponse.status, fetchResponse.statusText);
|
||||
return response.status(500).send({ error: true });
|
||||
}
|
||||
|
||||
const data = await fetchResponse.json();
|
||||
console.log('Ollama caption response:', data);
|
||||
|
||||
const caption = data?.response || '';
|
||||
|
||||
if (!caption) {
|
||||
console.log('Ollama caption is empty.');
|
||||
return response.status(500).send({ error: true });
|
||||
}
|
||||
|
||||
return response.send({ caption });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return response.status(500);
|
||||
}
|
||||
});
|
||||
|
||||
const llamacpp = express.Router();
|
||||
|
||||
llamacpp.post('/caption-image', jsonParser, async function (request, response) {
|
||||
try {
|
||||
if (!request.body.server_url) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
console.log('LlamaCpp caption request:', request.body);
|
||||
const baseUrl = trimV1(request.body.server_url);
|
||||
|
||||
const fetchResponse = await fetch(`${baseUrl}/completion`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 0,
|
||||
body: JSON.stringify({
|
||||
prompt: `USER:[img-1]${String(request.body.prompt).trim()}\nASSISTANT:`,
|
||||
image_data: [{ data: request.body.image, id: 1 }],
|
||||
temperature: 0.1,
|
||||
stream: false,
|
||||
stop: ['USER:', '</s>'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!fetchResponse.ok) {
|
||||
console.log('LlamaCpp caption error:', fetchResponse.status, fetchResponse.statusText);
|
||||
return response.status(500).send({ error: true });
|
||||
}
|
||||
|
||||
const data = await fetchResponse.json();
|
||||
console.log('LlamaCpp caption response:', data);
|
||||
|
||||
const caption = data?.content || '';
|
||||
|
||||
if (!caption) {
|
||||
console.log('LlamaCpp caption is empty.');
|
||||
return response.status(500).send({ error: true });
|
||||
}
|
||||
|
||||
return response.send({ caption });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return response.status(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/ollama', ollama);
|
||||
router.use('/llamacpp', llamacpp);
|
||||
|
||||
module.exports = { router };
|
@ -2,7 +2,6 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { DIRECTORIES, UPLOADS_PATH } = require('../constants');
|
||||
@ -17,17 +16,6 @@ router.post('/all', jsonParser, function (request, response) {
|
||||
|
||||
});
|
||||
|
||||
router.post('/set', jsonParser, function (request, response) {
|
||||
try {
|
||||
const bg = `#bg1 {background-image: url('../backgrounds/${request.body.bg}');}`;
|
||||
writeFileAtomicSync('public/css/bg_load.css', bg, 'utf8');
|
||||
response.send({ result: 'ok' });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
response.send(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/delete', jsonParser, function (request, response) {
|
||||
if (!request.body) return response.sendStatus(400);
|
||||
|
||||
|
@ -4,6 +4,7 @@ const readline = require('readline');
|
||||
const express = require('express');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const yaml = require('yaml');
|
||||
const _ = require('lodash');
|
||||
|
||||
const encode = require('png-chunks-encode');
|
||||
@ -19,11 +20,29 @@ const characterCardParser = require('../character-card-parser.js');
|
||||
const { readWorldInfoFile } = require('./worldinfo');
|
||||
const { invalidateThumbnail } = require('./thumbnails');
|
||||
const { importRisuSprites } = require('./sprites');
|
||||
const defaultAvatarPath = './public/img/ai4.png';
|
||||
|
||||
let characters = {};
|
||||
|
||||
// KV-store for parsed character data
|
||||
const characterDataCache = new Map();
|
||||
|
||||
/**
|
||||
* Reads the character card from the specified image file.
|
||||
* @param {string} img_url - Path to the image file
|
||||
* @param {string} input_format - 'png'
|
||||
* @returns {Promise<string | undefined>} - Character card data
|
||||
*/
|
||||
async function charaRead(img_url, input_format) {
|
||||
return characterCardParser.parse(img_url, input_format);
|
||||
const stat = fs.statSync(img_url);
|
||||
const cacheKey = `${img_url}-${stat.mtimeMs}`;
|
||||
if (characterDataCache.has(cacheKey)) {
|
||||
return characterDataCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const result = characterCardParser.parse(img_url, input_format);
|
||||
characterDataCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,6 +51,13 @@ async function charaRead(img_url, input_format) {
|
||||
*/
|
||||
async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok', crop = undefined) {
|
||||
try {
|
||||
// Reset the cache
|
||||
for (const key of characterDataCache.keys()) {
|
||||
if (key.startsWith(img_url)) {
|
||||
characterDataCache.delete(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Read the image, resize, and save it as a PNG into the buffer
|
||||
const image = await tryReadImage(img_url, crop);
|
||||
|
||||
@ -370,6 +396,36 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a character from a YAML file.
|
||||
* @param {string} uploadPath Path to the uploaded file
|
||||
* @param {import('express').Response} response Express response object
|
||||
*/
|
||||
function importFromYaml(uploadPath, response) {
|
||||
const fileText = fs.readFileSync(uploadPath, 'utf8');
|
||||
fs.rmSync(uploadPath);
|
||||
const yamlData = yaml.parse(fileText);
|
||||
console.log('importing from yaml');
|
||||
yamlData.name = sanitize(yamlData.name);
|
||||
const fileName = getPngName(yamlData.name);
|
||||
let char = convertToV2({
|
||||
'name': yamlData.name,
|
||||
'description': yamlData.context ?? '',
|
||||
'first_mes': yamlData.greeting ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'chat': `${yamlData.name} - ${humanizedISO8601DateTime()}`,
|
||||
'personality': '',
|
||||
'creatorcomment': '',
|
||||
'avatar': 'none',
|
||||
'mes_example': '',
|
||||
'scenario': '',
|
||||
'talkativeness': 0.5,
|
||||
'creator': '',
|
||||
'tags': '',
|
||||
});
|
||||
charaWrite(defaultAvatarPath, JSON.stringify(char), fileName, response, { file_name: fileName });
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/create', urlencodedParser, async function (request, response) {
|
||||
@ -736,144 +792,147 @@ function getPngName(file) {
|
||||
}
|
||||
|
||||
router.post('/import', urlencodedParser, async function (request, response) {
|
||||
|
||||
if (!request.body || request.file === undefined) return response.sendStatus(400);
|
||||
if (!request.body || !request.file) return response.sendStatus(400);
|
||||
|
||||
let png_name = '';
|
||||
let filedata = request.file;
|
||||
let uploadPath = path.join(UPLOADS_PATH, filedata.filename);
|
||||
var format = request.body.file_type;
|
||||
const defaultAvatarPath = './public/img/ai4.png';
|
||||
//console.log(format);
|
||||
if (filedata) {
|
||||
if (format == 'json') {
|
||||
fs.readFile(uploadPath, 'utf8', async (err, data) => {
|
||||
fs.unlinkSync(uploadPath);
|
||||
let format = request.body.file_type;
|
||||
|
||||
if (err) {
|
||||
console.log(err);
|
||||
response.send({ error: true });
|
||||
}
|
||||
if (format == 'yaml' || format == 'yml') {
|
||||
try {
|
||||
importFromYaml(uploadPath, response);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
response.send({ error: true });
|
||||
}
|
||||
} else if (format == 'json') {
|
||||
fs.readFile(uploadPath, 'utf8', async (err, data) => {
|
||||
fs.unlinkSync(uploadPath);
|
||||
|
||||
let jsonData = JSON.parse(data);
|
||||
|
||||
if (jsonData.spec !== undefined) {
|
||||
console.log('importing from v2 json');
|
||||
importRisuSprites(jsonData);
|
||||
unsetFavFlag(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
jsonData['create_date'] = humanizedISO8601DateTime();
|
||||
png_name = getPngName(jsonData.data?.name || jsonData.name);
|
||||
let char = JSON.stringify(jsonData);
|
||||
charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
|
||||
} else if (jsonData.name !== undefined) {
|
||||
console.log('importing from v1 json');
|
||||
jsonData.name = sanitize(jsonData.name);
|
||||
if (jsonData.creator_notes) {
|
||||
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
|
||||
}
|
||||
png_name = getPngName(jsonData.name);
|
||||
let char = {
|
||||
'name': jsonData.name,
|
||||
'description': jsonData.description ?? '',
|
||||
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
|
||||
'personality': jsonData.personality ?? '',
|
||||
'first_mes': jsonData.first_mes ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'mes_example': jsonData.mes_example ?? '',
|
||||
'scenario': jsonData.scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
};
|
||||
char = convertToV2(char);
|
||||
let charJSON = JSON.stringify(char);
|
||||
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
|
||||
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
|
||||
console.log('importing from gradio json');
|
||||
jsonData.char_name = sanitize(jsonData.char_name);
|
||||
if (jsonData.creator_notes) {
|
||||
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
|
||||
}
|
||||
png_name = getPngName(jsonData.char_name);
|
||||
let char = {
|
||||
'name': jsonData.char_name,
|
||||
'description': jsonData.char_persona ?? '',
|
||||
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
|
||||
'personality': '',
|
||||
'first_mes': jsonData.char_greeting ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'mes_example': jsonData.example_dialogue ?? '',
|
||||
'scenario': jsonData.world_scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
};
|
||||
char = convertToV2(char);
|
||||
let charJSON = JSON.stringify(char);
|
||||
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
|
||||
} else {
|
||||
console.log('Incorrect character format .json');
|
||||
response.send({ error: true });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
var img_data = await charaRead(uploadPath, format);
|
||||
if (img_data === undefined) throw new Error('Failed to read character data');
|
||||
|
||||
let jsonData = JSON.parse(img_data);
|
||||
|
||||
jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
|
||||
png_name = getPngName(jsonData.name);
|
||||
|
||||
if (jsonData.spec !== undefined) {
|
||||
console.log('Found a v2 character file.');
|
||||
importRisuSprites(jsonData);
|
||||
unsetFavFlag(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
jsonData['create_date'] = humanizedISO8601DateTime();
|
||||
const char = JSON.stringify(jsonData);
|
||||
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
|
||||
fs.unlinkSync(uploadPath);
|
||||
} else if (jsonData.name !== undefined) {
|
||||
console.log('Found a v1 character file.');
|
||||
|
||||
if (jsonData.creator_notes) {
|
||||
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
|
||||
}
|
||||
|
||||
let char = {
|
||||
'name': jsonData.name,
|
||||
'description': jsonData.description ?? '',
|
||||
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
|
||||
'personality': jsonData.personality ?? '',
|
||||
'first_mes': jsonData.first_mes ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'mes_example': jsonData.mes_example ?? '',
|
||||
'scenario': jsonData.scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
};
|
||||
char = convertToV2(char);
|
||||
const charJSON = JSON.stringify(char);
|
||||
await charaWrite(uploadPath, charJSON, png_name, response, { file_name: png_name });
|
||||
fs.unlinkSync(uploadPath);
|
||||
} else {
|
||||
console.log('Unknown character card format');
|
||||
response.send({ error: true });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
response.send({ error: true });
|
||||
}
|
||||
|
||||
let jsonData = JSON.parse(data);
|
||||
|
||||
if (jsonData.spec !== undefined) {
|
||||
console.log('importing from v2 json');
|
||||
importRisuSprites(jsonData);
|
||||
unsetFavFlag(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
jsonData['create_date'] = humanizedISO8601DateTime();
|
||||
png_name = getPngName(jsonData.data?.name || jsonData.name);
|
||||
let char = JSON.stringify(jsonData);
|
||||
charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
|
||||
} else if (jsonData.name !== undefined) {
|
||||
console.log('importing from v1 json');
|
||||
jsonData.name = sanitize(jsonData.name);
|
||||
if (jsonData.creator_notes) {
|
||||
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
|
||||
}
|
||||
png_name = getPngName(jsonData.name);
|
||||
let char = {
|
||||
'name': jsonData.name,
|
||||
'description': jsonData.description ?? '',
|
||||
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
|
||||
'personality': jsonData.personality ?? '',
|
||||
'first_mes': jsonData.first_mes ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'mes_example': jsonData.mes_example ?? '',
|
||||
'scenario': jsonData.scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
};
|
||||
char = convertToV2(char);
|
||||
let charJSON = JSON.stringify(char);
|
||||
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
|
||||
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
|
||||
console.log('importing from gradio json');
|
||||
jsonData.char_name = sanitize(jsonData.char_name);
|
||||
if (jsonData.creator_notes) {
|
||||
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
|
||||
}
|
||||
png_name = getPngName(jsonData.char_name);
|
||||
let char = {
|
||||
'name': jsonData.char_name,
|
||||
'description': jsonData.char_persona ?? '',
|
||||
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
|
||||
'personality': '',
|
||||
'first_mes': jsonData.char_greeting ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'mes_example': jsonData.example_dialogue ?? '',
|
||||
'scenario': jsonData.world_scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
};
|
||||
char = convertToV2(char);
|
||||
let charJSON = JSON.stringify(char);
|
||||
charaWrite(defaultAvatarPath, charJSON, png_name, response, { file_name: png_name });
|
||||
} else {
|
||||
console.log('Incorrect character format .json');
|
||||
response.send({ error: true });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
var img_data = await charaRead(uploadPath, format);
|
||||
if (img_data === undefined) throw new Error('Failed to read character data');
|
||||
|
||||
let jsonData = JSON.parse(img_data);
|
||||
|
||||
jsonData.name = sanitize(jsonData.data?.name || jsonData.name);
|
||||
png_name = getPngName(jsonData.name);
|
||||
|
||||
if (jsonData.spec !== undefined) {
|
||||
console.log('Found a v2 character file.');
|
||||
importRisuSprites(jsonData);
|
||||
unsetFavFlag(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
jsonData['create_date'] = humanizedISO8601DateTime();
|
||||
const char = JSON.stringify(jsonData);
|
||||
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
|
||||
fs.unlinkSync(uploadPath);
|
||||
} else if (jsonData.name !== undefined) {
|
||||
console.log('Found a v1 character file.');
|
||||
|
||||
if (jsonData.creator_notes) {
|
||||
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
|
||||
}
|
||||
|
||||
let char = {
|
||||
'name': jsonData.name,
|
||||
'description': jsonData.description ?? '',
|
||||
'creatorcomment': jsonData.creatorcomment ?? jsonData.creator_notes ?? '',
|
||||
'personality': jsonData.personality ?? '',
|
||||
'first_mes': jsonData.first_mes ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'mes_example': jsonData.mes_example ?? '',
|
||||
'scenario': jsonData.scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
};
|
||||
char = convertToV2(char);
|
||||
const charJSON = JSON.stringify(char);
|
||||
await charaWrite(uploadPath, charJSON, png_name, response, { file_name: png_name });
|
||||
fs.unlinkSync(uploadPath);
|
||||
} else {
|
||||
console.log('Unknown character card format');
|
||||
response.send({ error: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
response.send({ error: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -10,9 +10,9 @@ const { DIRECTORIES, UPLOADS_PATH } = require('../constants');
|
||||
const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string} chat
|
||||
* Saves a chat to the backups directory.
|
||||
* @param {string} name The name of the chat.
|
||||
* @param {string} chat The serialized chat to save.
|
||||
*/
|
||||
function backupChat(name, chat) {
|
||||
try {
|
||||
@ -65,7 +65,6 @@ router.post('/get', jsonParser, function (request, response) {
|
||||
return response.send({});
|
||||
}
|
||||
|
||||
|
||||
if (!request.body.file_name) {
|
||||
return response.send({});
|
||||
}
|
||||
@ -140,7 +139,6 @@ router.post('/delete', jsonParser, function (request, response) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
return response.send('ok');
|
||||
});
|
||||
|
||||
@ -190,6 +188,10 @@ router.post('/export', jsonParser, async function (request, response) {
|
||||
let buffer = '';
|
||||
rl.on('line', (line) => {
|
||||
const data = JSON.parse(line);
|
||||
// Skip non-printable/prompt-hidden messages
|
||||
if (data.is_system) {
|
||||
return;
|
||||
}
|
||||
if (data.mes) {
|
||||
const name = data.name;
|
||||
const message = (data?.extra?.display_text || data?.mes || '').replace(/\r?\n/g, '\n');
|
||||
|
66
src/endpoints/google.js
Normal file
66
src/endpoints/google.js
Normal file
@ -0,0 +1,66 @@
|
||||
const { readSecret, SECRET_KEYS } = require('./secrets');
|
||||
const fetch = require('node-fetch').default;
|
||||
const express = require('express');
|
||||
const { jsonParser } = require('../express-common');
|
||||
const { GEMINI_SAFETY } = require('../constants');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/caption-image', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const mimeType = request.body.image.split(';')[0].split(':')[1];
|
||||
const base64Data = request.body.image.split(',')[1];
|
||||
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=${readSecret(SECRET_KEYS.MAKERSUITE)}`;
|
||||
const body = {
|
||||
contents: [{
|
||||
parts: [
|
||||
{ text: request.body.prompt },
|
||||
{
|
||||
inlineData: {
|
||||
mimeType: 'image/png', // It needs to specify a MIME type in data if it's not a PNG
|
||||
data: mimeType === 'image/png' ? base64Data : request.body.image,
|
||||
},
|
||||
}],
|
||||
}],
|
||||
safetySettings: GEMINI_SAFETY,
|
||||
generationConfig: { maxOutputTokens: 1000 },
|
||||
};
|
||||
|
||||
console.log('Multimodal captioning request', body);
|
||||
|
||||
const result = await fetch(url, {
|
||||
body: JSON.stringify(body),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 0,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const error = await result.json();
|
||||
console.log(`MakerSuite API returned error: ${result.status} ${result.statusText}`, error);
|
||||
return response.status(result.status).send({ error: true });
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
console.log('Multimodal captioning response', data);
|
||||
|
||||
const candidates = data?.candidates;
|
||||
if (!candidates) {
|
||||
return response.status(500).send('No candidates found, image was most likely filtered.');
|
||||
}
|
||||
|
||||
const caption = candidates[0].content.parts[0].text;
|
||||
if (!caption) {
|
||||
return response.status(500).send('No caption found');
|
||||
}
|
||||
|
||||
return response.json({ caption });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).send('Internal server error');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { router };
|
@ -73,6 +73,7 @@ router.post('/create', jsonParser, (request, response) => {
|
||||
fav: request.body.fav,
|
||||
chat_id: request.body.chat_id ?? id,
|
||||
chats: request.body.chats ?? [id],
|
||||
auto_mode_delay: request.body.auto_mode_delay ?? 5,
|
||||
};
|
||||
const pathToFile = path.join(DIRECTORIES.groups, `${id}.json`);
|
||||
const fileData = JSON.stringify(groupMetadata);
|
||||
|
@ -1,20 +1,30 @@
|
||||
const fetch = require('node-fetch').default;
|
||||
const express = require('express');
|
||||
const AIHorde = require('../ai_horde');
|
||||
const { getVersion, delay } = require('../util');
|
||||
const { getVersion, delay, Cache } = require('../util');
|
||||
const { readSecret, SECRET_KEYS } = require('./secrets');
|
||||
const { jsonParser } = require('../express-common');
|
||||
|
||||
const ANONYMOUS_KEY = '0000000000';
|
||||
const cache = new Cache(60 * 1000);
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Returns the AIHorde client agent.
|
||||
* @returns {Promise<string>} AIHorde client agent
|
||||
*/
|
||||
async function getClientAgent() {
|
||||
const version = await getVersion();
|
||||
return version?.agent || 'SillyTavern:UNKNOWN:Cohee#1207';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the AIHorde client.
|
||||
* @returns {Promise<AIHorde>} AIHorde client
|
||||
*/
|
||||
async function getHordeClient() {
|
||||
const version = await getVersion();
|
||||
const ai_horde = new AIHorde({
|
||||
client_agent: version?.agent || 'SillyTavern:UNKNOWN:Cohee#1207',
|
||||
client_agent: await getClientAgent(),
|
||||
});
|
||||
return ai_horde;
|
||||
}
|
||||
@ -36,29 +46,122 @@ function sanitizeHordeImagePrompt(prompt) {
|
||||
prompt = prompt.replace(/\b(boy)\b/gmi, 'man');
|
||||
prompt = prompt.replace(/\b(girls)\b/gmi, 'women');
|
||||
prompt = prompt.replace(/\b(boys)\b/gmi, 'men');
|
||||
|
||||
//always remove these high risk words from prompt, as they add little value to image gen while increasing the risk the prompt gets flagged
|
||||
prompt = prompt.replace(/\b(under.age|under.aged|underage|underaged|loli|pedo|pedophile|(\w+).year.old|(\w+).years.old|minor|prepubescent|minors|shota)\b/gmi, '');
|
||||
|
||||
//if nsfw is detected, do not remove it but apply additional precautions
|
||||
let isNsfw = prompt.match(/\b(cock|ahegao|hentai|uncensored|lewd|cocks|deepthroat|deepthroating|dick|dicks|cumshot|lesbian|fuck|fucked|fucking|sperm|naked|nipples|tits|boobs|breasts|boob|breast|topless|ass|butt|fingering|masturbate|masturbating|bitch|blowjob|pussy|piss|asshole|dildo|dildos|vibrator|erection|foreskin|handjob|nude|penis|porn|vibrator|virgin|vagina|vulva|threesome|orgy|bdsm|hickey|condom|testicles|anal|bareback|bukkake|creampie|stripper|strap-on|missionary|clitoris|clit|clitty|cowgirl|fleshlight|sex|buttplug|milf|oral|sucking|bondage|orgasm|scissoring|railed|slut|sluts|slutty|cumming|cunt|faggot|sissy|anal|anus|cum|semen|scat|nsfw|xxx|explicit|erotic|horny|aroused|jizz|moan|rape|raped|raping|throbbing|humping)\b/gmi);
|
||||
|
||||
if (isNsfw) {
|
||||
//replace risky subject nouns with person
|
||||
prompt = prompt.replace(/\b(youngster|infant|baby|toddler|child|teen|kid|kiddie|kiddo|teenager|student|preteen|pre.teen)\b/gmi, 'person');
|
||||
|
||||
//remove risky adjectives and related words
|
||||
prompt = prompt.replace(/\b(young|younger|youthful|youth|small|smaller|smallest|girly|boyish|lil|tiny|teenaged|lit[tl]le|school.aged|school|highschool|kindergarten|teens|children|kids)\b/gmi, '');
|
||||
}
|
||||
//replace risky subject nouns with person
|
||||
prompt = prompt.replace(/\b(youngster|infant|baby|toddler|child|teen|kid|kiddie|kiddo|teenager|student|preteen|pre.teen)\b/gmi, 'person');
|
||||
//remove risky adjectives and related words
|
||||
prompt = prompt.replace(/\b(young|younger|youthful|youth|small|smaller|smallest|girly|boyish|lil|tiny|teenaged|lit[tl]le|school.aged|school|highschool|kindergarten|teens|children|kids)\b/gmi, '');
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
router.post('/text-workers', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const cachedWorkers = cache.get('workers');
|
||||
|
||||
if (cachedWorkers && !request.body.force) {
|
||||
return response.send(cachedWorkers);
|
||||
}
|
||||
|
||||
const agent = await getClientAgent();
|
||||
const fetchResult = await fetch('https://horde.koboldai.net/api/v2/workers?type=text', {
|
||||
headers: {
|
||||
'Client-Agent': agent,
|
||||
},
|
||||
});
|
||||
const data = await fetchResult.json();
|
||||
cache.set('workers', data);
|
||||
return response.send(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/text-models', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const cachedModels = cache.get('models');
|
||||
|
||||
if (cachedModels && !request.body.force) {
|
||||
return response.send(cachedModels);
|
||||
}
|
||||
|
||||
const agent = await getClientAgent();
|
||||
const fetchResult = await fetch('https://horde.koboldai.net/api/v2/status/models?type=text', {
|
||||
headers: {
|
||||
'Client-Agent': agent,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await fetchResult.json();
|
||||
cache.set('models', data);
|
||||
return response.send(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/status', jsonParser, async (_, response) => {
|
||||
try {
|
||||
const agent = await getClientAgent();
|
||||
const fetchResult = await fetch('https://horde.koboldai.net/api/v2/status/heartbeat', {
|
||||
headers: {
|
||||
'Client-Agent': agent,
|
||||
},
|
||||
});
|
||||
|
||||
return response.send({ ok: fetchResult.ok });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/cancel-task', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const taskId = request.body.taskId;
|
||||
const agent = await getClientAgent();
|
||||
const fetchResult = await fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${taskId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Client-Agent': agent,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await fetchResult.json();
|
||||
console.log(`Cancelled Horde task ${taskId}`);
|
||||
return response.send(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/task-status', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const taskId = request.body.taskId;
|
||||
const agent = await getClientAgent();
|
||||
const fetchResult = await fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${taskId}`, {
|
||||
headers: {
|
||||
'Client-Agent': agent,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await fetchResult.json();
|
||||
console.log(`Horde task ${taskId} status:`, data);
|
||||
return response.send(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/generate-text', jsonParser, async (request, response) => {
|
||||
const api_key_horde = readSecret(SECRET_KEYS.HORDE) || ANONYMOUS_KEY;
|
||||
const apiKey = readSecret(SECRET_KEYS.HORDE) || ANONYMOUS_KEY;
|
||||
const url = 'https://horde.koboldai.net/api/v2/generate/text/async';
|
||||
const agent = await getClientAgent();
|
||||
|
||||
console.log(request.body);
|
||||
try {
|
||||
@ -67,8 +170,8 @@ router.post('/generate-text', jsonParser, async (request, response) => {
|
||||
body: JSON.stringify(request.body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': api_key_horde,
|
||||
'Client-Agent': String(request.header('Client-Agent')),
|
||||
'apikey': apiKey,
|
||||
'Client-Agent': agent,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
const fetch = require('node-fetch').default;
|
||||
const express = require('express');
|
||||
const util = require('util');
|
||||
const { Readable } = require('stream');
|
||||
const { readSecret, SECRET_KEYS } = require('./secrets');
|
||||
const { readAllChunks, extractFileFromZipBuffer } = require('../util');
|
||||
const { readAllChunks, extractFileFromZipBuffer, forwardFetchResponse } = require('../util');
|
||||
const { jsonParser } = require('../express-common');
|
||||
|
||||
const API_NOVELAI = 'https://api.novelai.net';
|
||||
@ -190,17 +189,7 @@ router.post('/generate', jsonParser, async function (req, res) {
|
||||
|
||||
if (req.body.streaming) {
|
||||
// Pipe remote SSE stream to Express response
|
||||
response.body.pipe(res);
|
||||
|
||||
req.socket.on('close', function () {
|
||||
if (response.body instanceof Readable) response.body.destroy(); // Close the remote stream
|
||||
res.end(); // End the Express response
|
||||
});
|
||||
|
||||
response.body.on('end', function () {
|
||||
console.log('Streaming request finished');
|
||||
res.end();
|
||||
});
|
||||
forwardFetchResponse(response, res);
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
|
@ -4,22 +4,35 @@ const express = require('express');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml } = require('../util');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/caption-image', jsonParser, async (request, response) => {
|
||||
try {
|
||||
let key = '';
|
||||
let headers = {};
|
||||
let bodyParams = {};
|
||||
|
||||
if (request.body.api === 'openai') {
|
||||
if (request.body.api === 'openai' && !request.body.reverse_proxy) {
|
||||
key = readSecret(SECRET_KEYS.OPENAI);
|
||||
}
|
||||
|
||||
if (request.body.api === 'openrouter') {
|
||||
if (request.body.api === 'openrouter' && !request.body.reverse_proxy) {
|
||||
key = readSecret(SECRET_KEYS.OPENROUTER);
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
if (request.body.reverse_proxy && request.body.proxy_password) {
|
||||
key = request.body.proxy_password;
|
||||
}
|
||||
|
||||
if (request.body.api === 'custom') {
|
||||
key = readSecret(SECRET_KEYS.CUSTOM);
|
||||
mergeObjectWithYaml(bodyParams, request.body.custom_include_body);
|
||||
mergeObjectWithYaml(headers, request.body.custom_include_headers);
|
||||
}
|
||||
|
||||
if (!key && !request.body.reverse_proxy && request.body.api !== 'custom') {
|
||||
console.log('No key found for API', request.body.api);
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
@ -36,12 +49,24 @@ router.post('/caption-image', jsonParser, async (request, response) => {
|
||||
},
|
||||
],
|
||||
max_tokens: 500,
|
||||
...bodyParams,
|
||||
};
|
||||
|
||||
const captionSystemPrompt = getConfigValue('openai.captionSystemPrompt');
|
||||
if (captionSystemPrompt) {
|
||||
body.messages.unshift({
|
||||
role: 'system',
|
||||
content: captionSystemPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
if (request.body.api === 'custom') {
|
||||
excludeKeysByYaml(body, request.body.custom_exclude_body);
|
||||
}
|
||||
|
||||
console.log('Multimodal captioning request', body);
|
||||
|
||||
let apiUrl = '';
|
||||
let headers = {};
|
||||
|
||||
if (request.body.api === 'openrouter') {
|
||||
apiUrl = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
@ -52,6 +77,14 @@ router.post('/caption-image', jsonParser, async (request, response) => {
|
||||
apiUrl = 'https://api.openai.com/v1/chat/completions';
|
||||
}
|
||||
|
||||
if (request.body.reverse_proxy) {
|
||||
apiUrl = `${request.body.reverse_proxy}/chat/completions`;
|
||||
}
|
||||
|
||||
if (request.body.api === 'custom') {
|
||||
apiUrl = `${request.body.server_url}/chat/completions`;
|
||||
}
|
||||
|
||||
const result = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
159
src/endpoints/prompt-converters.js
Normal file
159
src/endpoints/prompt-converters.js
Normal file
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Convert a prompt from the ChatML objects to the format used by Claude.
|
||||
* @param {object[]} messages Array of messages
|
||||
* @param {boolean} addAssistantPostfix Add Assistant postfix.
|
||||
* @param {string} addAssistantPrefill Add Assistant prefill after the assistant postfix.
|
||||
* @param {boolean} withSysPromptSupport Indicates if the Claude model supports the system prompt format.
|
||||
* @param {boolean} useSystemPrompt Indicates if the system prompt format should be used.
|
||||
* @param {string} addSysHumanMsg Add Human message between system prompt and assistant.
|
||||
* @returns {string} Prompt for Claude
|
||||
* @copyright Prompt Conversion script taken from RisuAI by kwaroran (GPLv3).
|
||||
*/
|
||||
function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill, withSysPromptSupport, useSystemPrompt, addSysHumanMsg) {
|
||||
|
||||
//Prepare messages for claude.
|
||||
if (messages.length > 0) {
|
||||
messages[0].role = 'system';
|
||||
//Add the assistant's message to the end of messages.
|
||||
if (addAssistantPostfix) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: addAssistantPrefill || '',
|
||||
});
|
||||
}
|
||||
// Find the index of the first message with an assistant role and check for a "'user' role/Human:" before it.
|
||||
let hasUser = false;
|
||||
const firstAssistantIndex = messages.findIndex((message, i) => {
|
||||
if (i >= 0 && (message.role === 'user' || message.content.includes('\n\nHuman: '))) {
|
||||
hasUser = true;
|
||||
}
|
||||
return message.role === 'assistant' && i > 0;
|
||||
});
|
||||
// When 2.1+ and 'Use system prompt" checked, switches to the system prompt format by setting the first message's role to the 'system'.
|
||||
// Inserts the human's message before the first the assistant one, if there are no such message or prefix found.
|
||||
if (withSysPromptSupport && useSystemPrompt) {
|
||||
messages[0].role = 'system';
|
||||
if (firstAssistantIndex > 0 && addSysHumanMsg && !hasUser) {
|
||||
messages.splice(firstAssistantIndex, 0, {
|
||||
role: 'user',
|
||||
content: addSysHumanMsg,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use the default message format by setting the first message's role to 'user'(compatible with all claude models including 2.1.)
|
||||
messages[0].role = 'user';
|
||||
// Fix messages order for default message format when(messages > Context Size) by merging two messages with "\n\nHuman: " prefixes into one, before the first Assistant's message.
|
||||
if (firstAssistantIndex > 0) {
|
||||
messages[firstAssistantIndex - 1].role = firstAssistantIndex - 1 !== 0 && messages[firstAssistantIndex - 1].role === 'user' ? 'FixHumMsg' : messages[firstAssistantIndex - 1].role;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert messages to the prompt.
|
||||
let requestPrompt = messages.map((v, i) => {
|
||||
// Set prefix according to the role.
|
||||
let prefix = {
|
||||
'assistant': '\n\nAssistant: ',
|
||||
'user': '\n\nHuman: ',
|
||||
'system': i === 0 ? '' : v.name === 'example_assistant' ? '\n\nA: ' : v.name === 'example_user' ? '\n\nH: ' : '\n\n',
|
||||
'FixHumMsg': '\n\nFirst message: ',
|
||||
}[v.role] ?? '';
|
||||
// Claude doesn't support message names, so we'll just add them to the message content.
|
||||
return `${prefix}${v.name && v.role !== 'system' ? `${v.name}: ` : ''}${v.content}`;
|
||||
}).join('');
|
||||
|
||||
return requestPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a prompt from the ChatML objects to the format used by Google MakerSuite models.
|
||||
* @param {object[]} messages Array of messages
|
||||
* @param {string} model Model name
|
||||
* @returns {object[]} Prompt for Google MakerSuite models
|
||||
*/
|
||||
function convertGooglePrompt(messages, model) {
|
||||
// This is a 1x1 transparent PNG
|
||||
const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
const contents = [];
|
||||
let lastRole = '';
|
||||
let currentText = '';
|
||||
|
||||
const isMultimodal = model === 'gemini-pro-vision';
|
||||
|
||||
if (isMultimodal) {
|
||||
const combinedText = messages.map((message) => {
|
||||
const role = message.role === 'assistant' ? 'MODEL: ' : 'USER: ';
|
||||
return role + message.content;
|
||||
}).join('\n\n').trim();
|
||||
|
||||
const imageEntry = messages.find((message) => message.content?.[1]?.image_url);
|
||||
const imageData = imageEntry?.content?.[1]?.image_url?.data ?? PNG_PIXEL;
|
||||
contents.push({
|
||||
parts: [
|
||||
{ text: combinedText },
|
||||
{
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: imageData,
|
||||
},
|
||||
},
|
||||
],
|
||||
role: 'user',
|
||||
});
|
||||
} else {
|
||||
messages.forEach((message, index) => {
|
||||
const role = message.role === 'assistant' ? 'model' : 'user';
|
||||
if (lastRole === role) {
|
||||
currentText += '\n\n' + message.content;
|
||||
} else {
|
||||
if (currentText !== '') {
|
||||
contents.push({
|
||||
parts: [{ text: currentText.trim() }],
|
||||
role: lastRole,
|
||||
});
|
||||
}
|
||||
currentText = message.content;
|
||||
lastRole = role;
|
||||
}
|
||||
if (index === messages.length - 1) {
|
||||
contents.push({
|
||||
parts: [{ text: currentText.trim() }],
|
||||
role: lastRole,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a prompt from the ChatML objects to the format used by Text Completion API.
|
||||
* @param {object[]} messages Array of messages
|
||||
* @returns {string} Prompt for Text Completion API
|
||||
*/
|
||||
function convertTextCompletionPrompt(messages) {
|
||||
if (typeof messages === 'string') {
|
||||
return messages;
|
||||
}
|
||||
|
||||
const messageStrings = [];
|
||||
messages.forEach(m => {
|
||||
if (m.role === 'system' && m.name === undefined) {
|
||||
messageStrings.push('System: ' + m.content);
|
||||
}
|
||||
else if (m.role === 'system' && m.name !== undefined) {
|
||||
messageStrings.push(m.name + ': ' + m.content);
|
||||
}
|
||||
else {
|
||||
messageStrings.push(m.role + ': ' + m.content);
|
||||
}
|
||||
});
|
||||
return messageStrings.join('\n') + '\nassistant:';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
convertClaudePrompt,
|
||||
convertGooglePrompt,
|
||||
convertTextCompletionPrompt,
|
||||
};
|
@ -23,8 +23,11 @@ const SECRET_KEYS = {
|
||||
SCALE_COOKIE: 'scale_cookie',
|
||||
ONERING_URL: 'oneringtranslator_url',
|
||||
DEEPLX_URL: 'deeplx_url',
|
||||
PALM: 'api_key_palm',
|
||||
MAKERSUITE: 'api_key_makersuite',
|
||||
SERPAPI: 'api_key_serpapi',
|
||||
TOGETHERAI: 'api_key_togetherai',
|
||||
MISTRALAI: 'api_key_mistralai',
|
||||
CUSTOM: 'api_key_custom',
|
||||
};
|
||||
|
||||
/**
|
||||
@ -44,6 +47,17 @@ function writeSecret(key, value) {
|
||||
writeFileAtomicSync(SECRETS_FILE, JSON.stringify(secrets, null, 4), 'utf-8');
|
||||
}
|
||||
|
||||
function deleteSecret(key) {
|
||||
if (!fs.existsSync(SECRETS_FILE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileContents = fs.readFileSync(SECRETS_FILE, 'utf-8');
|
||||
const secrets = JSON.parse(fileContents);
|
||||
delete secrets[key];
|
||||
writeFileAtomicSync(SECRETS_FILE, JSON.stringify(secrets, null, 4), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a secret from the secrets file
|
||||
* @param {string} key Secret key
|
||||
@ -85,6 +99,13 @@ function readSecretState() {
|
||||
* @returns {void}
|
||||
*/
|
||||
function migrateSecrets(settingsFile) {
|
||||
const palmKey = readSecret('api_key_palm');
|
||||
if (palmKey) {
|
||||
console.log('Migrating Palm key...');
|
||||
writeSecret(SECRET_KEYS.MAKERSUITE, palmKey);
|
||||
deleteSecret('api_key_palm');
|
||||
}
|
||||
|
||||
if (!fs.existsSync(settingsFile)) {
|
||||
console.log('Settings file does not exist');
|
||||
return;
|
||||
|
@ -5,6 +5,23 @@ const { jsonParser } = require('../express-common');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Cosplay as Firefox
|
||||
const visitHeaders = {
|
||||
'Accept': 'text/html',
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'TE': 'trailers',
|
||||
'DNT': '1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
};
|
||||
|
||||
router.post('/search', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const key = readSecret(SECRET_KEYS.SERPAPI);
|
||||
@ -31,4 +48,61 @@ router.post('/search', jsonParser, async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/visit', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const url = request.body.url;
|
||||
|
||||
if (!url) {
|
||||
console.log('No url provided for /visit');
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Reject relative URLs
|
||||
if (urlObj.protocol === null || urlObj.host === null) {
|
||||
throw new Error('Invalid URL format');
|
||||
}
|
||||
|
||||
// Reject non-HTTP URLs
|
||||
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
|
||||
throw new Error('Invalid protocol');
|
||||
}
|
||||
|
||||
// Reject URLs with a non-standard port
|
||||
if (urlObj.port !== '') {
|
||||
throw new Error('Invalid port');
|
||||
}
|
||||
|
||||
// Reject IP addresses
|
||||
if (urlObj.hostname.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
throw new Error('Invalid hostname');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Invalid url provided for /visit', url);
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const result = await fetch(url, { headers: visitHeaders });
|
||||
|
||||
if (!result.ok) {
|
||||
console.log(`Visit failed ${result.status} ${result.statusText}`);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
const contentType = String(result.headers.get('content-type'));
|
||||
if (!contentType.includes('text/html')) {
|
||||
console.log(`Visit failed, content-type is ${contentType}, expected text/html`);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
const text = await result.text();
|
||||
return response.send(text);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { router };
|
||||
|
164
src/endpoints/settings.js
Normal file
164
src/endpoints/settings.js
Normal file
@ -0,0 +1,164 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const { DIRECTORIES } = require('../constants');
|
||||
const { getConfigValue, generateTimestamp, removeOldBackups } = require('../util');
|
||||
const { jsonParser } = require('../express-common');
|
||||
const { migrateSecrets } = require('./secrets');
|
||||
|
||||
const enableExtensions = getConfigValue('enableExtensions', true);
|
||||
const SETTINGS_FILE = './public/settings.json';
|
||||
|
||||
function readAndParseFromDirectory(directoryPath, fileExtension = '.json') {
|
||||
const files = fs
|
||||
.readdirSync(directoryPath)
|
||||
.filter(x => path.parse(x).ext == fileExtension)
|
||||
.sort();
|
||||
|
||||
const parsedFiles = [];
|
||||
|
||||
files.forEach(item => {
|
||||
try {
|
||||
const file = fs.readFileSync(path.join(directoryPath, item), 'utf-8');
|
||||
parsedFiles.push(fileExtension == '.json' ? JSON.parse(file) : file);
|
||||
}
|
||||
catch {
|
||||
// skip
|
||||
}
|
||||
});
|
||||
|
||||
return parsedFiles;
|
||||
}
|
||||
|
||||
function sortByName(_) {
|
||||
return (a, b) => a.localeCompare(b);
|
||||
}
|
||||
|
||||
function readPresetsFromDirectory(directoryPath, options = {}) {
|
||||
const {
|
||||
sortFunction,
|
||||
removeFileExtension = false,
|
||||
fileExtension = '.json',
|
||||
} = options;
|
||||
|
||||
const files = fs.readdirSync(directoryPath).sort(sortFunction).filter(x => path.parse(x).ext == fileExtension);
|
||||
const fileContents = [];
|
||||
const fileNames = [];
|
||||
|
||||
files.forEach(item => {
|
||||
try {
|
||||
const file = fs.readFileSync(path.join(directoryPath, item), 'utf8');
|
||||
JSON.parse(file);
|
||||
fileContents.push(file);
|
||||
fileNames.push(removeFileExtension ? item.replace(/\.[^/.]+$/, '') : item);
|
||||
} catch {
|
||||
// skip
|
||||
console.log(`${item} is not a valid JSON`);
|
||||
}
|
||||
});
|
||||
|
||||
return { fileContents, fileNames };
|
||||
}
|
||||
|
||||
function backupSettings() {
|
||||
try {
|
||||
if (!fs.existsSync(DIRECTORIES.backups)) {
|
||||
fs.mkdirSync(DIRECTORIES.backups);
|
||||
}
|
||||
|
||||
const backupFile = path.join(DIRECTORIES.backups, `settings_${generateTimestamp()}.json`);
|
||||
fs.copyFileSync(SETTINGS_FILE, backupFile);
|
||||
|
||||
removeOldBackups('settings_');
|
||||
} catch (err) {
|
||||
console.log('Could not backup settings file', err);
|
||||
}
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/save', jsonParser, function (request, response) {
|
||||
try {
|
||||
writeFileAtomicSync('public/settings.json', JSON.stringify(request.body, null, 4), 'utf8');
|
||||
response.send({ result: 'ok' });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
response.send(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Wintermute's code
|
||||
router.post('/get', jsonParser, (request, response) => {
|
||||
let settings;
|
||||
try {
|
||||
settings = fs.readFileSync('public/settings.json', 'utf8');
|
||||
} catch (e) {
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
// NovelAI Settings
|
||||
const { fileContents: novelai_settings, fileNames: novelai_setting_names }
|
||||
= readPresetsFromDirectory(DIRECTORIES.novelAI_Settings, {
|
||||
sortFunction: sortByName(DIRECTORIES.novelAI_Settings),
|
||||
removeFileExtension: true,
|
||||
});
|
||||
|
||||
// OpenAI Settings
|
||||
const { fileContents: openai_settings, fileNames: openai_setting_names }
|
||||
= readPresetsFromDirectory(DIRECTORIES.openAI_Settings, {
|
||||
sortFunction: sortByName(DIRECTORIES.openAI_Settings), removeFileExtension: true,
|
||||
});
|
||||
|
||||
// TextGenerationWebUI Settings
|
||||
const { fileContents: textgenerationwebui_presets, fileNames: textgenerationwebui_preset_names }
|
||||
= readPresetsFromDirectory(DIRECTORIES.textGen_Settings, {
|
||||
sortFunction: sortByName(DIRECTORIES.textGen_Settings), removeFileExtension: true,
|
||||
});
|
||||
|
||||
//Kobold
|
||||
const { fileContents: koboldai_settings, fileNames: koboldai_setting_names }
|
||||
= readPresetsFromDirectory(DIRECTORIES.koboldAI_Settings, {
|
||||
sortFunction: sortByName(DIRECTORIES.koboldAI_Settings), removeFileExtension: true,
|
||||
});
|
||||
|
||||
const worldFiles = fs
|
||||
.readdirSync(DIRECTORIES.worlds)
|
||||
.filter(file => path.extname(file).toLowerCase() === '.json')
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const world_names = worldFiles.map(item => path.parse(item).name);
|
||||
|
||||
const themes = readAndParseFromDirectory(DIRECTORIES.themes);
|
||||
const movingUIPresets = readAndParseFromDirectory(DIRECTORIES.movingUI);
|
||||
const quickReplyPresets = readAndParseFromDirectory(DIRECTORIES.quickreplies);
|
||||
|
||||
const instruct = readAndParseFromDirectory(DIRECTORIES.instruct);
|
||||
const context = readAndParseFromDirectory(DIRECTORIES.context);
|
||||
|
||||
response.send({
|
||||
settings,
|
||||
koboldai_settings,
|
||||
koboldai_setting_names,
|
||||
world_names,
|
||||
novelai_settings,
|
||||
novelai_setting_names,
|
||||
openai_settings,
|
||||
openai_setting_names,
|
||||
textgenerationwebui_presets,
|
||||
textgenerationwebui_preset_names,
|
||||
themes,
|
||||
movingUIPresets,
|
||||
quickReplyPresets,
|
||||
instruct,
|
||||
context,
|
||||
enable_extensions: enableExtensions,
|
||||
});
|
||||
});
|
||||
|
||||
// Sync for now, but should probably be migrated to async file APIs
|
||||
async function init() {
|
||||
backupSettings();
|
||||
migrateSecrets(SETTINGS_FILE);
|
||||
}
|
||||
|
||||
module.exports = { router, init };
|
@ -1,11 +1,12 @@
|
||||
const express = require('express');
|
||||
const fetch = require('node-fetch').default;
|
||||
const sanitize = require('sanitize-filename');
|
||||
const { getBasicAuthHeader, delay } = require('../util.js');
|
||||
const { getBasicAuthHeader, delay, getHexString } = require('../util.js');
|
||||
const fs = require('fs');
|
||||
const { DIRECTORIES } = require('../constants.js');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const { jsonParser } = require('../express-common');
|
||||
const { readSecret, SECRET_KEYS } = require('./secrets.js');
|
||||
|
||||
/**
|
||||
* Sanitizes a string.
|
||||
@ -545,6 +546,99 @@ comfy.post('/generate', jsonParser, async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
const together = express.Router();
|
||||
|
||||
together.post('/models', jsonParser, async (_, response) => {
|
||||
try {
|
||||
const key = readSecret(SECRET_KEYS.TOGETHERAI);
|
||||
|
||||
if (!key) {
|
||||
console.log('TogetherAI key not found.');
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const modelsResponse = await fetch('https://api.together.xyz/api/models', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!modelsResponse.ok) {
|
||||
console.log('TogetherAI returned an error.');
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
const data = await modelsResponse.json();
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
console.log('TogetherAI returned invalid data.');
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
const models = data
|
||||
.filter(x => x.display_type === 'image')
|
||||
.map(x => ({ value: x.name, text: x.display_name }));
|
||||
|
||||
return response.send(models);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
together.post('/generate', jsonParser, async (request, response) => {
|
||||
try {
|
||||
const key = readSecret(SECRET_KEYS.TOGETHERAI);
|
||||
|
||||
if (!key) {
|
||||
console.log('TogetherAI key not found.');
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
console.log('TogetherAI request:', request.body);
|
||||
|
||||
const result = await fetch('https://api.together.xyz/api/inference', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
request_type: 'image-model-inference',
|
||||
prompt: request.body.prompt,
|
||||
negative_prompt: request.body.negative_prompt,
|
||||
height: request.body.height,
|
||||
width: request.body.width,
|
||||
model: request.body.model,
|
||||
steps: request.body.steps,
|
||||
n: 1,
|
||||
seed: Math.floor(Math.random() * 10_000_000), // Limited to 10000 on playground, works fine with more.
|
||||
sessionKey: getHexString(40), // Don't know if that's supposed to be random or not. It works either way.
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
console.log('TogetherAI returned an error.');
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
console.log('TogetherAI response:', data);
|
||||
|
||||
if (data.status !== 'finished') {
|
||||
console.log('TogetherAI job failed.');
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
return response.send(data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/comfy', comfy);
|
||||
router.use('/together', together);
|
||||
|
||||
module.exports = { router };
|
||||
|
@ -111,7 +111,8 @@ async function generateThumbnail(type, file) {
|
||||
try {
|
||||
const quality = getConfigValue('thumbnailsQuality', 95);
|
||||
const image = await jimp.read(pathToOriginalFile);
|
||||
buffer = await image.cover(mySize[0], mySize[1]).quality(quality).getBufferAsync('image/jpeg');
|
||||
const imgType = type == 'avatar' && getConfigValue('avatarThumbnailsPng', false) ? 'image/png' : 'image/jpeg';
|
||||
buffer = await image.cover(mySize[0], mySize[1]).quality(quality).getBufferAsync(imgType);
|
||||
}
|
||||
catch (inner) {
|
||||
console.warn(`Thumbnailer can not process the image: ${pathToOriginalFile}. Using original size`);
|
||||
|
@ -4,9 +4,11 @@ const express = require('express');
|
||||
const { SentencePieceProcessor } = require('@agnai/sentencepiece-js');
|
||||
const tiktoken = require('@dqbd/tiktoken');
|
||||
const { Tokenizer } = require('@agnai/web-tokenizers');
|
||||
const { convertClaudePrompt } = require('../chat-completion');
|
||||
const { convertClaudePrompt, convertGooglePrompt } = require('./prompt-converters');
|
||||
const { readSecret, SECRET_KEYS } = require('./secrets');
|
||||
const { TEXTGEN_TYPES } = require('../constants');
|
||||
const { jsonParser } = require('../express-common');
|
||||
const { setAdditionalHeaders } = require('../additional-headers');
|
||||
|
||||
/**
|
||||
* @type {{[key: string]: import("@dqbd/tiktoken").Tiktoken}} Tokenizers cache
|
||||
@ -385,6 +387,26 @@ router.post('/ai21/count', jsonParser, async function (req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/google/count', jsonParser, async function (req, res) {
|
||||
if (!req.body) return res.sendStatus(400);
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ contents: convertGooglePrompt(req.body) }),
|
||||
};
|
||||
try {
|
||||
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${req.query.model}:countTokens?key=${readSecret(SECRET_KEYS.MAKERSUITE)}`, options);
|
||||
const data = await response.json();
|
||||
return res.send({ 'token_count': data?.totalTokens || 0 });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.send({ 'token_count': 0 });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/llama/encode', jsonParser, createSentencepieceEncodingHandler(spp_llama));
|
||||
router.post('/nerdstash/encode', jsonParser, createSentencepieceEncodingHandler(spp_nerd));
|
||||
router.post('/nerdstash_v2/encode', jsonParser, createSentencepieceEncodingHandler(spp_nerd_v2));
|
||||
@ -534,6 +556,101 @@ router.post('/openai/count', jsonParser, async function (req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/remote/kobold/count', jsonParser, async function (request, response) {
|
||||
if (!request.body) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
const text = String(request.body.text) || '';
|
||||
const baseUrl = String(request.body.url);
|
||||
|
||||
try {
|
||||
const args = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ 'prompt': text }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
let url = String(baseUrl).replace(/\/$/, '');
|
||||
url += '/extra/tokencount';
|
||||
|
||||
const result = await fetch(url, args);
|
||||
|
||||
if (!result.ok) {
|
||||
console.log(`API returned error: ${result.status} ${result.statusText}`);
|
||||
return response.send({ error: true });
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
const count = data['value'];
|
||||
const ids = data['ids'] ?? [];
|
||||
return response.send({ count, ids });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.send({ error: true });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/remote/textgenerationwebui/encode', jsonParser, async function (request, response) {
|
||||
if (!request.body) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
const text = String(request.body.text) || '';
|
||||
const baseUrl = String(request.body.url);
|
||||
const legacyApi = Boolean(request.body.legacy_api);
|
||||
|
||||
try {
|
||||
const args = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
setAdditionalHeaders(request, args, null);
|
||||
|
||||
// Convert to string + remove trailing slash + /v1 suffix
|
||||
let url = String(baseUrl).replace(/\/$/, '').replace(/\/v1$/, '');
|
||||
|
||||
if (legacyApi) {
|
||||
url += '/v1/token-count';
|
||||
args.body = JSON.stringify({ 'prompt': text });
|
||||
} else {
|
||||
switch (request.body.api_type) {
|
||||
case TEXTGEN_TYPES.TABBY:
|
||||
url += '/v1/token/encode';
|
||||
args.body = JSON.stringify({ 'text': text });
|
||||
break;
|
||||
case TEXTGEN_TYPES.KOBOLDCPP:
|
||||
url += '/api/extra/tokencount';
|
||||
args.body = JSON.stringify({ 'prompt': text });
|
||||
break;
|
||||
case TEXTGEN_TYPES.LLAMACPP:
|
||||
url += '/tokenize';
|
||||
args.body = JSON.stringify({ 'content': text });
|
||||
break;
|
||||
default:
|
||||
url += '/v1/internal/encode';
|
||||
args.body = JSON.stringify({ 'text': text });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await fetch(url, args);
|
||||
|
||||
if (!result.ok) {
|
||||
console.log(`API returned error: ${result.status} ${result.statusText}`);
|
||||
return response.send({ error: true });
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
const count = legacyApi ? data?.results[0]?.tokens : (data?.length ?? data?.value ?? data?.tokens?.length);
|
||||
const ids = legacyApi ? [] : (data?.tokens ?? data?.ids ?? []);
|
||||
|
||||
return response.send({ count, ids });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.send({ error: true });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
TEXT_COMPLETION_MODELS,
|
||||
getTokenizerModel,
|
||||
|
@ -106,6 +106,10 @@ router.post('/deepl', jsonParser, async (request, response) => {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
|
||||
request.body.lang = 'ZH';
|
||||
}
|
||||
|
||||
const text = request.body.text;
|
||||
const lang = request.body.lang;
|
||||
const formality = getConfigValue('deepl.formality', 'default');
|
||||
@ -221,7 +225,7 @@ router.post('/deeplx', jsonParser, async (request, response) => {
|
||||
|
||||
const text = request.body.text;
|
||||
let lang = request.body.lang;
|
||||
if (request.body.lang === 'zh-CN') {
|
||||
if (request.body.lang === 'zh-CN' || request.body.lang === 'zh-TW') {
|
||||
lang = 'ZH';
|
||||
}
|
||||
|
||||
|
@ -12,12 +12,13 @@ const { jsonParser } = require('../express-common');
|
||||
*/
|
||||
async function getVector(source, text) {
|
||||
switch (source) {
|
||||
case 'mistral':
|
||||
case 'openai':
|
||||
return require('../openai-vectors').getOpenAIVector(text);
|
||||
return require('../openai-vectors').getOpenAIVector(text, source);
|
||||
case 'transformers':
|
||||
return require('../embedding').getTransformersVector(text);
|
||||
case 'palm':
|
||||
return require('../palm-vectors').getPaLMVector(text);
|
||||
return require('../makersuite-vectors').getMakerSuiteVector(text);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown vector source ${source}`);
|
||||
@ -196,7 +197,7 @@ router.post('/purge', jsonParser, async (req, res) => {
|
||||
|
||||
const collectionId = String(req.body.collectionId);
|
||||
|
||||
const sources = ['transformers', 'openai'];
|
||||
const sources = ['transformers', 'openai', 'palm'];
|
||||
for (const source of sources) {
|
||||
const index = await getIndex(collectionId, source, false);
|
||||
|
||||
|
@ -6,15 +6,15 @@ const { SECRET_KEYS, readSecret } = require('./endpoints/secrets');
|
||||
* @param {string} text - The text to get the vector for
|
||||
* @returns {Promise<number[]>} - The vector for the text
|
||||
*/
|
||||
async function getPaLMVector(text) {
|
||||
const key = readSecret(SECRET_KEYS.PALM);
|
||||
async function getMakerSuiteVector(text) {
|
||||
const key = readSecret(SECRET_KEYS.MAKERSUITE);
|
||||
|
||||
if (!key) {
|
||||
console.log('No PaLM key found');
|
||||
throw new Error('No PaLM key found');
|
||||
console.log('No MakerSuite key found');
|
||||
throw new Error('No MakerSuite key found');
|
||||
}
|
||||
|
||||
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta2/models/embedding-gecko-001:embedText?key=${key}`, {
|
||||
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/embedding-gecko-001:embedText?key=${key}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -26,8 +26,8 @@ async function getPaLMVector(text) {
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.log('PaLM request failed', response.statusText, text);
|
||||
throw new Error('PaLM request failed');
|
||||
console.log('MakerSuite request failed', response.statusText, text);
|
||||
throw new Error('MakerSuite request failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@ -39,5 +39,5 @@ async function getPaLMVector(text) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPaLMVector,
|
||||
getMakerSuiteVector,
|
||||
};
|
@ -2,7 +2,7 @@
|
||||
* When applied, this middleware will ensure the request contains the required header for basic authentication and only
|
||||
* allow access to the endpoint after successful authentication.
|
||||
*/
|
||||
const { getConfig } = require('./../util.js');
|
||||
const { getConfig } = require('../util.js');
|
||||
|
||||
const unauthorizedResponse = (res) => {
|
||||
res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
|
63
src/middleware/whitelist.js
Normal file
63
src/middleware/whitelist.js
Normal file
@ -0,0 +1,63 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ipaddr = require('ipaddr.js');
|
||||
const ipMatching = require('ip-matching');
|
||||
|
||||
const { color, getConfigValue } = require('../util');
|
||||
|
||||
const whitelistPath = path.join(process.cwd(), './whitelist.txt');
|
||||
let whitelist = getConfigValue('whitelist', []);
|
||||
let knownIPs = new Set();
|
||||
const listen = getConfigValue('listen', false);
|
||||
const whitelistMode = getConfigValue('whitelistMode', true);
|
||||
|
||||
if (fs.existsSync(whitelistPath)) {
|
||||
try {
|
||||
let whitelistTxt = fs.readFileSync(whitelistPath, 'utf-8');
|
||||
whitelist = whitelistTxt.split('\n').filter(ip => ip).map(ip => ip.trim());
|
||||
} catch (e) {
|
||||
// Ignore errors that may occur when reading the whitelist (e.g. permissions)
|
||||
}
|
||||
}
|
||||
|
||||
function getIpFromRequest(req) {
|
||||
let clientIp = req.connection.remoteAddress;
|
||||
let ip = ipaddr.parse(clientIp);
|
||||
// Check if the IP address is IPv4-mapped IPv6 address
|
||||
if (ip.kind() === 'ipv6' && ip instanceof ipaddr.IPv6 && ip.isIPv4MappedAddress()) {
|
||||
const ipv4 = ip.toIPv4Address().toString();
|
||||
clientIp = ipv4;
|
||||
} else {
|
||||
clientIp = ip;
|
||||
clientIp = clientIp.toString();
|
||||
}
|
||||
return clientIp;
|
||||
}
|
||||
|
||||
const whitelistMiddleware = function (req, res, next) {
|
||||
const clientIp = getIpFromRequest(req);
|
||||
|
||||
if (listen && !knownIPs.has(clientIp)) {
|
||||
const userAgent = req.headers['user-agent'];
|
||||
console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
|
||||
knownIPs.add(clientIp);
|
||||
|
||||
// Write access log
|
||||
const timestamp = new Date().toISOString();
|
||||
const log = `${timestamp} ${clientIp} ${userAgent}\n`;
|
||||
fs.appendFile('access.log', log, (err) => {
|
||||
if (err) {
|
||||
console.error('Failed to write access log:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//clientIp = req.connection.remoteAddress.split(':').pop();
|
||||
if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))) {
|
||||
console.log(color.red('Forbidden: Connection attempt from ' + clientIp + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n'));
|
||||
return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + clientIp + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = whitelistMiddleware;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user